From 779ed892b533b4b73eb05a18a341c3314476a56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Pestana?= Date: Thu, 23 Feb 2023 11:21:00 +0000 Subject: [PATCH] Abstracts elections-phragmen pallet to use NposSolver (#12588) * Abstracts elections-phragmen pallet to use NposSolver * Update frame/elections-phragmen/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Update frame/elections-phragmen/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * changes the name of the pallet; adds changelog * update changelog * Adds weight testing * Adds log macro_rules * renames elections-phragment dir to elections * weights rename * fixes typo in cargo toml * pre/post solve weight scafolding * refactor do_post_election * refactors into pre and post election solve for independent benchmarking * deconstructs PreElectionResults struct * updates benchmarking pre and post election solve; mock weights * Update frame/elections/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Update frame/elections/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * addresses PR comments * adds pre_solve and post_sove weights * Adds comments on election pallet id param name change * ".git/.scripts/bench-bot.sh" pallet dev pallet_elections * Finishes pre-post solve weights * Update frame/elections/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Update frame/elections/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Addresses PR comments: no panic in on_init path; nits * Fixes node build * Implements approval voting to use as a `NposSolver` (#13367) * Implements the approval voting methods in sp_npos_elections * fmt * remove unecessary file * comment clarification * re-run weights * fix typo * updates MaxVoters in tests for integrity_tests to pass * Refactors election provider support benchmarks outside its own crate (#13431) * Refactors election provider support benchmarks outside its own crate --------- Co-authored-by: command-bot <> --------- Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> Co-authored-by: parity-processbot <> Co-authored-by: Ross Bulat --- Cargo.toml | 3 +- bin/node/runtime/Cargo.toml | 12 +- bin/node/runtime/src/lib.rs | 30 +- .../election-provider-multi-phase/Cargo.toml | 2 - frame/election-provider-support/Cargo.toml | 8 +- .../benchmarking/Cargo.toml | 37 -- .../src/lib.rs => src/benchmarking.rs} | 18 +- frame/election-provider-support/src/lib.rs | 26 + .../election-provider-support/src/onchain.rs | 30 +- .../CHANGELOG.md | 11 + .../Cargo.toml | 5 +- .../README.md | 6 +- .../src/benchmarking.rs | 57 ++- .../src/lib.rs | 482 +++++++++++------- .../src/migrations/mod.rs | 0 .../src/migrations/v3.rs | 0 .../src/migrations/v4.rs | 0 .../src/migrations/v5.rs | 0 frame/elections/src/weights.rs | 397 +++++++++++++++ .../npos-elections/src/approval_voting.rs | 79 +++ primitives/npos-elections/src/lib.rs | 4 + primitives/npos-elections/src/phragmen.rs | 2 +- primitives/npos-elections/src/tests.rs | 49 +- scripts/run_all_benchmarks.sh | 2 - utils/frame/remote-externalities/Cargo.toml | 2 +- 25 files changed, 978 insertions(+), 284 deletions(-) delete mode 100644 frame/election-provider-support/benchmarking/Cargo.toml rename frame/election-provider-support/{benchmarking/src/lib.rs => src/benchmarking.rs} (86%) rename frame/{elections-phragmen => elections}/CHANGELOG.md (80%) rename frame/{elections-phragmen => elections}/Cargo.toml (90%) rename frame/{elections-phragmen => elections}/README.md (94%) rename frame/{elections-phragmen => elections}/src/benchmarking.rs (85%) rename frame/{elections-phragmen => elections}/src/lib.rs (90%) rename frame/{elections-phragmen => elections}/src/migrations/mod.rs (100%) rename frame/{elections-phragmen => elections}/src/migrations/v3.rs (100%) rename frame/{elections-phragmen => elections}/src/migrations/v4.rs (100%) rename frame/{elections-phragmen => elections}/src/migrations/v5.rs (100%) create mode 100644 frame/elections/src/weights.rs create mode 100644 primitives/npos-elections/src/approval_voting.rs diff --git a/Cargo.toml b/Cargo.toml index 53370d9921814..68f69aa7f982c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,10 +98,9 @@ members = [ "frame/democracy", "frame/fast-unstake", "frame/try-runtime", - "frame/elections-phragmen", + "frame/elections", "frame/election-provider-multi-phase", "frame/election-provider-support", - "frame/election-provider-support/benchmarking", "frame/election-provider-support/solution-type", "frame/election-provider-support/solution-type/fuzzer", "frame/examples/basic", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index d244304526636..acb2cd58f8a0c 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -69,8 +69,7 @@ pallet-conviction-voting = { version = "4.0.0-dev", default-features = false, pa pallet-core-fellowship = { version = "4.0.0-dev", default-features = false, path = "../../../frame/core-fellowship" } pallet-democracy = { version = "4.0.0-dev", default-features = false, path = "../../../frame/democracy" } pallet-election-provider-multi-phase = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-multi-phase" } -pallet-election-provider-support-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../../../frame/election-provider-support/benchmarking", optional = true } -pallet-elections-phragmen = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections-phragmen" } +pallet-elections = { version = "5.0.0-dev", default-features = false, path = "../../../frame/elections" } pallet-fast-unstake = { version = "4.0.0-dev", default-features = false, path = "../../../frame/fast-unstake" } pallet-nis = { version = "4.0.0-dev", default-features = false, path = "../../../frame/nis" } pallet-grandpa = { version = "4.0.0-dev", default-features = false, path = "../../../frame/grandpa" } @@ -130,7 +129,6 @@ std = [ "sp-sandbox/std", "pallet-whitelist/std", "pallet-offences-benchmarking?/std", - "pallet-election-provider-support-benchmarking?/std", "pallet-asset-tx-payment/std", "frame-system-benchmarking?/std", "frame-election-provider-support/std", @@ -153,7 +151,7 @@ std = [ "pallet-conviction-voting/std", "pallet-core-fellowship/std", "pallet-democracy/std", - "pallet-elections-phragmen/std", + "pallet-elections/std", "pallet-fast-unstake/std", "frame-executive/std", "pallet-nis/std", @@ -228,6 +226,7 @@ runtime-benchmarks = [ "frame-benchmarking-pallet-pov/runtime-benchmarks", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "frame-election-provider-support/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "pallet-alliance/runtime-benchmarks", "pallet-assets/runtime-benchmarks", @@ -242,8 +241,7 @@ runtime-benchmarks = [ "pallet-core-fellowship/runtime-benchmarks", "pallet-democracy/runtime-benchmarks", "pallet-election-provider-multi-phase/runtime-benchmarks", - "pallet-election-provider-support-benchmarking/runtime-benchmarks", - "pallet-elections-phragmen/runtime-benchmarks", + "pallet-elections/runtime-benchmarks", "pallet-fast-unstake/runtime-benchmarks", "pallet-nis/runtime-benchmarks", "pallet-grandpa/runtime-benchmarks", @@ -302,7 +300,7 @@ try-runtime = [ "pallet-core-fellowship/try-runtime", "pallet-democracy/try-runtime", "pallet-election-provider-multi-phase/try-runtime", - "pallet-elections-phragmen/try-runtime", + "pallet-elections/try-runtime", "pallet-fast-unstake/try-runtime", "pallet-nis/try-runtime", "pallet-grandpa/try-runtime", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 433ca8e8b839e..703025b7275d2 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -24,7 +24,8 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_election_provider_support::{ - onchain, BalancingConfig, ElectionDataProvider, SequentialPhragmen, VoteWeight, + onchain, weights::SubstrateWeight, ApprovalVoting, BalancingConfig, ElectionDataProvider, + SequentialPhragmen, VoteWeight, }; use frame_support::{ construct_runtime, @@ -1023,18 +1024,21 @@ parameter_types! { pub const TermDuration: BlockNumber = 7 * DAYS; pub const DesiredMembers: u32 = 13; pub const DesiredRunnersUp: u32 = 7; - pub const MaxVotesPerVoter: u32 = 16; pub const MaxVoters: u32 = 512; pub const MaxCandidates: u32 = 64; - pub const ElectionsPhragmenPalletId: LockIdentifier = *b"phrelect"; + pub const MaxVotesPerVoter: u32 = 16; + // The ElectionsPalletId parameter name was changed along with the renaming of the elections + // pallet, but we keep the same lock ID to prevent a migration from current runtimes. + // Related to https://github.com/paritytech/substrate/issues/8250 + pub const ElectionsPalletId: LockIdentifier = *b"phrelect"; } // Make sure that there are no more than `MaxMembers` members elected via elections-phragmen. const_assert!(DesiredMembers::get() <= CouncilMaxMembers::get()); -impl pallet_elections_phragmen::Config for Runtime { +impl pallet_elections::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type PalletId = ElectionsPhragmenPalletId; + type PalletId = ElectionsPalletId; type Currency = Balances; type ChangeMembers = Council; // NOTE: this implies that council's genesis members cannot be set directly and must come from @@ -1052,7 +1056,9 @@ impl pallet_elections_phragmen::Config for Runtime { type MaxVoters = MaxVoters; type MaxVotesPerVoter = MaxVotesPerVoter; type MaxCandidates = MaxCandidates; - type WeightInfo = pallet_elections_phragmen::weights::SubstrateWeight; + type ElectionSolver = ApprovalVoting; + type SolverWeightInfo = SubstrateWeight; + type WeightInfo = pallet_elections::weights::SubstrateWeight; } parameter_types! { @@ -1761,7 +1767,7 @@ construct_runtime!( Democracy: pallet_democracy, Council: pallet_collective::, TechnicalCommittee: pallet_collective::, - Elections: pallet_elections_phragmen, + Elections: pallet_elections, TechnicalMembership: pallet_membership::, Grandpa: pallet_grandpa, Treasury: pallet_treasury, @@ -1877,6 +1883,7 @@ mod benches { frame_benchmarking::define_benchmarks!( [frame_benchmarking, BaselineBench::] [frame_benchmarking_pallet_pov, Pov] + [frame_election_provider_support, EPSBench::] [pallet_alliance, Alliance] [pallet_assets, Assets] [pallet_babe, Babe] @@ -1890,8 +1897,7 @@ mod benches { [pallet_core_fellowship, CoreFellowship] [pallet_democracy, Democracy] [pallet_election_provider_multi_phase, ElectionProviderMultiPhase] - [pallet_election_provider_support_benchmarking, EPSBench::] - [pallet_elections_phragmen, Elections] + [pallet_elections, Elections] [pallet_fast_unstake, FastUnstake] [pallet_nis, Nis] [pallet_grandpa, Grandpa] @@ -2371,7 +2377,7 @@ impl_runtime_apis! { // which is why we need these two lines below. use pallet_session_benchmarking::Pallet as SessionBench; use pallet_offences_benchmarking::Pallet as OffencesBench; - use pallet_election_provider_support_benchmarking::Pallet as EPSBench; + use frame_election_provider_support::benchmarking::Pallet as EPSBench; use frame_system_benchmarking::Pallet as SystemBench; use baseline::Pallet as BaselineBench; use pallet_nomination_pools_benchmarking::Pallet as NominationPoolsBench; @@ -2394,14 +2400,14 @@ impl_runtime_apis! { // which is why we need these two lines below. use pallet_session_benchmarking::Pallet as SessionBench; use pallet_offences_benchmarking::Pallet as OffencesBench; - use pallet_election_provider_support_benchmarking::Pallet as EPSBench; + use frame_election_provider_support::benchmarking::Pallet as EPSBench; use frame_system_benchmarking::Pallet as SystemBench; use baseline::Pallet as BaselineBench; use pallet_nomination_pools_benchmarking::Pallet as NominationPoolsBench; impl pallet_session_benchmarking::Config for Runtime {} impl pallet_offences_benchmarking::Config for Runtime {} - impl pallet_election_provider_support_benchmarking::Config for Runtime {} + impl frame_election_provider_support::benchmarking::Config for Runtime {} impl frame_system_benchmarking::Config for Runtime {} impl baseline::Config for Runtime {} impl pallet_nomination_pools_benchmarking::Config for Runtime {} diff --git a/frame/election-provider-multi-phase/Cargo.toml b/frame/election-provider-multi-phase/Cargo.toml index aa734850aae43..996f40febdeba 100644 --- a/frame/election-provider-multi-phase/Cargo.toml +++ b/frame/election-provider-multi-phase/Cargo.toml @@ -33,7 +33,6 @@ frame-election-provider-support = { version = "4.0.0-dev", default-features = fa # Optional imports for benchmarking frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } -pallet-election-provider-support-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../election-provider-support/benchmarking", optional = true } rand = { version = "0.8.5", default-features = false, features = ["alloc", "small_rng"], optional = true } strum = { version = "0.24.1", default-features = false, features = ["derive"], optional = true } @@ -50,7 +49,6 @@ frame-benchmarking = { version = "4.0.0-dev", path = "../benchmarking" } [features] default = ["std"] std = [ - "pallet-election-provider-support-benchmarking?/std", "codec/std", "scale-info/std", "log/std", diff --git a/frame/election-provider-support/Cargo.toml b/frame/election-provider-support/Cargo.toml index 114caee793f1a..f9cb5233ced22 100644 --- a/frame/election-provider-support/Cargo.toml +++ b/frame/election-provider-support/Cargo.toml @@ -23,6 +23,8 @@ sp-runtime = { version = "7.0.0", default-features = false, path = "../../primit sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" } +frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true } + [dev-dependencies] rand = { version = "0.8.5", features = ["small_rng"] } sp-io = { version = "7.0.0", path = "../../primitives/io" } @@ -41,6 +43,10 @@ std = [ "sp-core/std", "sp-runtime/std", "sp-std/std", + + "frame-benchmarking?/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", ] -runtime-benchmarks = [] try-runtime = [] diff --git a/frame/election-provider-support/benchmarking/Cargo.toml b/frame/election-provider-support/benchmarking/Cargo.toml deleted file mode 100644 index bef371ec5efbf..0000000000000 --- a/frame/election-provider-support/benchmarking/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "pallet-election-provider-support-benchmarking" -version = "4.0.0-dev" -authors = ["Parity Technologies "] -edition = "2021" -license = "Apache-2.0" -homepage = "https://substrate.io" -repository = "https://github.com/paritytech/substrate/" -description = "Benchmarking for election provider support onchain config trait" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = [ - "derive", -] } -frame-benchmarking = { version = "4.0.0-dev", default-features = false, optional = true, path = "../../benchmarking" } -frame-election-provider-support = { version = "4.0.0-dev", default-features = false, path = ".." } -frame-system = { version = "4.0.0-dev", default-features = false, path = "../../system" } -sp-npos-elections = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/npos-elections" } -sp-runtime = { version = "7.0.0", default-features = false, path = "../../../primitives/runtime" } - -[features] -default = ["std"] -std = [ - "codec/std", - "frame-benchmarking?/std", - "frame-election-provider-support/std", - "frame-system/std", - "sp-npos-elections/std", - "sp-runtime/std", -] -runtime-benchmarks = [ - "frame-benchmarking/runtime-benchmarks", - "frame-election-provider-support/runtime-benchmarks", -] diff --git a/frame/election-provider-support/benchmarking/src/lib.rs b/frame/election-provider-support/src/benchmarking.rs similarity index 86% rename from frame/election-provider-support/benchmarking/src/lib.rs rename to frame/election-provider-support/src/benchmarking.rs index 774b7036fd413..5c5b952b6dcc9 100644 --- a/frame/election-provider-support/benchmarking/src/lib.rs +++ b/frame/election-provider-support/src/benchmarking.rs @@ -18,12 +18,9 @@ //! Election provider support pallet benchmarking. //! This is separated into its own crate to avoid bloating the size of the runtime. -#![cfg(feature = "runtime-benchmarks")] -#![cfg_attr(not(feature = "std"), no_std)] - +use crate::{ApprovalVoting, NposSolver, PhragMMS, SequentialPhragmen}; use codec::Decode; use frame_benchmarking::v1::{benchmarks, Vec}; -use frame_election_provider_support::{NposSolver, PhragMMS, SequentialPhragmen}; pub struct Pallet(frame_system::Pallet); pub trait Config: frame_system::Config {} @@ -88,4 +85,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 750ccca46213f..423ce3d67e8cb 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -199,6 +199,9 @@ pub use sp_arithmetic; #[doc(hidden)] pub use sp_std; +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + pub mod weights; pub use weights::WeightInfo; @@ -660,6 +663,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 a312562d4944c..9226751f4b21b 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/elections-phragmen/CHANGELOG.md b/frame/elections/CHANGELOG.md similarity index 80% rename from frame/elections-phragmen/CHANGELOG.md rename to frame/elections/CHANGELOG.md index 231de1d2e475e..b173557be6feb 100644 --- a/frame/elections-phragmen/CHANGELOG.md +++ b/frame/elections/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this crate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.0] - UNRELEASED + +### Added + +### Changed +Generalized the pallet to use `NposSolver` instead of hard coding the phragmen algorithm; Changed the name of the pallet from `pallet-elections-phragmen` to `pallet-elections` + +### Fixed + +### Security + ## [4.0.0] - UNRELEASED ### Added diff --git a/frame/elections-phragmen/Cargo.toml b/frame/elections/Cargo.toml similarity index 90% rename from frame/elections-phragmen/Cargo.toml rename to frame/elections/Cargo.toml index ce39e42b8eeb5..14d07eca7dde7 100644 --- a/frame/elections-phragmen/Cargo.toml +++ b/frame/elections/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "pallet-elections-phragmen" +name = "pallet-elections" version = "5.0.0-dev" authors = ["Parity Technologies "] edition = "2021" license = "Apache-2.0" homepage = "https://substrate.io" repository = "https://github.com/paritytech/substrate/" -description = "FRAME pallet based on seq-Phragmén election method." +description = "FRAME pallet for generic elections." readme = "README.md" [package.metadata.docs.rs] @@ -24,6 +24,7 @@ frame-system = { version = "4.0.0-dev", default-features = false, path = "../sys sp-core = { version = "7.0.0", default-features = false, path = "../../primitives/core" } sp-io = { version = "7.0.0", default-features = false, path = "../../primitives/io" } sp-npos-elections = { version = "4.0.0-dev", default-features = false, path = "../../primitives/npos-elections" } +frame-election-provider-support = { version = "4.0.0-dev", default-features = false, path = "../election-provider-support" } sp-runtime = { version = "7.0.0", default-features = false, path = "../../primitives/runtime" } sp-std = { version = "5.0.0", default-features = false, path = "../../primitives/std" } diff --git a/frame/elections-phragmen/README.md b/frame/elections/README.md similarity index 94% rename from frame/elections-phragmen/README.md rename to frame/elections/README.md index 26b3f260da563..0f27bdd189110 100644 --- a/frame/elections-phragmen/README.md +++ b/frame/elections/README.md @@ -1,6 +1,6 @@ -# Phragmén Election Module. +# Generic Elections Module. -An election module based on sequential phragmen. +A generic elections module. ### Term and Round @@ -60,7 +60,7 @@ being re-elected at the end of each round. ### Module Information -- [`election_sp_phragmen::Config`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/trait.Config.html) +- [`elections::Config`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/trait.Config.html) - [`Call`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/enum.Call.html) - [`Module`](https://docs.rs/pallet-elections-phragmen/latest/pallet_elections_phragmen/struct.Module.html) diff --git a/frame/elections-phragmen/src/benchmarking.rs b/frame/elections/src/benchmarking.rs similarity index 85% rename from frame/elections-phragmen/src/benchmarking.rs rename to frame/elections/src/benchmarking.rs index 56ea19578c8f5..da755fe9b56c4 100644 --- a/frame/elections-phragmen/src/benchmarking.rs +++ b/frame/elections/src/benchmarking.rs @@ -15,14 +15,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Elections-Phragmen pallet benchmarking. +//! Elections pallet benchmarking. #![cfg(feature = "runtime-benchmarks")] use super::*; use frame_benchmarking::v1::{account, benchmarks, whitelist, BenchmarkError, BenchmarkResult}; -use frame_support::{dispatch::DispatchResultWithPostInfo, traits::OnInitialize}; +use frame_support::dispatch::DispatchResultWithPostInfo; use frame_system::RawOrigin; use crate::Pallet as Elections; @@ -37,7 +37,7 @@ fn endowed_account(name: &'static str, index: u32) -> T::AccountId { let amount = default_stake::(T::MaxVoters::get()) * BalanceOf::::from(BALANCE_FACTOR); let _ = T::Currency::make_free_balance_be(&account, amount); // important to increase the total issuance since T::CurrencyToVote will need it to be sane for - // phragmen to work. + // the election to work. T::Currency::issue(amount); account @@ -122,7 +122,7 @@ fn distribute_voters( fn fill_seats_up_to(m: u32) -> Result, &'static str> { let _ = submit_candidates_with_self_vote::(m, "fill_seats_up_to")?; assert_eq!(>::candidates().len() as u32, m, "wrong number of candidates."); - >::do_phragmen(); + >::do_election(); assert_eq!(>::candidates().len(), 0, "some candidates remaining."); assert_eq!( >::members().len() + >::runners_up().len(), @@ -382,11 +382,11 @@ benchmarks! { assert_eq!(>::iter().count() as u32, 0); } - election_phragmen { - // This is just to focus on phragmen in the context of this module. We always select 20 - // members, this is hard-coded in the runtime and cannot be trivially changed at this stage. - // Yet, change the number of voters, candidates and edge per voter to see the impact. Note - // that we give all candidates a self vote to make sure they are all considered. + pre_solve_election { + // We always select 20 members, this is hard-coded in the runtime and cannot be trivially + // changed at this stage. Yet, change the number of voters, candidates and edge per voter + // to see the impact. Note that we give all candidates a self vote to make sure they are + // all considered. let c in 1 .. T::MaxCandidates::get(); let v in 1 .. T::MaxVoters::get(); let e in (T::MaxVoters::get()) .. T::MaxVoters::get() * T::MaxVotesPerVoter::get(); @@ -404,7 +404,44 @@ benchmarks! { let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; let _ = distribute_voters::(all_candidates, v.saturating_sub(c), votes_per_voter as usize)?; }: { - >::on_initialize(T::TermDuration::get()); + >::do_pre_solve_election().unwrap(); + } + + post_solve_election { + // We always select 20 members, this is hard-coded in the runtime and cannot be trivially + // changed at this stage. Yet, change the number of voters, candidates and edge per voter + // to see the impact. Note that we give all candidates a self vote to make sure they are + // all considered. + let c in 1 .. T::MaxCandidates::get(); + let v in 1 .. T::MaxVoters::get(); + let e in (T::MaxVoters::get()) .. T::MaxVoters::get() as u32 * T::MaxVotesPerVoter::get() as u32; + clean::(); + + // so we have a situation with v and e. we want e to basically always be in the range of `e + // -> e * T::MaxVotesPerVoter`, but we cannot express that now with the benchmarks. So what we do + // is: when c is being iterated, v, and e are max and fine. when v is being iterated, e is + // being set to max and this is a problem. In these cases, we cap e to a lower value, namely + // v * `T::MaxVotesPerVoter`. when e is being iterated, v is at max, and again fine. all in all, + // votes_per_voter can never be more than `T::MaxVotesPerVoter`. Note that this might cause `v` to be + // an overestimate. + let votes_per_voter = (e / v).min(T::MaxVotesPerVoter::get() as u32); + + let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; + let _ = distribute_voters::(all_candidates, v.saturating_sub(c), votes_per_voter as usize)?; + + let pre_election_result = >::do_pre_solve_election().unwrap(); + let election_result = T::ElectionSolver::solve( + pre_election_result.num_to_elect, + pre_election_result.candidate_ids, + pre_election_result.voters_and_votes, + ).unwrap(); + + }: { + >::do_post_solve_election( + election_result.winners, + pre_election_result.candidates_and_deposit, + pre_election_result.voters_and_stakes, + ); } verify { assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(c)); diff --git a/frame/elections-phragmen/src/lib.rs b/frame/elections/src/lib.rs similarity index 90% rename from frame/elections-phragmen/src/lib.rs rename to frame/elections/src/lib.rs index d83c94db139a4..7d0608eb8c2d5 100644 --- a/frame/elections-phragmen/src/lib.rs +++ b/frame/elections/src/lib.rs @@ -15,9 +15,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! # Phragmén Election Module. +//! # Generic Election Module. //! -//! An election module based on sequential phragmen. +//! An election module based on a generic election algorithm. //! //! ### Term and Round //! @@ -99,6 +99,7 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Decode, Encode}; +use frame_election_provider_support::NposSolver; use frame_support::{ traits::{ defensive_prelude::*, ChangeMembers, Contains, ContainsLengthBound, Currency, @@ -108,12 +109,12 @@ use frame_support::{ weights::Weight, }; use scale_info::TypeInfo; -use sp_npos_elections::{ElectionResult, ExtendedBalance}; +use sp_npos_elections::{ElectionResult, ExtendedBalance, VoteWeight}; use sp_runtime::{ traits::{Saturating, StaticLookup, Zero}, - DispatchError, Perbill, RuntimeDebug, + DispatchError, RuntimeDebug, }; -use sp_std::{cmp::Ordering, prelude::*}; +use sp_std::{cmp::Ordering, iter::IntoIterator, prelude::*}; mod benchmarking; pub mod weights; @@ -122,7 +123,17 @@ pub use weights::WeightInfo; /// All migrations. pub mod migrations; -const LOG_TARGET: &str = "runtime::elections-phragmen"; +pub(crate) const LOG_TARGET: &str = "runtime::elections"; + +// logging helper. +macro_rules! log { + ($level:tt, $patter:expr $(, $values:expr)* $(,)?) => { + log::$level!( + target: crate::LOG_TARGET, + concat!("[{:?}] 🗳️ ", $patter), >::block_number() $(, $values)* + ) + }; +} type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -173,6 +184,17 @@ pub struct SeatHolder { pub deposit: Balance, } +/// The results of running the pre-election step. +#[derive(Debug, Clone)] +struct PreElectionResults { + pub num_to_elect: usize, + pub candidate_ids: Vec, + pub candidates_and_deposit: Vec<(T::AccountId, BalanceOf)>, + pub voters_and_stakes: Vec<(T::AccountId, BalanceOf, Vec)>, + pub voters_and_votes: Vec<(T::AccountId, VoteWeight, Vec)>, + pub num_edges: u32, +} + pub use pallet::*; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; @@ -195,7 +217,7 @@ pub mod pallet { pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; - /// Identifier for the elections-phragmen pallet's lock + /// Identifier for the elections pallet's lock #[pallet::constant] type PalletId: Get; @@ -248,7 +270,7 @@ pub mod pallet { #[pallet::constant] type TermDuration: Get; - /// The maximum number of candidates in a phragmen election. + /// The maximum number of candidates in an election. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and /// consider how it will impact `T::WeightInfo::election_phragmen`. @@ -257,7 +279,7 @@ pub mod pallet { #[pallet::constant] type MaxCandidates: Get; - /// The maximum number of voters to allow in a phragmen election. + /// The maximum number of voters to allow in an election. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and /// consider how it will impact `T::WeightInfo::election_phragmen`. @@ -266,6 +288,12 @@ pub mod pallet { #[pallet::constant] type MaxVoters: Get; + /// Something that will calculate the result of elections. + type ElectionSolver: NposSolver; + + /// Weight information for the [`Config::ElectionSolver`]. + type SolverWeightInfo: frame_election_provider_support::WeightInfo; + /// Maximum numbers of votes per voter. /// /// Warning: This impacts the size of the election which is run onchain. Chose wisely, and @@ -285,7 +313,7 @@ pub mod pallet { fn on_initialize(n: T::BlockNumber) -> Weight { let term_duration = T::TermDuration::get(); if !term_duration.is_zero() && (n % term_duration).is_zero() { - Self::do_phragmen() + Self::do_election() } else { Weight::zero() } @@ -294,11 +322,26 @@ pub mod pallet { fn integrity_test() { let block_weight = T::BlockWeights::get().max_block; // mind the order. - let election_weight = T::WeightInfo::election_phragmen( + let pre_solve_weight = T::WeightInfo::pre_solve_election( T::MaxCandidates::get(), T::MaxVoters::get(), T::MaxVotesPerVoter::get() * T::MaxVoters::get(), ); + // mind the order. + let post_solve_weight = T::WeightInfo::post_solve_election( + T::MaxCandidates::get(), + T::MaxVoters::get(), + T::MaxVotesPerVoter::get() * T::MaxVoters::get(), + ); + // mind the order. + let election_weight = T::ElectionSolver::weight::( + T::MaxCandidates::get(), + T::MaxVoters::get(), + T::MaxVotesPerVoter::get() * T::MaxVoters::get(), + ); + let election_weight = pre_solve_weight + .saturating_sub(post_solve_weight) + .saturating_add(election_weight); let to_seconds = |w: &Weight| { w.ref_time() as f32 / @@ -533,8 +576,8 @@ pub mod pallet { /// the outgoing member is slashed. /// /// If a runner-up is available, then the best runner-up will be removed and replaces the - /// outgoing member. Otherwise, if `rerun_election` is `true`, a new phragmen election is - /// started, else, nothing happens. + /// outgoing member. Otherwise, if `rerun_election` is `true`, a new election is started, + /// else, nothing happens. /// /// If `slash_bond` is set to true, the bond of the member being removed is slashed. Else, /// it is returned. @@ -564,7 +607,7 @@ pub mod pallet { Self::deposit_event(Event::MemberKicked { member: who }); if rerun_election { - Self::do_phragmen(); + Self::do_election(); } // no refund needed. @@ -663,6 +706,8 @@ pub mod pallet { InvalidRenouncing, /// Prediction regarding replacement after member removal is wrong. InvalidReplacement, + /// No candidates for the next term. + EmptyTerm, } /// The current elected members. @@ -741,7 +786,7 @@ pub mod pallet { Members::::mutate(|members| { match members.binary_search_by(|m| m.who.cmp(member)) { Ok(_) => { - panic!("Duplicate member in elections-phragmen genesis: {}", member) + panic!("Duplicate member in elections genesis: {}", member) }, Err(pos) => members.insert( pos, @@ -933,11 +978,77 @@ impl Pallet { debug_assert!(_remainder.is_zero()); } - /// Run the phragmen election with all required side processes and state updates, if election - /// succeeds. Else, it will emit an `ElectionError` event. + /// Run an election with all required side processes and state updates, if election + /// succeeds. Else, it will emit an `ElectionError` event. The election algorithm is defined + /// by the implementor of `Self::ElectionSolver`. /// /// Calls the appropriate [`ChangeMembers`] function variant internally. - fn do_phragmen() -> Weight { + fn do_election() -> Weight { + let PreElectionResults { + num_to_elect, + candidate_ids, + candidates_and_deposit, + voters_and_stakes, + voters_and_votes, + num_edges, + } = match Self::do_pre_solve_election() { + Ok(results) => results, + Err(err) => match err { + Error::EmptyTerm => { + Self::deposit_event(Event::EmptyTerm); + return T::DbWeight::get().reads(3) + }, + Error::TooManyVotes => { + Self::deposit_event(Event::ElectionError); + log!(error, "Failed to run election. Number of voters exceeded",); + let max_voters = ::MaxVoters::get() as usize; + return T::DbWeight::get().reads(3 + max_voters as u64) + }, + _ => { + log!(error, "Unexpected pre-election error",); + let max_voters = ::MaxVoters::get() as usize; + return T::DbWeight::get().reads(3 + max_voters as u64) + }, + }, + }; + + let num_candidates = candidates_and_deposit.len() as u32; + let num_voters = voters_and_votes.len() as u32; + let num_edges = num_edges; + + let election_winners = + T::ElectionSolver::solve(num_to_elect, candidate_ids, voters_and_votes) + .map( + |ElectionResult::< + T::AccountId, + ::Accuracy, + > { + winners, + assignments: _, + }| winners, + ) + .map_err(|e| { + log!(warn, "Failed to run election [{:?}].", e); + Self::deposit_event(Event::ElectionError); + }); + + let post_election_weight = if let Ok(winners) = election_winners { + Self::do_post_solve_election(winners, candidates_and_deposit, voters_and_stakes); + T::WeightInfo::post_solve_election(num_candidates, num_voters, num_edges) + } else { + Weight::zero() + }; + + T::ElectionSolver::weight::(num_candidates, num_voters, num_edges) + .saturating_add(T::WeightInfo::pre_solve_election( + num_candidates, + num_voters, + num_edges, + )) + .saturating_add(post_election_weight) + } + + fn do_pre_solve_election() -> Result, Error> { let desired_seats = T::DesiredMembers::get() as usize; let desired_runners_up = T::DesiredRunnersUp::get() as usize; let num_to_elect = desired_runners_up + desired_seats; @@ -947,44 +1058,33 @@ impl Pallet { candidates_and_deposit.append(&mut Self::implicit_candidates_with_deposit()); if candidates_and_deposit.len().is_zero() { - Self::deposit_event(Event::EmptyTerm); - return T::DbWeight::get().reads(3) + return Err(Error::EmptyTerm) } - // All of the new winners that come out of phragmen will thus have a deposit recorded. + // All of the new winners that come out of the election will thus have a deposit recorded. let candidate_ids = candidates_and_deposit.iter().map(|(x, _)| x).cloned().collect::>(); // helper closures to deal with balance/stake. let total_issuance = T::Currency::total_issuance(); let to_votes = |b: BalanceOf| T::CurrencyToVote::to_vote(b, total_issuance); - let to_balance = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance); let mut num_edges: u32 = 0; let max_voters = ::MaxVoters::get() as usize; // used for prime election. let mut voters_and_stakes = Vec::new(); - match Voting::::iter().try_for_each(|(voter, Voter { stake, votes, .. })| { + + Voting::::iter().try_for_each(|(voter, Voter { stake, votes, .. })| { if voters_and_stakes.len() < max_voters { voters_and_stakes.push((voter, stake, votes)); Ok(()) } else { - Err(()) + Err(Error::TooManyVotes) } - }) { - Ok(_) => (), - Err(_) => { - log::error!( - target: LOG_TARGET, - "Failed to run election. Number of voters exceeded", - ); - Self::deposit_event(Event::ElectionError); - return T::DbWeight::get().reads(3 + max_voters as u64) - }, - } + })?; - // used for phragmen. + // used for elections. let voters_and_votes = voters_and_stakes .iter() .cloned() @@ -994,157 +1094,144 @@ impl Pallet { }) .collect::>(); - let weight_candidates = candidates_and_deposit.len() as u32; - let weight_voters = voters_and_votes.len() as u32; - let weight_edges = num_edges; - let _ = - sp_npos_elections::seq_phragmen(num_to_elect, candidate_ids, voters_and_votes, None) - .map(|ElectionResult:: { winners, assignments: _ }| { - // this is already sorted by id. - let old_members_ids_sorted = >::take() - .into_iter() - .map(|m| m.who) - .collect::>(); - // this one needs a sort by id. - let mut old_runners_up_ids_sorted = >::take() - .into_iter() - .map(|r| r.who) - .collect::>(); - old_runners_up_ids_sorted.sort(); - - // filter out those who end up with no backing stake. - let mut new_set_with_stake = winners - .into_iter() - .filter_map( - |(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) }, - ) - .collect::)>>(); - - // OPTIMIZATION NOTE: we could bail out here if `new_set.len() == 0`. There - // isn't much left to do. Yet, re-arranging the code would require duplicating - // the slashing of exposed candidates, cleaning any previous members, and so on. - // For now, in favor of readability and veracity, we keep it simple. - - // split new set into winners and runners up. - let split_point = desired_seats.min(new_set_with_stake.len()); - let mut new_members_sorted_by_id = - new_set_with_stake.drain(..split_point).collect::>(); - new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0)); - - // all the rest will be runners-up - new_set_with_stake.reverse(); - let new_runners_up_sorted_by_rank = new_set_with_stake; - let mut new_runners_up_ids_sorted = new_runners_up_sorted_by_rank - .iter() - .map(|(r, _)| r.clone()) - .collect::>(); - new_runners_up_ids_sorted.sort(); - - // Now we select a prime member using a [Borda - // count](https://en.wikipedia.org/wiki/Borda_count). We weigh everyone's vote for - // that new member by a multiplier based on the order of the votes. i.e. the - // first person a voter votes for gets a 16x multiplier, the next person gets a - // 15x multiplier, an so on... (assuming `T::MaxVotesPerVoter` = 16) - let mut prime_votes = new_members_sorted_by_id - .iter() - .map(|c| (&c.0, BalanceOf::::zero())) - .collect::>(); - for (_, stake, votes) in voters_and_stakes.into_iter() { - for (vote_multiplier, who) in - votes.iter().enumerate().map(|(vote_position, who)| { - ((T::MaxVotesPerVoter::get() as usize - vote_position) as u32, who) - }) { - if let Ok(i) = prime_votes.binary_search_by_key(&who, |k| k.0) { - prime_votes[i].1 = prime_votes[i] - .1 - .saturating_add(stake.saturating_mul(vote_multiplier.into())); - } - } - } - // We then select the new member with the highest weighted stake. In the case of - // a tie, the last person in the list with the tied score is selected. This is - // the person with the "highest" account id based on the sort above. - let prime = prime_votes.into_iter().max_by_key(|x| x.1).map(|x| x.0.clone()); - - // new_members_sorted_by_id is sorted by account id. - let new_members_ids_sorted = new_members_sorted_by_id - .iter() - .map(|(m, _)| m.clone()) - .collect::>(); - - // report member changes. We compute diff because we need the outgoing list. - let (incoming, outgoing) = T::ChangeMembers::compute_members_diff_sorted( - &new_members_ids_sorted, - &old_members_ids_sorted, - ); - T::ChangeMembers::change_members_sorted( - &incoming, - &outgoing, - &new_members_ids_sorted, - ); - T::ChangeMembers::set_prime(prime); - - // All candidates/members/runners-up who are no longer retaining a position as a - // seat holder will lose their bond. - candidates_and_deposit.iter().for_each(|(c, d)| { - if new_members_ids_sorted.binary_search(c).is_err() && - new_runners_up_ids_sorted.binary_search(c).is_err() - { - let (imbalance, _) = T::Currency::slash_reserved(c, *d); - T::LoserCandidate::on_unbalanced(imbalance); - Self::deposit_event(Event::CandidateSlashed { - candidate: c.clone(), - amount: *d, - }); - } - }); + Ok(PreElectionResults { + num_to_elect, + candidate_ids, + candidates_and_deposit, + voters_and_stakes, + voters_and_votes, + num_edges, + }) + } - // write final values to storage. - let deposit_of_candidate = |x: &T::AccountId| -> BalanceOf { - // defensive-only. This closure is used against the new members and new - // runners-up, both of which are phragmen winners and thus must have - // deposit. - candidates_and_deposit - .iter() - .find_map(|(c, d)| if c == x { Some(*d) } else { None }) - .defensive_unwrap_or_default() - }; - // fetch deposits from the one recorded one. This will make sure that a - // candidate who submitted candidacy before a change to candidacy deposit will - // have the correct amount recorded. - >::put( - new_members_sorted_by_id - .iter() - .map(|(who, stake)| SeatHolder { - deposit: deposit_of_candidate(who), - who: who.clone(), - stake: *stake, - }) - .collect::>(), - ); - >::put( - new_runners_up_sorted_by_rank - .into_iter() - .map(|(who, stake)| SeatHolder { - deposit: deposit_of_candidate(&who), - who, - stake, - }) - .collect::>(), - ); + fn do_post_solve_election( + winners: Vec<(T::AccountId, u128)>, + candidates_and_deposit: Vec<(T::AccountId, BalanceOf)>, + voters_and_stakes: Vec<(T::AccountId, BalanceOf, Vec)>, + ) { + let desired_seats = T::DesiredMembers::get() as usize; + let total_issuance = T::Currency::total_issuance(); + let to_balance = |e: ExtendedBalance| T::CurrencyToVote::to_currency(e, total_issuance); - // clean candidates. - >::kill(); + // this is already sorted by id. + let old_members_ids_sorted = + >::take().into_iter().map(|m| m.who).collect::>(); + // this one needs sorted by id. + let mut old_runners_up_ids_sorted = + >::take().into_iter().map(|r| r.who).collect::>(); + old_runners_up_ids_sorted.sort(); - Self::deposit_event(Event::NewTerm { new_members: new_members_sorted_by_id }); - >::mutate(|v| *v += 1); - }) - .map_err(|e| { - log::error!(target: LOG_TARGET, "Failed to run election [{:?}].", e,); - Self::deposit_event(Event::ElectionError); - }); + // filter out those who end up with no backing stake. + let mut new_set_with_stake = winners + .into_iter() + .filter_map(|(m, b)| if b.is_zero() { None } else { Some((m, to_balance(b))) }) + .collect::)>>(); + + // OPTIMIZATION NOTE: we could bail out here if `new_set.len() == 0`. There + // isn't much left to do. Yet, re-arranging the code would require duplicating + // the slashing of exposed candidates, cleaning any previous members, and so on. + // For now, in favor of readability and veracity, we keep it simple. + + // split new set into winners and runners up. + let split_point = desired_seats.min(new_set_with_stake.len()); + let mut new_members_sorted_by_id = + new_set_with_stake.drain(..split_point).collect::>(); + new_members_sorted_by_id.sort_by(|i, j| i.0.cmp(&j.0)); + + // all the rest will be runners-up + new_set_with_stake.reverse(); + let new_runners_up_sorted_by_rank = new_set_with_stake; + let mut new_runners_up_ids_sorted = + new_runners_up_sorted_by_rank.iter().map(|(r, _)| r.clone()).collect::>(); + new_runners_up_ids_sorted.sort(); + + // Now we select a prime member using a [Borda + // count](https://en.wikipedia.org/wiki/Borda_count). We weigh everyone's vote for + // that new member by a multiplier based on the order of the votes. i.e. the + // first person a voter votes for gets a 16x multiplier, the next person gets a + // 15x multiplier, an so on... (assuming `T::MaxVotesPerVoter` = 16). + let mut prime_votes = new_members_sorted_by_id + .iter() + .map(|c| (&c.0, BalanceOf::::zero())) + .collect::>(); + for (_, stake, votes) in voters_and_stakes.into_iter() { + for (vote_multiplier, who) in votes.iter().enumerate().map(|(vote_position, who)| { + ((T::MaxVotesPerVoter::get() as usize - vote_position) as u32, who) + }) { + if let Ok(i) = prime_votes.binary_search_by_key(&who, |k| k.0) { + prime_votes[i].1 = prime_votes[i] + .1 + .saturating_add(stake.saturating_mul(vote_multiplier.into())); + } + } + } + // We then select the new member with the highest weighted stake. In the case of + // a tie, the last person in the list with the tied score is selected. This is + // the person with the "highest" account id based on the sort above. + let prime = prime_votes.into_iter().max_by_key(|x| x.1).map(|x| x.0.clone()); - T::WeightInfo::election_phragmen(weight_candidates, weight_voters, weight_edges) + // new_members_sorted_by_id is sorted by account id. + let new_members_ids_sorted = new_members_sorted_by_id + .iter() + .map(|(m, _)| m.clone()) + .collect::>(); + + // report member changes. We compute diff because we need the outgoing list. + let (incoming, outgoing) = T::ChangeMembers::compute_members_diff_sorted( + &new_members_ids_sorted, + &old_members_ids_sorted, + ); + T::ChangeMembers::change_members_sorted(&incoming, &outgoing, &new_members_ids_sorted); + T::ChangeMembers::set_prime(prime); + + // All candidates/members/runners-up who are no longer retaining a position as a + // seat holder will lose their bond. + candidates_and_deposit.iter().for_each(|(c, d)| { + if new_members_ids_sorted.binary_search(c).is_err() && + new_runners_up_ids_sorted.binary_search(c).is_err() + { + let (imbalance, _) = T::Currency::slash_reserved(c, *d); + T::LoserCandidate::on_unbalanced(imbalance); + Self::deposit_event(Event::CandidateSlashed { candidate: c.clone(), amount: *d }); + } + }); + + // write final values to storage. + let deposit_of_candidate = |x: &T::AccountId| -> BalanceOf { + // defensive-only. This closure is used against the new members and new + // runners-up, both of which are election winners and thus must have + // deposit. + candidates_and_deposit + .iter() + .find_map(|(c, d)| if c == x { Some(*d) } else { None }) + .defensive_unwrap_or_default() + }; + // fetch deposits from the one recorded one. This will make sure that a + // candidate who submitted candidacy before a change to candidacy deposit will + // have the correct amount recorded. + >::put( + new_members_sorted_by_id + .iter() + .map(|(who, stake)| SeatHolder { + deposit: deposit_of_candidate(who), + who: who.clone(), + stake: *stake, + }) + .collect::>(), + ); + >::put( + new_runners_up_sorted_by_rank + .into_iter() + .map(|(who, stake)| SeatHolder { deposit: deposit_of_candidate(&who), who, stake }) + .collect::>(), + ); + + // clean candidates. + >::kill(); + + log!(info, "New term election successful."); + Self::deposit_event(Event::NewTerm { new_members: new_members_sorted_by_id }); + >::mutate(|v| *v += 1); } } @@ -1195,7 +1282,8 @@ impl ContainsLengthBound for Pallet { #[cfg(test)] mod tests { use super::*; - use crate as elections_phragmen; + use crate as elections; + use frame_election_provider_support::{weights::SubstrateWeight, ApprovalVoting}; use frame_support::{ assert_noop, assert_ok, dispatch::DispatchResultWithPostInfo, @@ -1207,7 +1295,7 @@ mod tests { use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, + BuildStorage, Perbill, }; use substrate_test_utils::assert_eq_uvec; @@ -1310,13 +1398,13 @@ mod tests { } parameter_types! { - pub const ElectionsPhragmenPalletId: LockIdentifier = *b"phrelect"; - pub const PhragmenMaxVoters: u32 = 1000; - pub const PhragmenMaxCandidates: u32 = 100; + pub const ElectionsPalletId: LockIdentifier = *b"phrelect"; + pub const MaxVoters: u32 = 256; + pub const MaxCandidates: u32 = 64; } impl Config for Test { - type PalletId = ElectionsPhragmenPalletId; + type PalletId = ElectionsPalletId; type RuntimeEvent = RuntimeEvent; type Currency = Balances; type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; @@ -1331,9 +1419,11 @@ mod tests { type LoserCandidate = (); type KickedMember = (); type WeightInfo = (); - type MaxVoters = PhragmenMaxVoters; + type MaxVoters = MaxVoters; + type MaxCandidates = MaxCandidates; + type ElectionSolver = ApprovalVoting; + type SolverWeightInfo = SubstrateWeight; type MaxVotesPerVoter = ConstU32<16>; - type MaxCandidates = PhragmenMaxCandidates; } pub type Block = sp_runtime::generic::Block; @@ -1348,7 +1438,7 @@ mod tests { { System: frame_system::{Pallet, Call, Event}, Balances: pallet_balances::{Pallet, Call, Event, Config}, - Elections: elections_phragmen::{Pallet, Call, Event, Config}, + Elections: elections::{Pallet, Call, Event, Config}, } ); @@ -1409,9 +1499,7 @@ mod tests { (6, 60 * self.balance_factor), ], }, - elections: elections_phragmen::GenesisConfig:: { - members: self.genesis_members, - }, + elections: elections::GenesisConfig:: { members: self.genesis_members }, } .build_storage() .unwrap() @@ -1469,7 +1557,7 @@ mod tests { .get(0) .cloned() .map(|lock| { - assert_eq!(lock.id, ElectionsPhragmenPalletId::get()); + assert_eq!(lock.id, ElectionsPalletId::get()); lock.amount }) .unwrap_or_default() @@ -1660,7 +1748,7 @@ mod tests { } #[test] - #[should_panic = "Duplicate member in elections-phragmen genesis: 2"] + #[should_panic = "Duplicate member in elections genesis: 2"] fn genesis_members_cannot_be_duplicate() { ExtBuilder::default() .desired_members(3) diff --git a/frame/elections-phragmen/src/migrations/mod.rs b/frame/elections/src/migrations/mod.rs similarity index 100% rename from frame/elections-phragmen/src/migrations/mod.rs rename to frame/elections/src/migrations/mod.rs diff --git a/frame/elections-phragmen/src/migrations/v3.rs b/frame/elections/src/migrations/v3.rs similarity index 100% rename from frame/elections-phragmen/src/migrations/v3.rs rename to frame/elections/src/migrations/v3.rs diff --git a/frame/elections-phragmen/src/migrations/v4.rs b/frame/elections/src/migrations/v4.rs similarity index 100% rename from frame/elections-phragmen/src/migrations/v4.rs rename to frame/elections/src/migrations/v4.rs diff --git a/frame/elections-phragmen/src/migrations/v5.rs b/frame/elections/src/migrations/v5.rs similarity index 100% rename from frame/elections-phragmen/src/migrations/v5.rs rename to frame/elections/src/migrations/v5.rs diff --git a/frame/elections/src/weights.rs b/frame/elections/src/weights.rs new file mode 100644 index 0000000000000..8caa93f0aa8cd --- /dev/null +++ b/frame/elections/src/weights.rs @@ -0,0 +1,397 @@ +// This file is part of Substrate. + +// Copyright (C) 2022 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. + +//! Autogenerated weights for pallet_elections +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-11-22, STEPS: `50`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! HOSTNAME: `bm3`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// /home/benchbot/cargo_target_dir/production/substrate +// benchmark +// pallet +// --steps=50 +// --repeat=20 +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --json-file=/var/lib/gitlab-runner/builds/zyw4fam_/0/parity/mirrors/substrate/.git/.artifacts/bench.json +// --pallet=pallet_elections +// --chain=dev +// --header=./HEADER-APACHE2 +// --output=./frame/elections/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_elections. +pub trait WeightInfo { + fn vote_equal(v: u32, ) -> Weight; + fn vote_more(v: u32, ) -> Weight; + fn vote_less(v: u32, ) -> Weight; + fn remove_voter() -> Weight; + fn submit_candidacy(c: u32, ) -> Weight; + fn renounce_candidacy_candidate(c: u32, ) -> Weight; + fn renounce_candidacy_members() -> Weight; + fn renounce_candidacy_runners_up() -> Weight; + fn remove_member_without_replacement() -> Weight; + fn remove_member_with_replacement() -> Weight; + fn clean_defunct_voters(v: u32, d: u32, ) -> Weight; + fn pre_solve_election(c: u32, v: u32, e: u32, ) -> Weight; + fn post_solve_election(c: u32, v: u32, e: u32, ) -> Weight; +} + +/// Weights for pallet_elections using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[1, 16]`. + fn vote_equal(v: u32, ) -> Weight { + // Minimum execution time: 37_500 nanoseconds. + Weight::from_ref_time(38_575_649) + // Standard Error: 2_961 + .saturating_add(Weight::from_ref_time(154_225).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[2, 16]`. + fn vote_more(v: u32, ) -> Weight { + // Minimum execution time: 48_836 nanoseconds. + Weight::from_ref_time(49_566_969) + // Standard Error: 3_258 + .saturating_add(Weight::from_ref_time(153_342).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[2, 16]`. + fn vote_less(v: u32, ) -> Weight { + // Minimum execution time: 47_664 nanoseconds. + Weight::from_ref_time(48_768_157) + // Standard Error: 3_321 + .saturating_add(Weight::from_ref_time(215_112).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn remove_voter() -> Weight { + // Minimum execution time: 47_146 nanoseconds. + Weight::from_ref_time(47_846_000) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:1) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + /// The range of component `c` is `[1, 1000]`. + fn submit_candidacy(c: u32, ) -> Weight { + // Minimum execution time: 42_799 nanoseconds. + Weight::from_ref_time(46_920_164) + // Standard Error: 780 + .saturating_add(Weight::from_ref_time(81_672).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Elections Candidates (r:1 w:1) + /// The range of component `c` is `[1, 1000]`. + fn renounce_candidacy_candidate(c: u32, ) -> Weight { + // Minimum execution time: 40_946 nanoseconds. + Weight::from_ref_time(53_109_738) + // Standard Error: 1_220 + .saturating_add(Weight::from_ref_time(60_643).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Elections Members (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Prime (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Members (r:0 w:1) + fn renounce_candidacy_members() -> Weight { + // Minimum execution time: 53_454 nanoseconds. + Weight::from_ref_time(53_921_000) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(4)) + } + // Storage: Elections RunnersUp (r:1 w:1) + fn renounce_candidacy_runners_up() -> Weight { + // Minimum execution time: 41_275 nanoseconds. + Weight::from_ref_time(42_444_000) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + // Storage: Benchmark Override (r:0 w:0) + fn remove_member_without_replacement() -> Weight { + // Minimum execution time: 2_000_000_000 nanoseconds. + Weight::from_ref_time(2_000_000_000_000) + } + // Storage: Elections Members (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Prime (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Members (r:0 w:1) + fn remove_member_with_replacement() -> Weight { + // Minimum execution time: 62_040 nanoseconds. + Weight::from_ref_time(62_569_000) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(5)) + } + // Storage: Elections Voting (r:5001 w:5000) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Candidates (r:1 w:0) + // Storage: Balances Locks (r:5000 w:5000) + // Storage: System Account (r:5000 w:5000) + /// The range of component `v` is `[5000, 10000]`. + /// The range of component `d` is `[0, 5000]`. + fn clean_defunct_voters(v: u32, _d: u32, ) -> Weight { + // Minimum execution time: 298_212_195 nanoseconds. + Weight::from_ref_time(298_678_889_000) + // Standard Error: 264_713 + .saturating_add(Weight::from_ref_time(38_222_955).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(v.into()))) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(v.into()))) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:10001 w:0) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `v` is `[1, 10000]`. + /// The range of component `e` is `[10000, 160000]`. + fn pre_solve_election(_c: u32, v: u32, _e: u32, ) -> Weight { + // Minimum execution time: 6_543_626 nanoseconds. + Weight::from_ref_time(6_627_885_000) + // Standard Error: 14_605 + .saturating_add(Weight::from_ref_time(7_613_226).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(296)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(v.into()))) + } + // Storage: Elections Members (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Elections ElectionRounds (r:1 w:1) + // Storage: Elections Candidates (r:0 w:1) + // Storage: Council Members (r:0 w:1) + // Storage: Council Prime (r:0 w:1) + // Storage: System Account (r:1 w:1) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `v` is `[1, 10000]`. + /// The range of component `e` is `[10000, 160000]`. + fn post_solve_election(c: u32, v: u32, _e: u32, ) -> Weight { + // Minimum execution time: 3_843_566 nanoseconds. + Weight::from_ref_time(3_854_020_000) + // Standard Error: 59_766 + .saturating_add(Weight::from_ref_time(9_622_797).saturating_mul(c.into())) + // Standard Error: 5_975 + .saturating_add(Weight::from_ref_time(541_471).saturating_mul(v.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(6)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(c.into()))) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[1, 16]`. + fn vote_equal(v: u32, ) -> Weight { + // Minimum execution time: 37_500 nanoseconds. + Weight::from_ref_time(38_575_649) + // Standard Error: 2_961 + .saturating_add(Weight::from_ref_time(154_225).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(5)) + .saturating_add(RocksDbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[2, 16]`. + fn vote_more(v: u32, ) -> Weight { + // Minimum execution time: 48_836 nanoseconds. + Weight::from_ref_time(49_566_969) + // Standard Error: 3_258 + .saturating_add(Weight::from_ref_time(153_342).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(5)) + .saturating_add(RocksDbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + /// The range of component `v` is `[2, 16]`. + fn vote_less(v: u32, ) -> Weight { + // Minimum execution time: 47_664 nanoseconds. + Weight::from_ref_time(48_768_157) + // Standard Error: 3_321 + .saturating_add(Weight::from_ref_time(215_112).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(5)) + .saturating_add(RocksDbWeight::get().writes(2)) + } + // Storage: Elections Voting (r:1 w:1) + // Storage: Balances Locks (r:1 w:1) + fn remove_voter() -> Weight { + // Minimum execution time: 47_146 nanoseconds. + Weight::from_ref_time(47_846_000) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(2)) + } + // Storage: Elections Candidates (r:1 w:1) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + /// The range of component `c` is `[1, 1000]`. + fn submit_candidacy(c: u32, ) -> Weight { + // Minimum execution time: 42_799 nanoseconds. + Weight::from_ref_time(46_920_164) + // Standard Error: 780 + .saturating_add(Weight::from_ref_time(81_672).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(3)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + // Storage: Elections Candidates (r:1 w:1) + /// The range of component `c` is `[1, 1000]`. + fn renounce_candidacy_candidate(c: u32, ) -> Weight { + // Minimum execution time: 40_946 nanoseconds. + Weight::from_ref_time(53_109_738) + // Standard Error: 1_220 + .saturating_add(Weight::from_ref_time(60_643).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + // Storage: Elections Members (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Prime (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Members (r:0 w:1) + fn renounce_candidacy_members() -> Weight { + // Minimum execution time: 53_454 nanoseconds. + Weight::from_ref_time(53_921_000) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().writes(4)) + } + // Storage: Elections RunnersUp (r:1 w:1) + fn renounce_candidacy_runners_up() -> Weight { + // Minimum execution time: 41_275 nanoseconds. + Weight::from_ref_time(42_444_000) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(1)) + } + // Storage: Benchmark Override (r:0 w:0) + fn remove_member_without_replacement() -> Weight { + // Minimum execution time: 2_000_000_000 nanoseconds. + Weight::from_ref_time(2_000_000_000_000) + } + // Storage: Elections Members (r:1 w:1) + // Storage: System Account (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Prime (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Council Members (r:0 w:1) + fn remove_member_with_replacement() -> Weight { + // Minimum execution time: 62_040 nanoseconds. + Weight::from_ref_time(62_569_000) + .saturating_add(RocksDbWeight::get().reads(5)) + .saturating_add(RocksDbWeight::get().writes(5)) + } + // Storage: Elections Voting (r:5001 w:5000) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Candidates (r:1 w:0) + // Storage: Balances Locks (r:5000 w:5000) + // Storage: System Account (r:5000 w:5000) + /// The range of component `v` is `[5000, 10000]`. + /// The range of component `d` is `[0, 5000]`. + fn clean_defunct_voters(v: u32, _d: u32, ) -> Weight { + // Minimum execution time: 298_212_195 nanoseconds. + Weight::from_ref_time(298_678_889_000) + // Standard Error: 264_713 + .saturating_add(Weight::from_ref_time(38_222_955).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(v.into()))) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(v.into()))) + } + // Storage: Elections Candidates (r:1 w:0) + // Storage: Elections Members (r:1 w:0) + // Storage: Elections RunnersUp (r:1 w:0) + // Storage: Elections Voting (r:10001 w:0) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `v` is `[1, 10000]`. + /// The range of component `e` is `[10000, 160000]`. + fn pre_solve_election(_c: u32, v: u32, _e: u32, ) -> Weight { + // Minimum execution time: 6_543_626 nanoseconds. + Weight::from_ref_time(6_627_885_000) + // Standard Error: 14_605 + .saturating_add(Weight::from_ref_time(7_613_226).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(296)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(v.into()))) + } + // Storage: Elections Members (r:1 w:1) + // Storage: Elections RunnersUp (r:1 w:1) + // Storage: Council Proposals (r:1 w:0) + // Storage: Elections ElectionRounds (r:1 w:1) + // Storage: Elections Candidates (r:0 w:1) + // Storage: Council Members (r:0 w:1) + // Storage: Council Prime (r:0 w:1) + // Storage: System Account (r:1 w:1) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `v` is `[1, 10000]`. + /// The range of component `e` is `[10000, 160000]`. + fn post_solve_election(c: u32, v: u32, _e: u32, ) -> Weight { + // Minimum execution time: 3_843_566 nanoseconds. + Weight::from_ref_time(3_854_020_000) + // Standard Error: 59_766 + .saturating_add(Weight::from_ref_time(9_622_797).saturating_mul(c.into())) + // Standard Error: 5_975 + .saturating_add(Weight::from_ref_time(541_471).saturating_mul(v.into())) + .saturating_add(RocksDbWeight::get().reads(4)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().writes(6)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(c.into()))) + } +} diff --git a/primitives/npos-elections/src/approval_voting.rs b/primitives/npos-elections/src/approval_voting.rs new file mode 100644 index 0000000000000..2fcf17b60565d --- /dev/null +++ b/primitives/npos-elections/src/approval_voting.rs @@ -0,0 +1,79 @@ +// 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. The weight +/// vector of all voters who contribute to the winners, which for this scheme is always 100% per +/// vote. +/// +/// - 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 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`. +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 716c4b283c68e..a1a38b9b0df12 100644 --- a/primitives/npos-elections/src/lib.rs +++ b/primitives/npos-elections/src/lib.rs @@ -25,6 +25,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 //! @@ -90,6 +92,7 @@ mod mock; #[cfg(test)] mod tests; +pub mod approval_voting; mod assignments; pub mod balancing; pub mod helpers; @@ -100,6 +103,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 c3578065f364c..b7204ce1c34d8 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 72ae9a0222be1..5fea4c2506406 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]; diff --git a/scripts/run_all_benchmarks.sh b/scripts/run_all_benchmarks.sh index b632cb5c12f04..818a5df7f6d95 100755 --- a/scripts/run_all_benchmarks.sh +++ b/scripts/run_all_benchmarks.sh @@ -67,8 +67,6 @@ SUBSTRATE=./target/production/substrate # Manually exclude some pallets. EXCLUDED_PALLETS=( - # Helper pallets - "pallet_election_provider_support_benchmarking" # Pallets without automatic benchmarking "pallet_babe" "pallet_grandpa" diff --git a/utils/frame/remote-externalities/Cargo.toml b/utils/frame/remote-externalities/Cargo.toml index 8611ae4980f12..e55b64efbdc75 100644 --- a/utils/frame/remote-externalities/Cargo.toml +++ b/utils/frame/remote-externalities/Cargo.toml @@ -25,7 +25,7 @@ futures = "0.3" [dev-dependencies] frame-support = { version = "4.0.0-dev", path = "../../../frame/support" } -pallet-elections-phragmen = { version = "5.0.0-dev", path = "../../../frame/elections-phragmen" } +pallet-elections = { version = "5.0.0-dev", path = "../../../frame/elections" } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } [features]