// Copyright 2020 Parity Technologies (UK) Ltd. // This file is part of Substrate. // Substrate is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Substrate is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . //! Elections-Phragmen pallet benchmarking. #![cfg(feature = "runtime-benchmarks")] use super::*; use frame_system::RawOrigin; use frame_benchmarking::{benchmarks, account}; use frame_support::traits::OnInitialize; use crate::Module as Elections; const BALANCE_FACTOR: u32 = 250; const MAX_VOTERS: u32 = 500; const MAX_CANDIDATES: u32 = 100; type Lookup = <::Lookup as StaticLookup>::Source; /// grab new account with infinite balance. fn endowed_account(name: &'static str, index: u32) -> T::AccountId { let account: T::AccountId = account(name, index, 0); let amount = default_stake::(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. T::Currency::issue(amount); account } /// Account to lookup type of system trait. fn as_lookup(account: T::AccountId) -> Lookup { T::Lookup::unlookup(account) } /// Get a reasonable amount of stake based on the execution trait's configuration fn default_stake(factor: u32) -> BalanceOf { let factor = BalanceOf::::from(factor); T::Currency::minimum_balance() * factor } /// Get the current number of candidates. fn candidate_count() -> u32 { >::decode_len().unwrap_or(0usize) as u32 } /// Get the number of votes of a voter. fn vote_count_of(who: &T::AccountId) -> u32 { >::get(who).1.len() as u32 } /// A `DefunctVoter` struct with correct value fn defunct_for(who: T::AccountId) -> DefunctVoter> { DefunctVoter { who: as_lookup::(who.clone()), candidate_count: candidate_count::(), vote_count: vote_count_of::(&who), } } /// 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::(BALANCE_FACTOR); let _ = candidates.iter().map(|c| submit_voter::(c.clone(), vec![c.clone()], stake) ).collect::>()?; Ok(candidates) } /// Submit one voter. fn submit_voter(caller: T::AccountId, votes: Vec, stake: BalanceOf) -> Result<(), &'static str> { >::vote(RawOrigin::Signed(caller).into(), votes, stake) .map_err(|_| "failed to submit vote") } /// 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::(BALANCE_FACTOR); 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 candidates = 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(candidates) } /// removes all the storage items to reverse any genesis state. fn clean() { >::kill(); >::kill(); >::kill(); let _ = >::drain(); } benchmarks! { _ { // User account seed let u in 0 .. 1000 => (); } // -- Signed ones vote { let u in ...; // we fix the number of voted candidates to max let v = MAXIMUM_VOTE; clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(MAXIMUM_VOTE as u32, "candidates")?; let caller = endowed_account::("caller", u); let stake = default_stake::(BALANCE_FACTOR); // vote for all of them. let votes = all_candidates.into_iter().take(v).collect(); }: _(RawOrigin::Signed(caller), votes, stake) vote_update { let u in ...; // we fix the number of voted candidates to max let v = MAXIMUM_VOTE; clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(MAXIMUM_VOTE as u32, "candidates")?; let caller = endowed_account::("caller", u); let stake = default_stake::(BALANCE_FACTOR); // original votes. let mut votes = all_candidates.into_iter().take(v).collect::>(); submit_voter::(caller.clone(), votes.clone(), stake)?; // new votes. votes.rotate_left(1); }: vote(RawOrigin::Signed(caller), votes, stake) remove_voter { let u in ...; // we fix the number of voted candidates to max let v = MAXIMUM_VOTE as u32; clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", u); let stake = default_stake::(BALANCE_FACTOR); submit_voter::(caller.clone(), all_candidates, stake)?; }: _(RawOrigin::Signed(caller)) report_defunct_voter_correct { // number of already existing candidates that may or may not be voted by the reported // account. let c in 1 .. MAX_CANDIDATES; // number of candidates that the reported voter voted for. The worse case of search here is // basically `c * v`. let v in 1 .. (MAXIMUM_VOTE as u32); // 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::(); let stake = default_stake::(BALANCE_FACTOR); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; // create a bunch of candidates as well. let bailing_candidates = submit_candidates::(v, "bailing_candidates")?; let all_candidates = submit_candidates::(c, "all_candidates")?; // account 1 is the reporter and it doesn't matter how many it votes. But it has to be a // voter. let account_1 = endowed_account::("caller", 0); submit_voter::( account_1.clone(), all_candidates.iter().take(1).cloned().collect(), stake, )?; // account 2 votes for all of the mentioned candidates. let account_2 = endowed_account::("caller_2", 1); submit_voter::( account_2.clone(), bailing_candidates.clone(), stake, )?; // all the bailers go away. bailing_candidates.into_iter().for_each(|b| { let count = candidate_count::(); assert!(>::renounce_candidacy( RawOrigin::Signed(b).into(), Renouncing::Candidate(count), ).is_ok()); }); let defunct = defunct_for::(account_2.clone()); }: report_defunct_voter(RawOrigin::Signed(account_1.clone()), defunct) verify { assert!(>::is_voter(&account_1)); assert!(!>::is_voter(&account_2)); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } report_defunct_voter_incorrect { // number of already existing candidates that may or may not be voted by the reported // account. let c in 1 .. MAX_CANDIDATES; // number of candidates that the reported voter voted for. The worse case of search here is // basically `c * v`. let v in 1 .. (MAXIMUM_VOTE as u32); // 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::(); let stake = default_stake::(BALANCE_FACTOR); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; // create a bunch of candidates as well. let all_candidates = submit_candidates::(c, "candidates")?; // account 1 is the reporter and it doesn't matter how many it votes. let account_1 = endowed_account::("caller", 0); submit_voter::( account_1.clone(), all_candidates.iter().take(1).cloned().collect(), stake, )?; // account 2 votes for a bunch of crap, and finally a correct candidate. let account_2 = endowed_account::("caller_2", 1); let mut invalid: Vec = (0..(v-1)).map(|seed| account::("invalid", 0, seed).clone()).collect(); invalid.push(all_candidates.last().unwrap().clone()); submit_voter::( account_2.clone(), invalid, stake, )?; let defunct = defunct_for::(account_2.clone()); // no one bails out. account_1 is slashed and removed as voter now. }: report_defunct_voter(RawOrigin::Signed(account_1.clone()), defunct) verify { assert!(!>::is_voter(&account_1)); assert!(>::is_voter(&account_2)); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } submit_candidacy { // number of already existing candidates. let c in 1 .. MAX_CANDIDATES; // 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::(); let stake = default_stake::(BALANCE_FACTOR); // 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); }: _(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 .. MAX_CANDIDATES; // 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 _ = 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::(); }: 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_member_runner_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 u in ...; 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(); let renouncing = if >::is_member(&bailing) { Renouncing::Member } else if >::is_runner_up(&bailing) { Renouncing::RunnerUp } else { panic!("Bailing must be a member or runner-up for this bench to be sane."); }; }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), renouncing) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } // -- Root ones remove_member_without_replacement { // worse case is when we remove a member and we have no runner as a replacement. This // triggers phragmen again. The only parameter is how many candidates will compete for the // new slot. let c in 1 .. MAX_CANDIDATES; clean::(); // fill only desired members. no runners-up. let all_members = fill_seats_up_to::(T::DesiredMembers::get())?; assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); // submit a new one to compensate, with self-vote. let replacements = submit_candidates_with_self_vote::(c, "new_candidate")?; // create some voters for these replacements. distribute_voters::(replacements, MAX_VOTERS, MAXIMUM_VOTE)?; let to_remove = as_lookup::(all_members[0].clone()); }: remove_member(RawOrigin::Root, to_remove, false) verify { // must still have the desired number of members 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![]); } } 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 u in ...; 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) 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![]); } } remove_member_wrong_refund { // The root call by mistake indicated that this will have no replacement, while it has! // this has now consumed a lot of weight and need to refund. let u in ...; let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let _ = fill_seats_up_to::(m)?; let removing = as_lookup::(>::members_ids()[0].clone()); }: { assert_eq!( >::remove_member(RawOrigin::Root.into(), removing, false).unwrap_err().error, Error::::InvalidReplacement.into(), ); } 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![]); } } on_initialize { // if n % TermDuration is zero, then we run phragmen. The weight function must and should // check this as it is cheap to do so. TermDuration is not a storage item, it is a constant // encoded in the runtime. let c in 1 .. MAX_CANDIDATES; clean::(); // create c candidates. let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; // create 500 voters, each voting the maximum 16 distribute_voters::(all_candidates, MAX_VOTERS, MAXIMUM_VOTE)?; }: { // elect >::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![]); } } 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 .. MAX_CANDIDATES; let v in 1 .. MAX_VOTERS; let e in 1 .. (MAXIMUM_VOTE as u32); clean::(); let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; let _ = distribute_voters::(all_candidates, v, e 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![]); } } } #[cfg(test)] mod tests { use super::*; use crate::tests::{ExtBuilder, Test}; use frame_support::assert_ok; #[test] fn test_benchmarks_elections_phragmen() { ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_vote::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_remove_voter::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_report_defunct_voter_correct::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_report_defunct_voter_incorrect::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_submit_candidacy::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_renounce_candidacy_candidate::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_renounce_candidacy_member_runner_up::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_remove_member_without_replacement::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_remove_member_with_replacement::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_on_initialize::()); }); ExtBuilder::default().desired_members(13).desired_runners_up(7).build_and_execute(|| { assert_ok!(test_benchmark_phragmen::()); }); } }