// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // 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 pezpallet benchmarking. use core::cmp::Reverse; use pezframe_benchmarking::{v2::*, BenchmarkError}; use pezframe_election_provider_support::{bounds::DataProviderBounds, IndexAssignment}; use pezframe_support::{ assert_ok, traits::{Hooks, TryCollect}, BoundedVec, }; use pezframe_system::RawOrigin; use pezsp_arithmetic::{per_things::Percent, traits::One}; use pezsp_runtime::InnerOf; use rand::{prelude::SliceRandom, rngs::SmallRng, SeedableRng}; use crate::{unsigned::IndexAssignmentOf, *}; 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, ) -> Result>, &'static str> { ensure!(size.targets >= desired_targets, "must have enough targets"); ensure!( size.targets >= (>::LIMIT * 2) as u32, "must have enough targets for unique votes." ); ensure!(size.voters >= active_voters_count, "must have enough voters"); ensure!( (>::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| pezframe_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: BoundedVec<_, _> = winners .as_slice() .choose_multiple(&mut rng, >::LIMIT) .cloned() .try_collect() .expect(">::LIMIT is the correct bound; qed."); let voter = pezframe_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: BoundedVec<_, _> = (&non_winners) .choose_multiple(&mut rng, >::LIMIT) .cloned() .try_collect() .expect(">::LIMIT is the correct bound; qed."); let voter = pezframe_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); SnapshotMetadata::::put(SolutionOrSnapshotSize { voters: all_voters.len() as u32, targets: targets.len() as u32, }); DesiredTargets::::put(desired_targets); Snapshot::::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")); unsigned::Assignment:: { who: voter.clone(), distribution: votes .iter() .map(|t| (t.clone(), SolutionAccuracyOf::::from_percent(percent_per_edge))) .collect::>(), } }) .collect::>(); let solution = >::from_assignment(&assignments, &voter_index, &target_index) .unwrap(); let score = solution.clone().score(stake_of, voter_at, target_at).unwrap(); let round = Round::::get(); assert!( score.minimal_stake > 0, "score is zero, this probably means that the stakes are not set." ); Ok(RawSolution { solution, score, round }) } fn set_up_data_provider(v: u32, t: u32) { T::DataProvider::clear(); log!( info, "setting up with voters = {} [degree = {}], targets = {}", v, ::MaxVotesPerVoter::get(), t ); // fill targets. let mut targets = (0..t) .map(|i| { let target = pezframe_benchmarking::account::("Target", i, SEED); T::DataProvider::add_target(target.clone()); target }) .collect::>(); // we should always have enough voters to fill. assert!( targets.len() > ::MaxVotesPerVoter::get() as usize ); targets.truncate(::MaxVotesPerVoter::get() as usize); // fill voters. (0..v).for_each(|i| { let voter = pezframe_benchmarking::account::("Voter", i, SEED); let weight = T::Currency::minimum_balance().saturated_into::() * 1000; T::DataProvider::add_voter(voter, weight, targets.clone().try_into().unwrap()); }); } #[benchmarks] mod benchmarks { use super::*; #[benchmark] fn on_initialize_nothing() { assert!(CurrentPhase::::get().is_off()); #[block] { Pezpallet::::on_initialize(1_u32.into()); } assert!(CurrentPhase::::get().is_off()); } #[benchmark] fn on_initialize_open_signed() { assert!(Snapshot::::get().is_none()); assert!(CurrentPhase::::get().is_off()); #[block] { Pezpallet::::phase_transition(Phase::Signed); } assert!(Snapshot::::get().is_none()); assert!(CurrentPhase::::get().is_signed()); } #[benchmark] fn on_initialize_open_unsigned() { assert!(Snapshot::::get().is_none()); assert!(CurrentPhase::::get().is_off()); #[block] { let now = pezframe_system::Pezpallet::::block_number(); Pezpallet::::phase_transition(Phase::Unsigned((true, now))); } assert!(Snapshot::::get().is_none()); assert!(CurrentPhase::::get().is_unsigned()); } #[benchmark] fn finalize_signed_phase_accept_solution() { let receiver = account("receiver", 0, SEED); let initial_balance = T::Currency::minimum_balance() + 10_u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); let ready = Default::default(); let deposit: BalanceOf = 10_u32.into(); let reward: BalanceOf = T::SignedRewardBase::get(); let call_fee: BalanceOf = 30_u32.into(); assert_ok!(T::Currency::reserve(&receiver, deposit)); assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); #[block] { Pezpallet::::finalize_signed_phase_accept_solution( ready, &receiver, deposit, call_fee, ); } assert_eq!(T::Currency::free_balance(&receiver), initial_balance + reward + call_fee); assert_eq!(T::Currency::reserved_balance(&receiver), 0_u32.into()); } #[benchmark] fn finalize_signed_phase_reject_solution() { let receiver = account("receiver", 0, SEED); let initial_balance = T::Currency::minimum_balance() + 10_u32.into(); let deposit: BalanceOf = 10_u32.into(); T::Currency::make_free_balance_be(&receiver, initial_balance); assert_ok!(T::Currency::reserve(&receiver, deposit)); assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); assert_eq!(T::Currency::reserved_balance(&receiver), 10_u32.into()); #[block] { Pezpallet::::finalize_signed_phase_reject_solution(&receiver, deposit) } assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance()); assert_eq!(T::Currency::reserved_balance(&receiver), 0_u32.into()); } #[benchmark] fn create_snapshot_internal( // Number of votes in snapshot. v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>, // Number of targets in snapshot. t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>, ) -> Result<(), BenchmarkError> { // We don't directly need the data-provider to be populated, but it is just easy to use it. set_up_data_provider::(v, t); // default bounds are unbounded. let targets = T::DataProvider::electable_targets(DataProviderBounds::default(), Zero::zero())?; let voters = T::DataProvider::electing_voters(DataProviderBounds::default(), Zero::zero())?; let desired_targets = T::DataProvider::desired_targets()?; assert!(Snapshot::::get().is_none()); #[block] { Pezpallet::::create_snapshot_internal(targets, voters, desired_targets) } assert!(Snapshot::::get().is_some()); assert_eq!(SnapshotMetadata::::get().ok_or("metadata missing")?.voters, v); assert_eq!(SnapshotMetadata::::get().ok_or("metadata missing")?.targets, t); Ok(()) } // A call to `::elect` where we only return the queued solution. #[benchmark] fn elect_queued( // Number of assignments, i.e. `solution.len()`. // This means the active nominators, thus must be a subset of `v`. a: Linear< { T::BenchmarkingConfig::ACTIVE_VOTERS[0] }, { T::BenchmarkingConfig::ACTIVE_VOTERS[1] }, >, // Number of desired targets. Must be a subset of `t`. d: Linear< { T::BenchmarkingConfig::DESIRED_TARGETS[0] }, { T::BenchmarkingConfig::DESIRED_TARGETS[1] }, >, ) -> Result<(), BenchmarkError> { // Number of votes in snapshot. Not dominant. let v = T::BenchmarkingConfig::VOTERS[1]; // Number of targets in snapshot. Not dominant. let t = T::BenchmarkingConfig::TARGETS[1]; let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(witness, a, d)?; let ready_solution = Pezpallet::::feasibility_check(raw_solution, ElectionCompute::Signed) .map_err(<&str>::from)?; CurrentPhase::::put(Phase::Signed); // Assume a queued solution is stored, regardless of where it comes from. QueuedSolution::::put(ready_solution); // These are set by the `solution_with_size` function. assert!(DesiredTargets::::get().is_some()); assert!(Snapshot::::get().is_some()); assert!(SnapshotMetadata::::get().is_some()); let result; #[block] { result = as ElectionProvider>::elect(Zero::zero()); } assert!(result.is_ok()); assert!(QueuedSolution::::get().is_none()); assert!(DesiredTargets::::get().is_none()); assert!(Snapshot::::get().is_none()); assert!(SnapshotMetadata::::get().is_none()); assert_eq!( CurrentPhase::::get(), >>::Off ); Ok(()) } #[benchmark] fn submit() -> Result<(), BenchmarkError> { // The queue is full and the solution is only better than the worse. Pezpallet::::create_snapshot().map_err(<&str>::from)?; Pezpallet::::phase_transition(Phase::Signed); Round::::put(1); let mut signed_submissions = SignedSubmissions::::get(); // Insert `max` submissions for i in 0..(T::SignedMaxSubmissions::get() - 1) { let raw_solution = RawSolution { score: ElectionScore { minimal_stake: 10_000_000u128 + (i as u128), ..Default::default() }, ..Default::default() }; let signed_submission = SignedSubmission { raw_solution, who: account("submitters", i, SEED), deposit: Default::default(), call_fee: Default::default(), }; signed_submissions.insert(signed_submission); } signed_submissions.put(); // This score will eject the weakest one. let solution = RawSolution { score: ElectionScore { minimal_stake: 10_000_000u128 + 1, ..Default::default() }, ..Default::default() }; let caller = pezframe_benchmarking::whitelisted_caller(); let deposit = Pezpallet::::deposit_for( &solution, SnapshotMetadata::::get().unwrap_or_default(), ); T::Currency::make_free_balance_be( &caller, T::Currency::minimum_balance() * 1000u32.into() + deposit, ); #[extrinsic_call] _(RawOrigin::Signed(caller), Box::new(solution)); assert!( Pezpallet::::signed_submissions().len() as u32 == T::SignedMaxSubmissions::get() ); Ok(()) } #[benchmark] fn submit_unsigned( // Number of votes in snapshot. v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>, // Number of targets in snapshot. t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>, // Number of assignments, i.e. `solution.len()`. // This means the active nominators, thus must be a subset of `v` component. a: Linear< { T::BenchmarkingConfig::ACTIVE_VOTERS[0] }, { T::BenchmarkingConfig::ACTIVE_VOTERS[1] }, >, // Number of desired targets. Must be a subset of `t` component. d: Linear< { T::BenchmarkingConfig::DESIRED_TARGETS[0] }, { T::BenchmarkingConfig::DESIRED_TARGETS[1] }, >, ) -> Result<(), BenchmarkError> { let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(witness, a, d)?; assert!(QueuedSolution::::get().is_none()); CurrentPhase::::put(Phase::Unsigned((true, 1_u32.into()))); #[extrinsic_call] _(RawOrigin::None, Box::new(raw_solution), witness); assert!(QueuedSolution::::get().is_some()); Ok(()) } // This is checking a valid solution. The worse case is indeed a valid solution. #[benchmark] fn feasibility_check( // Number of votes in snapshot. v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>, // Number of targets in snapshot. t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>, // Number of assignments, i.e. `solution.len()`. // This means the active nominators, thus must be a subset of `v` component. a: Linear< { T::BenchmarkingConfig::ACTIVE_VOTERS[0] }, { T::BenchmarkingConfig::ACTIVE_VOTERS[1] }, >, // Number of desired targets. Must be a subset of `t` component. d: Linear< { T::BenchmarkingConfig::DESIRED_TARGETS[0] }, { T::BenchmarkingConfig::DESIRED_TARGETS[1] }, >, ) -> Result<(), BenchmarkError> { let size = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(size, a, d)?; assert_eq!(raw_solution.solution.voter_count() as u32, a); assert_eq!(raw_solution.solution.unique_targets().len() as u32, d); let result; #[block] { result = Pezpallet::::feasibility_check(raw_solution, ElectionCompute::Unsigned); } assert!(result.is_ok()); Ok(()) } // NOTE: this weight is not used anywhere, but the fact that it should succeed when execution in // isolation is vital to ensure memory-safety. For the same reason, we don't care about the // components iterating, we merely check that this operation will work with the "maximum" // numbers. // // ONLY run this benchmark in isolation, and pass the `--extra` flag to enable it. // // NOTE: If this benchmark does not run out of memory with a given heap pages, it means that the // OCW process can SURELY succeed with the given configuration, but the opposite is not true. // This benchmark is doing more work than a raw call to `OffchainWorker_offchain_worker` runtime // api call, since it is also setting up some mock data, which will itself exhaust the heap to // some extent. #[benchmark(extra)] fn mine_solution_offchain_memory() { // Number of votes in snapshot. Fixed to maximum. let v = T::BenchmarkingConfig::MINER_MAXIMUM_VOTERS; // Number of targets in snapshot. Fixed to maximum. let t = T::BenchmarkingConfig::MAXIMUM_TARGETS; set_up_data_provider::(v, t); let now = pezframe_system::Pezpallet::::block_number(); CurrentPhase::::put(Phase::Unsigned((true, now))); Pezpallet::::create_snapshot().unwrap(); #[block] { // we can't really verify this as it won't write anything to state, check logs. Pezpallet::::offchain_worker(now) } } // NOTE: this weight is not used anywhere, but the fact that it should succeed when execution in // isolation is vital to ensure memory-safety. For the same reason, we don't care about the // components iterating, we merely check that this operation will work with the "maximum" // numbers. // // ONLY run this benchmark in isolation, and pass the `--extra` flag to enable it. #[benchmark(extra)] fn create_snapshot_memory() -> Result<(), BenchmarkError> { // Number of votes in snapshot. Fixed to maximum. let v = T::BenchmarkingConfig::SNAPSHOT_MAXIMUM_VOTERS; // Number of targets in snapshot. Fixed to maximum. let t = T::BenchmarkingConfig::MAXIMUM_TARGETS; set_up_data_provider::(v, t); assert!(Snapshot::::get().is_none()); #[block] { Pezpallet::::create_snapshot().map_err(|_| "could not create snapshot")?; } assert!(Snapshot::::get().is_some()); assert_eq!(SnapshotMetadata::::get().ok_or("snapshot missing")?.voters, v); assert_eq!(SnapshotMetadata::::get().ok_or("snapshot missing")?.targets, t); Ok(()) } #[benchmark(extra)] fn trim_assignments_length( // Number of votes in snapshot. v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>, // Number of targets in snapshot. t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>, // Number of assignments, i.e. `solution.len()`. // This means the active nominators, thus must be a subset of `v` component. a: Linear< { T::BenchmarkingConfig::ACTIVE_VOTERS[0] }, { T::BenchmarkingConfig::ACTIVE_VOTERS[1] }, >, // Number of desired targets. Must be a subset of `t` component. d: Linear< { T::BenchmarkingConfig::DESIRED_TARGETS[0] }, { T::BenchmarkingConfig::DESIRED_TARGETS[1] }, >, // Subtract this percentage from the actual encoded size. f: Linear<0, 95>, ) -> Result<(), BenchmarkError> { // 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 { solution, .. } = solution_with_size::(witness, a, d)?; let RoundSnapshot { voters, targets } = Snapshot::::get().ok_or("snapshot missing")?; let voter_at = helpers::voter_at_fn::(&voters); let target_at = helpers::target_at_fn::(&targets); let mut assignments = solution .into_assignment(voter_at, target_at) .expect("solution generated by `solution_with_size` must be valid."); // 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(|unsigned::Assignment:: { who, .. }| { let stake = cache .get(who) .map(|idx| { let (_, stake, _) = voters[*idx]; stake }) .unwrap_or_default(); 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]| { SolutionOf::::try_from(assignments) .map(|solution| solution.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); #[block] { Miner::::trim_assignments_length( desired_size.saturated_into(), &mut index_assignments, &encoded_size_of, ) .unwrap(); } let solution = SolutionOf::::try_from(index_assignments.as_slice()).unwrap(); let encoding = solution.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); Ok(()) } impl_benchmark_test_suite! { Pezpallet, mock::ExtBuilder::default().build_offchainify(10).0, mock::Runtime, } }