// This file is part of Substrate. // Copyright (C) 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. //! Elections-Phragmen 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_system::RawOrigin; use crate::Pallet as Elections; const BALANCE_FACTOR: u32 = 250; /// grab new account with infinite balance. fn endowed_account(name: &'static str, index: u32) -> T::AccountId { let account: T::AccountId = account(name, index, 0); // Fund each account with at-least their stake but still a sane amount as to not mess up // the vote calculation. 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. let _ = T::Currency::issue(amount); account } /// Account to lookup type of system trait. fn as_lookup(account: T::AccountId) -> AccountIdLookupOf { T::Lookup::unlookup(account) } /// Get a reasonable amount of stake based on the execution trait's configuration fn default_stake(num_votes: u32) -> BalanceOf { let min = T::Currency::minimum_balance(); Elections::::deposit_of(num_votes as usize).max(min) } /// Get the current number of candidates. fn candidate_count() -> u32 { >::decode_len().unwrap_or(0usize) as u32 } /// Add `c` new candidates. fn submit_candidates( c: u32, prefix: &'static str, ) -> Result, &'static str> { (0..c) .map(|i| { let account = endowed_account::(prefix, i); >::submit_candidacy( RawOrigin::Signed(account.clone()).into(), candidate_count::(), ) .map_err(|_| "failed to submit candidacy")?; Ok(account) }) .collect::>() } /// Add `c` new candidates with self vote. fn submit_candidates_with_self_vote( c: u32, prefix: &'static str, ) -> Result, &'static str> { let candidates = submit_candidates::(c, prefix)?; let stake = default_stake::(c); let _ = candidates .iter() .try_for_each(|c| submit_voter::(c.clone(), vec![c.clone()], stake).map(|_| ()))?; Ok(candidates) } /// Submit one voter. fn submit_voter( caller: T::AccountId, votes: Vec, stake: BalanceOf, ) -> DispatchResultWithPostInfo { >::vote(RawOrigin::Signed(caller).into(), votes, stake) } /// create `num_voter` voters who randomly vote for at most `votes` of `all_candidates` if /// available. fn distribute_voters( mut all_candidates: Vec, num_voters: u32, votes: usize, ) -> Result<(), &'static str> { let stake = default_stake::(num_voters); for i in 0..num_voters { // to ensure that votes are different all_candidates.rotate_left(1); let votes = all_candidates.iter().cloned().take(votes).collect::>(); let voter = endowed_account::("voter", i); submit_voter::(voter, votes, stake)?; } Ok(()) } /// Fill the seats of members and runners-up up until `m`. Note that this might include either only /// members, or members and runners-up. 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(); assert_eq!(>::candidates().len(), 0, "some candidates remaining."); assert_eq!( >::members().len() + >::runners_up().len(), m as usize, "wrong number of members and runners-up", ); Ok(>::members() .into_iter() .map(|m| m.who) .chain(>::runners_up().into_iter().map(|r| r.who)) .collect()) } /// removes all the storage items to reverse any genesis state. fn clean() { >::kill(); >::kill(); >::kill(); #[allow(deprecated)] >::remove_all(None); } benchmarks! { // -- Signed ones vote_equal { let v in 1 .. T::MaxVotesPerVoter::get(); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(v); // original votes. let mut votes = all_candidates; submit_voter::(caller.clone(), votes.clone(), stake)?; // new votes. votes.rotate_left(1); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake) vote_more { let v in 2 .. T::MaxVotesPerVoter::get(); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); // Multiply the stake with 10 since we want to be able to divide it by 10 again. let stake = default_stake::(v) * BalanceOf::::from(10u32); // original votes. let mut votes = all_candidates.iter().skip(1).cloned().collect::>(); submit_voter::(caller.clone(), votes.clone(), stake / >::from(10u32))?; // new votes. votes = all_candidates; assert!(votes.len() > >::get(caller.clone()).votes.len()); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake / >::from(10u32)) vote_less { let v in 2 .. T::MaxVotesPerVoter::get(); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(v); // original votes. let mut votes = all_candidates; submit_voter::(caller.clone(), votes.clone(), stake)?; // new votes. votes = votes.into_iter().skip(1).collect::>(); assert!(votes.len() < >::get(caller.clone()).votes.len()); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake) remove_voter { // we fix the number of voted candidates to max let v = T::MaxVotesPerVoter::get(); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(v); submit_voter::(caller.clone(), all_candidates, stake)?; whitelist!(caller); }: _(RawOrigin::Signed(caller)) submit_candidacy { // number of already existing candidates. let c in 1 .. T::MaxCandidates::get(); // we fix the number of members to the number of desired members and runners-up. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let stake = default_stake::(c); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; // create previous candidates; let _ = submit_candidates::(c, "candidates")?; // we assume worse case that: extrinsic is successful and candidate is not duplicate. let candidate_account = endowed_account::("caller", 0); whitelist!(candidate_account); }: _(RawOrigin::Signed(candidate_account.clone()), candidate_count::()) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_candidate { // this will check members, runners-up and candidate for removal. Members and runners-up are // limited by the runtime bound, nonetheless we fill them by `m`. // number of already existing candidates. let c in 1 .. T::MaxCandidates::get(); // we fix the number of members to the number of desired members and runners-up. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; let all_candidates = submit_candidates::(c, "caller")?; let bailing = all_candidates[0].clone(); // Should be ("caller", 0) let count = candidate_count::(); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing), Renouncing::Candidate(count)) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_members { // removing members and runners will be cheaper than a candidate. // we fix the number of members to when members and runners-up to the desired. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let members_and_runners_up = fill_seats_up_to::(m)?; let bailing = members_and_runners_up[0].clone(); assert!(>::is_member(&bailing)); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::Member) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_runners_up { // removing members and runners will be cheaper than a candidate. // we fix the number of members to when members and runners-up to the desired. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let members_and_runners_up = fill_seats_up_to::(m)?; let bailing = members_and_runners_up[T::DesiredMembers::get() as usize + 1].clone(); assert!(>::is_runner_up(&bailing)); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::RunnerUp) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } // We use the max block weight for this extrinsic for now. See below. remove_member_without_replacement {}: { Err(BenchmarkError::Override( BenchmarkResult::from_weight(T::BlockWeights::get().max_block) ))?; } remove_member_with_replacement { // easy case. We have a runner up. Nothing will have that much of an impact. m will be // number of members and runners. There is always at least one runner. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let _ = fill_seats_up_to::(m)?; let removing = as_lookup::(>::members_ids()[0].clone()); }: remove_member(RawOrigin::Root, removing, true, false) verify { // must still have enough members. assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } clean_defunct_voters { // total number of voters. let v in (T::MaxVoters::get() / 2) .. T::MaxVoters::get(); // those that are defunct and need removal. let d in 0 .. (T::MaxVoters::get() / 2); // remove any previous stuff. clean::(); let all_candidates = submit_candidates::(T::MaxCandidates::get(), "candidates")?; distribute_voters::(all_candidates, v, T::MaxVotesPerVoter::get() as usize)?; // all candidates leave. >::kill(); // now everyone is defunct assert!(>::iter().all(|(_, v)| >::is_defunct_voter(&v.votes))); assert_eq!(>::iter().count() as u32, v); let root = RawOrigin::Root; }: _(root, v, d) verify { assert_eq!(>::iter().count() as u32, v - d); } 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. 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(); 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::get()`, 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::get(). 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::get(). Note that this might cause `v` to be an overestimate. let votes_per_voter = (e / v).min(T::MaxVotesPerVoter::get()); 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()); } verify { assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(c)); assert_eq!( >::runners_up().len() as u32, T::DesiredRunnersUp::get().min(c.saturating_sub(T::DesiredMembers::get())), ); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } impl_benchmark_test_suite!( Elections, crate::tests::ExtBuilder::default().desired_members(13).desired_runners_up(7), crate::tests::Test, exec_name = build_and_execute, ); }