// This file is part of Substrate. // Copyright (C) 2021 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. //! Two phase election pallet benchmarking. use super::*; use crate::{Pallet as MultiPhase, unsigned::IndexAssignmentOf}; use frame_benchmarking::{account, impl_benchmark_test_suite}; use frame_support::{assert_ok, traits::OnInitialize}; use frame_system::RawOrigin; use rand::{prelude::SliceRandom, rngs::SmallRng, SeedableRng}; use frame_election_provider_support::Assignment; use sp_arithmetic::{per_things::Percent, traits::One}; use sp_npos_elections::IndexAssignment; use sp_runtime::InnerOf; use sp_std::convert::{TryFrom, TryInto}; const SEED: u32 = 999; /// Creates a **valid** solution with exactly the given size. /// /// The snapshot is also created internally. fn solution_with_size( size: SolutionOrSnapshotSize, active_voters_count: u32, desired_targets: u32, ) -> RawSolution> { assert!(size.targets >= desired_targets, "must have enough targets"); assert!( size.targets >= (>::LIMIT * 2) as u32, "must have enough targets for unique votes." ); assert!(size.voters >= active_voters_count, "must have enough voters"); assert!( (>::LIMIT as u32) < desired_targets, "must have enough winners to give them votes." ); let ed: VoteWeight = T::Currency::minimum_balance().saturated_into::(); let stake: VoteWeight = ed.max(One::one()).saturating_mul(100); // first generates random targets. let targets: Vec = (0..size.targets).map(|i| frame_benchmarking::account("Targets", i, SEED)).collect(); let mut rng = SmallRng::seed_from_u64(SEED.into()); // decide who are the winners. let winners = targets .as_slice() .choose_multiple(&mut rng, desired_targets as usize) .cloned() .collect::>(); // first generate active voters who must vote for a subset of winners. let active_voters = (0..active_voters_count) .map(|i| { // chose a random subset of winners. let winner_votes = winners .as_slice() .choose_multiple(&mut rng, >::LIMIT) .cloned() .collect::>(); let voter = frame_benchmarking::account::("Voter", i, SEED); (voter, stake, winner_votes) }) .collect::>(); // rest of the voters. They can only vote for non-winners. let non_winners = targets.iter().filter(|t| !winners.contains(t)).cloned().collect::>(); let rest_voters = (active_voters_count..size.voters) .map(|i| { let votes = (&non_winners) .choose_multiple(&mut rng, >::LIMIT) .cloned() .collect::>(); let voter = frame_benchmarking::account::("Voter", i, SEED); (voter, stake, votes) }) .collect::>(); let mut all_voters = active_voters.clone(); all_voters.extend(rest_voters); all_voters.shuffle(&mut rng); assert_eq!(active_voters.len() as u32, active_voters_count); assert_eq!(all_voters.len() as u32, size.voters); assert_eq!(winners.len() as u32, desired_targets); >::put(SolutionOrSnapshotSize { voters: all_voters.len() as u32, targets: targets.len() as u32, }); >::put(desired_targets); >::put(RoundSnapshot { voters: all_voters.clone(), targets: targets.clone() }); // write the snapshot to staking or whoever is the data provider, in case it is needed further // down the road. T::DataProvider::put_snapshot(all_voters.clone(), targets.clone(), Some(stake)); let cache = helpers::generate_voter_cache::(&all_voters); let stake_of = helpers::stake_of_fn::(&all_voters, &cache); let voter_index = helpers::voter_index_fn::(&cache); let target_index = helpers::target_index_fn::(&targets); let voter_at = helpers::voter_at_fn::(&all_voters); let target_at = helpers::target_at_fn::(&targets); let assignments = active_voters .iter() .map(|(voter, _stake, votes)| { let percent_per_edge: InnerOf> = (100 / votes.len()).try_into().unwrap_or_else(|_| panic!("failed to convert")); Assignment { who: voter.clone(), distribution: votes .iter() .map(|t| (t.clone(), >::from_percent(percent_per_edge))) .collect::>(), } }) .collect::>(); let compact = >::from_assignment(&assignments, &voter_index, &target_index).unwrap(); let score = compact.clone().score(&winners, stake_of, voter_at, target_at).unwrap(); let round = >::round(); assert!(score[0] > 0, "score is zero, this probably means that the stakes are not set."); RawSolution { compact, score, round } } frame_benchmarking::benchmarks! { on_initialize_nothing { assert!(>::current_phase().is_off()); }: { >::on_initialize(1u32.into()); } verify { assert!(>::current_phase().is_off()); } on_initialize_open_signed { // NOTE: this benchmark currently doesn't have any components because the length of a db // read/write is not captured. Otherwise, it is quite influenced by how much data // `T::ElectionDataProvider` is reading and passing on. assert!(>::snapshot().is_none()); assert!(>::current_phase().is_off()); }: { >::on_initialize_open_signed().unwrap(); } verify { assert!(>::snapshot().is_some()); assert!(>::current_phase().is_signed()); } on_initialize_open_unsigned_with_snapshot { assert!(>::snapshot().is_none()); assert!(>::current_phase().is_off()); }: { >::on_initialize_open_unsigned(true, true, 1u32.into()).unwrap(); } verify { assert!(>::snapshot().is_some()); assert!(>::current_phase().is_unsigned()); } finalize_signed_phase_accept_solution { let receiver = account("receiver", 0, SEED); let initial_balance = T::Currency::minimum_balance() * 10u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); let ready: ReadySolution = Default::default(); let deposit: BalanceOf = 10u32.into(); let reward: BalanceOf = 20u32.into(); assert_ok!(T::Currency::reserve(&receiver, deposit)); assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); }: { >::finalize_signed_phase_accept_solution(ready, &receiver, deposit, reward) } verify { assert_eq!(T::Currency::free_balance(&receiver), initial_balance + 20u32.into()); assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into()); } finalize_signed_phase_reject_solution { let receiver = account("receiver", 0, SEED); let initial_balance = T::Currency::minimum_balance().max(One::one()) * 10u32.into(); let deposit: BalanceOf = 10u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); assert_ok!(T::Currency::reserve(&receiver, deposit)); assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); assert_eq!(T::Currency::reserved_balance(&receiver), 10u32.into()); }: { >::finalize_signed_phase_reject_solution(&receiver, deposit) } verify { assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into()); } on_initialize_open_unsigned_without_snapshot { // need to assume signed phase was open before >::on_initialize_open_signed().unwrap(); assert!(>::snapshot().is_some()); assert!(>::current_phase().is_signed()); }: { >::on_initialize_open_unsigned(false, true, 1u32.into()).unwrap(); } verify { assert!(>::snapshot().is_some()); assert!(>::current_phase().is_unsigned()); } // a call to `::elect` where we only return the queued solution. elect_queued { // assume largest values for the election status. These will merely affect the decoding. let v = T::BenchmarkingConfig::VOTERS[1]; let t = T::BenchmarkingConfig::TARGETS[1]; let a = T::BenchmarkingConfig::ACTIVE_VOTERS[1]; let d = T::BenchmarkingConfig::DESIRED_TARGETS[1]; let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(witness, a, d); let ready_solution = >::feasibility_check(raw_solution, ElectionCompute::Signed).unwrap(); // these are set by the `solution_with_size` function. assert!(>::get().is_some()); assert!(>::get().is_some()); assert!(>::get().is_some()); >::put(Phase::Signed); // assume a queued solution is stored, regardless of where it comes from. >::put(ready_solution); }: { assert_ok!( as ElectionProvider>::elect()); } verify { assert!(>::queued_solution().is_none()); assert!(>::get().is_none()); assert!(>::get().is_none()); assert!(>::get().is_none()); assert_eq!(>::get(), >::Off); } #[extra] create_snapshot { assert!(>::snapshot().is_none()); }: { >::create_snapshot().unwrap() } verify { assert!(>::snapshot().is_some()); } submit { let c in 1 .. (T::SignedMaxSubmissions::get() - 1); // the solution will be worse than all of them meaning the score need to be checked against // ~ log2(c) let solution = RawSolution { score: [(10_000_000u128 - 1).into(), 0, 0], ..Default::default() }; MultiPhase::::on_initialize_open_signed().expect("should be ok to start signed phase"); >::put(1); let mut signed_submissions = SignedSubmissions::::get(); for i in 0..c { let solution = RawSolution { score: [(10_000_000 + i).into(), 0, 0], ..Default::default() }; let signed_submission = SignedSubmission { solution, ..Default::default() }; signed_submissions.insert(signed_submission); } signed_submissions.put(); let caller = frame_benchmarking::whitelisted_caller(); T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance() * 10u32.into()); }: _(RawOrigin::Signed(caller), solution, c) verify { assert!(>::signed_submissions().len() as u32 == c + 1); } submit_unsigned { // number of votes in snapshot. let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(witness, a, d); assert!(>::queued_solution().is_none()); >::put(Phase::Unsigned((true, 1u32.into()))); // encode the most significant storage item that needs to be decoded in the dispatch. let encoded_snapshot = >::snapshot().unwrap().encode(); let encoded_call = >::submit_unsigned(raw_solution.clone(), witness).encode(); }: { assert_ok!(>::submit_unsigned(RawOrigin::None.into(), raw_solution, witness)); let _decoded_snap = as Decode>::decode(&mut &*encoded_snapshot) .unwrap(); let _decoded_call = as Decode>::decode(&mut &*encoded_call).unwrap(); } verify { assert!(>::queued_solution().is_some()); } #[extra] trim_assignments_length { // number of votes in snapshot. let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; // Subtract this percentage from the actual encoded size let f in 0 .. 95; // Compute a random solution, then work backwards to get the lists of voters, targets, and // assignments let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let RawSolution { compact, .. } = solution_with_size::(witness, a, d); let RoundSnapshot { voters, targets } = MultiPhase::::snapshot().unwrap(); let voter_at = helpers::voter_at_fn::(&voters); let target_at = helpers::target_at_fn::(&targets); let mut assignments = compact.into_assignment(voter_at, target_at).unwrap(); // make a voter cache and some helper functions for access let cache = helpers::generate_voter_cache::(&voters); let voter_index = helpers::voter_index_fn::(&cache); let target_index = helpers::target_index_fn::(&targets); // sort assignments by decreasing voter stake assignments.sort_by_key(|crate::unsigned::Assignment:: { who, .. }| { let stake = cache.get(&who).map(|idx| { let (_, stake, _) = voters[*idx]; stake }).unwrap_or_default(); sp_std::cmp::Reverse(stake) }); let mut index_assignments = assignments .into_iter() .map(|assignment| IndexAssignment::new(&assignment, &voter_index, &target_index)) .collect::, _>>() .unwrap(); let encoded_size_of = |assignments: &[IndexAssignmentOf]| { CompactOf::::try_from(assignments).map(|compact| compact.encoded_size()) }; let desired_size = Percent::from_percent(100 - f.saturated_into::()) .mul_ceil(encoded_size_of(index_assignments.as_slice()).unwrap()); log!(trace, "desired_size = {}", desired_size); }: { MultiPhase::::trim_assignments_length( desired_size.saturated_into(), &mut index_assignments, &encoded_size_of, ).unwrap(); } verify { let compact = CompactOf::::try_from(index_assignments.as_slice()).unwrap(); let encoding = compact.encode(); log!( trace, "encoded size prediction = {}", encoded_size_of(index_assignments.as_slice()).unwrap(), ); log!(trace, "actual encoded size = {}", encoding.len()); assert!(encoding.len() <= desired_size); } // This is checking a valid solution. The worse case is indeed a valid solution. feasibility_check { // number of votes in snapshot. let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; let size = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(size, a, d); assert_eq!(raw_solution.compact.voter_count() as u32, a); assert_eq!(raw_solution.compact.unique_targets().len() as u32, d); // encode the most significant storage item that needs to be decoded in the dispatch. let encoded_snapshot = >::snapshot().unwrap().encode(); }: { assert_ok!(>::feasibility_check(raw_solution, ElectionCompute::Unsigned)); let _decoded_snap = as Decode>::decode(&mut &*encoded_snapshot) .unwrap(); } } impl_benchmark_test_suite!( MultiPhase, crate::mock::ExtBuilder::default().build(), crate::mock::Runtime, );