1use core::cmp::Reverse;
21use frame_benchmarking::{v2::*, BenchmarkError};
22use frame_election_provider_support::{bounds::DataProviderBounds, IndexAssignment};
23use frame_support::{
24 assert_ok,
25 traits::{Hooks, TryCollect},
26 BoundedVec,
27};
28use frame_system::RawOrigin;
29use rand::{prelude::SliceRandom, rngs::SmallRng, SeedableRng};
30use sp_arithmetic::{per_things::Percent, traits::One};
31use sp_runtime::InnerOf;
32
33use crate::{unsigned::IndexAssignmentOf, *};
34
35const SEED: u32 = 999;
36
37fn solution_with_size<T: Config>(
41 size: SolutionOrSnapshotSize,
42 active_voters_count: u32,
43 desired_targets: u32,
44) -> Result<RawSolution<SolutionOf<T::MinerConfig>>, &'static str> {
45 ensure!(size.targets >= desired_targets, "must have enough targets");
46 ensure!(
47 size.targets >= (<SolutionOf<T::MinerConfig>>::LIMIT * 2) as u32,
48 "must have enough targets for unique votes."
49 );
50 ensure!(size.voters >= active_voters_count, "must have enough voters");
51 ensure!(
52 (<SolutionOf<T::MinerConfig>>::LIMIT as u32) < desired_targets,
53 "must have enough winners to give them votes."
54 );
55
56 let ed: VoteWeight = T::Currency::minimum_balance().saturated_into::<u64>();
57 let stake: VoteWeight = ed.max(One::one()).saturating_mul(100);
58
59 let targets: Vec<T::AccountId> = (0..size.targets)
61 .map(|i| frame_benchmarking::account("Targets", i, SEED))
62 .collect();
63
64 let mut rng = SmallRng::seed_from_u64(SEED.into());
65
66 let winners = targets
68 .as_slice()
69 .choose_multiple(&mut rng, desired_targets as usize)
70 .cloned()
71 .collect::<Vec<_>>();
72
73 let active_voters = (0..active_voters_count)
75 .map(|i| {
76 let winner_votes: BoundedVec<_, _> = winners
78 .as_slice()
79 .choose_multiple(&mut rng, <SolutionOf<T::MinerConfig>>::LIMIT)
80 .cloned()
81 .try_collect()
82 .expect("<SolutionOf<T::MinerConfig>>::LIMIT is the correct bound; qed.");
83 let voter = frame_benchmarking::account::<T::AccountId>("Voter", i, SEED);
84 (voter, stake, winner_votes)
85 })
86 .collect::<Vec<_>>();
87
88 let non_winners = targets
90 .iter()
91 .filter(|t| !winners.contains(t))
92 .cloned()
93 .collect::<Vec<T::AccountId>>();
94 let rest_voters = (active_voters_count..size.voters)
95 .map(|i| {
96 let votes: BoundedVec<_, _> = (&non_winners)
97 .choose_multiple(&mut rng, <SolutionOf<T::MinerConfig>>::LIMIT)
98 .cloned()
99 .try_collect()
100 .expect("<SolutionOf<T::MinerConfig>>::LIMIT is the correct bound; qed.");
101 let voter = frame_benchmarking::account::<T::AccountId>("Voter", i, SEED);
102 (voter, stake, votes)
103 })
104 .collect::<Vec<_>>();
105
106 let mut all_voters = active_voters.clone();
107 all_voters.extend(rest_voters);
108 all_voters.shuffle(&mut rng);
109
110 assert_eq!(active_voters.len() as u32, active_voters_count);
111 assert_eq!(all_voters.len() as u32, size.voters);
112 assert_eq!(winners.len() as u32, desired_targets);
113
114 SnapshotMetadata::<T>::put(SolutionOrSnapshotSize {
115 voters: all_voters.len() as u32,
116 targets: targets.len() as u32,
117 });
118 DesiredTargets::<T>::put(desired_targets);
119 Snapshot::<T>::put(RoundSnapshot { voters: all_voters.clone(), targets: targets.clone() });
120
121 T::DataProvider::put_snapshot(all_voters.clone(), targets.clone(), Some(stake));
124
125 let cache = helpers::generate_voter_cache::<T::MinerConfig>(&all_voters);
126 let stake_of = helpers::stake_of_fn::<T::MinerConfig>(&all_voters, &cache);
127 let voter_index = helpers::voter_index_fn::<T::MinerConfig>(&cache);
128 let target_index = helpers::target_index_fn::<T::MinerConfig>(&targets);
129 let voter_at = helpers::voter_at_fn::<T::MinerConfig>(&all_voters);
130 let target_at = helpers::target_at_fn::<T::MinerConfig>(&targets);
131
132 let assignments = active_voters
133 .iter()
134 .map(|(voter, _stake, votes)| {
135 let percent_per_edge: InnerOf<SolutionAccuracyOf<T>> =
136 (100 / votes.len()).try_into().unwrap_or_else(|_| panic!("failed to convert"));
137 unsigned::Assignment::<T> {
138 who: voter.clone(),
139 distribution: votes
140 .iter()
141 .map(|t| (t.clone(), SolutionAccuracyOf::<T>::from_percent(percent_per_edge)))
142 .collect::<Vec<_>>(),
143 }
144 })
145 .collect::<Vec<_>>();
146
147 let solution =
148 <SolutionOf<T::MinerConfig>>::from_assignment(&assignments, &voter_index, &target_index)
149 .unwrap();
150 let score = solution.clone().score(stake_of, voter_at, target_at).unwrap();
151 let round = Round::<T>::get();
152
153 assert!(
154 score.minimal_stake > 0,
155 "score is zero, this probably means that the stakes are not set."
156 );
157 Ok(RawSolution { solution, score, round })
158}
159
160fn set_up_data_provider<T: Config>(v: u32, t: u32) {
161 T::DataProvider::clear();
162 log!(
163 info,
164 "setting up with voters = {} [degree = {}], targets = {}",
165 v,
166 <T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get(),
167 t
168 );
169
170 let mut targets = (0..t)
172 .map(|i| {
173 let target = frame_benchmarking::account::<T::AccountId>("Target", i, SEED);
174
175 T::DataProvider::add_target(target.clone());
176 target
177 })
178 .collect::<Vec<_>>();
179
180 assert!(
182 targets.len() > <T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get() as usize
183 );
184 targets.truncate(<T::DataProvider as ElectionDataProvider>::MaxVotesPerVoter::get() as usize);
185
186 (0..v).for_each(|i| {
188 let voter = frame_benchmarking::account::<T::AccountId>("Voter", i, SEED);
189 let weight = T::Currency::minimum_balance().saturated_into::<u64>() * 1000;
190 T::DataProvider::add_voter(voter, weight, targets.clone().try_into().unwrap());
191 });
192}
193
194#[benchmarks]
195mod benchmarks {
196 use super::*;
197
198 #[benchmark]
199 fn on_initialize_nothing() {
200 assert!(CurrentPhase::<T>::get().is_off());
201
202 #[block]
203 {
204 Pallet::<T>::on_initialize(1_u32.into());
205 }
206
207 assert!(CurrentPhase::<T>::get().is_off());
208 }
209
210 #[benchmark]
211 fn on_initialize_open_signed() {
212 assert!(Snapshot::<T>::get().is_none());
213 assert!(CurrentPhase::<T>::get().is_off());
214
215 #[block]
216 {
217 Pallet::<T>::phase_transition(Phase::Signed);
218 }
219
220 assert!(Snapshot::<T>::get().is_none());
221 assert!(CurrentPhase::<T>::get().is_signed());
222 }
223
224 #[benchmark]
225 fn on_initialize_open_unsigned() {
226 assert!(Snapshot::<T>::get().is_none());
227 assert!(CurrentPhase::<T>::get().is_off());
228
229 #[block]
230 {
231 let now = frame_system::Pallet::<T>::block_number();
232 Pallet::<T>::phase_transition(Phase::Unsigned((true, now)));
233 }
234
235 assert!(Snapshot::<T>::get().is_none());
236 assert!(CurrentPhase::<T>::get().is_unsigned());
237 }
238
239 #[benchmark]
240 fn finalize_signed_phase_accept_solution() {
241 let receiver = account("receiver", 0, SEED);
242 let initial_balance = T::Currency::minimum_balance() + 10_u32.into();
243 T::Currency::make_free_balance_be(&receiver, initial_balance);
244 let ready = Default::default();
245 let deposit: BalanceOf<T> = 10_u32.into();
246
247 let reward: BalanceOf<T> = T::SignedRewardBase::get();
248 let call_fee: BalanceOf<T> = 30_u32.into();
249
250 assert_ok!(T::Currency::reserve(&receiver, deposit));
251 assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance());
252
253 #[block]
254 {
255 Pallet::<T>::finalize_signed_phase_accept_solution(ready, &receiver, deposit, call_fee);
256 }
257
258 assert_eq!(T::Currency::free_balance(&receiver), initial_balance + reward + call_fee);
259 assert_eq!(T::Currency::reserved_balance(&receiver), 0_u32.into());
260 }
261
262 #[benchmark]
263 fn finalize_signed_phase_reject_solution() {
264 let receiver = account("receiver", 0, SEED);
265 let initial_balance = T::Currency::minimum_balance() + 10_u32.into();
266 let deposit: BalanceOf<T> = 10_u32.into();
267 T::Currency::make_free_balance_be(&receiver, initial_balance);
268 assert_ok!(T::Currency::reserve(&receiver, deposit));
269
270 assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance());
271 assert_eq!(T::Currency::reserved_balance(&receiver), 10_u32.into());
272
273 #[block]
274 {
275 Pallet::<T>::finalize_signed_phase_reject_solution(&receiver, deposit)
276 }
277
278 assert_eq!(T::Currency::free_balance(&receiver), T::Currency::minimum_balance());
279 assert_eq!(T::Currency::reserved_balance(&receiver), 0_u32.into());
280 }
281
282 #[benchmark]
283 fn create_snapshot_internal(
284 v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>,
286 t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>,
288 ) -> Result<(), BenchmarkError> {
289 set_up_data_provider::<T>(v, t);
291 let targets =
293 T::DataProvider::electable_targets(DataProviderBounds::default(), Zero::zero())?;
294 let voters = T::DataProvider::electing_voters(DataProviderBounds::default(), Zero::zero())?;
295
296 let desired_targets = T::DataProvider::desired_targets()?;
297 assert!(Snapshot::<T>::get().is_none());
298
299 #[block]
300 {
301 Pallet::<T>::create_snapshot_internal(targets, voters, desired_targets)
302 }
303
304 assert!(Snapshot::<T>::get().is_some());
305 assert_eq!(SnapshotMetadata::<T>::get().ok_or("metadata missing")?.voters, v);
306 assert_eq!(SnapshotMetadata::<T>::get().ok_or("metadata missing")?.targets, t);
307
308 Ok(())
309 }
310
311 #[benchmark]
313 fn elect_queued(
314 a: Linear<
317 { T::BenchmarkingConfig::ACTIVE_VOTERS[0] },
318 { T::BenchmarkingConfig::ACTIVE_VOTERS[1] },
319 >,
320 d: Linear<
322 { T::BenchmarkingConfig::DESIRED_TARGETS[0] },
323 { T::BenchmarkingConfig::DESIRED_TARGETS[1] },
324 >,
325 ) -> Result<(), BenchmarkError> {
326 let v = T::BenchmarkingConfig::VOTERS[1];
328 let t = T::BenchmarkingConfig::TARGETS[1];
330
331 let witness = SolutionOrSnapshotSize { voters: v, targets: t };
332 let raw_solution = solution_with_size::<T>(witness, a, d)?;
333 let ready_solution = Pallet::<T>::feasibility_check(raw_solution, ElectionCompute::Signed)
334 .map_err(<&str>::from)?;
335 CurrentPhase::<T>::put(Phase::Signed);
336 QueuedSolution::<T>::put(ready_solution);
338
339 assert!(DesiredTargets::<T>::get().is_some());
341 assert!(Snapshot::<T>::get().is_some());
342 assert!(SnapshotMetadata::<T>::get().is_some());
343
344 let result;
345
346 #[block]
347 {
348 result = <Pallet<T> as ElectionProvider>::elect(Zero::zero());
349 }
350
351 assert!(result.is_ok());
352 assert!(QueuedSolution::<T>::get().is_none());
353 assert!(DesiredTargets::<T>::get().is_none());
354 assert!(Snapshot::<T>::get().is_none());
355 assert!(SnapshotMetadata::<T>::get().is_none());
356 assert_eq!(
357 CurrentPhase::<T>::get(),
358 <Phase<frame_system::pallet_prelude::BlockNumberFor::<T>>>::Off
359 );
360
361 Ok(())
362 }
363
364 #[benchmark]
365 fn submit() -> Result<(), BenchmarkError> {
366 Pallet::<T>::create_snapshot().map_err(<&str>::from)?;
368 Pallet::<T>::phase_transition(Phase::Signed);
369 Round::<T>::put(1);
370
371 let mut signed_submissions = SignedSubmissions::<T>::get();
372
373 for i in 0..(T::SignedMaxSubmissions::get() - 1) {
375 let raw_solution = RawSolution {
376 score: ElectionScore {
377 minimal_stake: 10_000_000u128 + (i as u128),
378 ..Default::default()
379 },
380 ..Default::default()
381 };
382 let signed_submission = SignedSubmission {
383 raw_solution,
384 who: account("submitters", i, SEED),
385 deposit: Default::default(),
386 call_fee: Default::default(),
387 };
388 signed_submissions.insert(signed_submission);
389 }
390 signed_submissions.put();
391
392 let solution = RawSolution {
394 score: ElectionScore { minimal_stake: 10_000_000u128 + 1, ..Default::default() },
395 ..Default::default()
396 };
397
398 let caller = frame_benchmarking::whitelisted_caller();
399 let deposit =
400 Pallet::<T>::deposit_for(&solution, SnapshotMetadata::<T>::get().unwrap_or_default());
401 T::Currency::make_free_balance_be(
402 &caller,
403 T::Currency::minimum_balance() * 1000u32.into() + deposit,
404 );
405
406 #[extrinsic_call]
407 _(RawOrigin::Signed(caller), Box::new(solution));
408
409 assert!(Pallet::<T>::signed_submissions().len() as u32 == T::SignedMaxSubmissions::get());
410
411 Ok(())
412 }
413
414 #[benchmark]
415 fn submit_unsigned(
416 v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>,
418 t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>,
420 a: Linear<
423 { T::BenchmarkingConfig::ACTIVE_VOTERS[0] },
424 { T::BenchmarkingConfig::ACTIVE_VOTERS[1] },
425 >,
426 d: Linear<
428 { T::BenchmarkingConfig::DESIRED_TARGETS[0] },
429 { T::BenchmarkingConfig::DESIRED_TARGETS[1] },
430 >,
431 ) -> Result<(), BenchmarkError> {
432 let witness = SolutionOrSnapshotSize { voters: v, targets: t };
433 let raw_solution = solution_with_size::<T>(witness, a, d)?;
434
435 assert!(QueuedSolution::<T>::get().is_none());
436 CurrentPhase::<T>::put(Phase::Unsigned((true, 1_u32.into())));
437
438 #[extrinsic_call]
439 _(RawOrigin::None, Box::new(raw_solution), witness);
440
441 assert!(QueuedSolution::<T>::get().is_some());
442
443 Ok(())
444 }
445
446 #[benchmark]
448 fn feasibility_check(
449 v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>,
451 t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>,
453 a: Linear<
456 { T::BenchmarkingConfig::ACTIVE_VOTERS[0] },
457 { T::BenchmarkingConfig::ACTIVE_VOTERS[1] },
458 >,
459 d: Linear<
461 { T::BenchmarkingConfig::DESIRED_TARGETS[0] },
462 { T::BenchmarkingConfig::DESIRED_TARGETS[1] },
463 >,
464 ) -> Result<(), BenchmarkError> {
465 let size = SolutionOrSnapshotSize { voters: v, targets: t };
466 let raw_solution = solution_with_size::<T>(size, a, d)?;
467
468 assert_eq!(raw_solution.solution.voter_count() as u32, a);
469 assert_eq!(raw_solution.solution.unique_targets().len() as u32, d);
470
471 let result;
472
473 #[block]
474 {
475 result = Pallet::<T>::feasibility_check(raw_solution, ElectionCompute::Unsigned);
476 }
477
478 assert!(result.is_ok());
479
480 Ok(())
481 }
482
483 #[benchmark(extra)]
496 fn mine_solution_offchain_memory() {
497 let v = T::BenchmarkingConfig::MINER_MAXIMUM_VOTERS;
499 let t = T::BenchmarkingConfig::MAXIMUM_TARGETS;
501
502 set_up_data_provider::<T>(v, t);
503 let now = frame_system::Pallet::<T>::block_number();
504 CurrentPhase::<T>::put(Phase::Unsigned((true, now)));
505 Pallet::<T>::create_snapshot().unwrap();
506
507 #[block]
508 {
509 Pallet::<T>::offchain_worker(now)
511 }
512 }
513
514 #[benchmark(extra)]
521 fn create_snapshot_memory() -> Result<(), BenchmarkError> {
522 let v = T::BenchmarkingConfig::SNAPSHOT_MAXIMUM_VOTERS;
524 let t = T::BenchmarkingConfig::MAXIMUM_TARGETS;
526
527 set_up_data_provider::<T>(v, t);
528 assert!(Snapshot::<T>::get().is_none());
529
530 #[block]
531 {
532 Pallet::<T>::create_snapshot().map_err(|_| "could not create snapshot")?;
533 }
534
535 assert!(Snapshot::<T>::get().is_some());
536 assert_eq!(SnapshotMetadata::<T>::get().ok_or("snapshot missing")?.voters, v);
537 assert_eq!(SnapshotMetadata::<T>::get().ok_or("snapshot missing")?.targets, t);
538
539 Ok(())
540 }
541
542 #[benchmark(extra)]
543 fn trim_assignments_length(
544 v: Linear<{ T::BenchmarkingConfig::VOTERS[0] }, { T::BenchmarkingConfig::VOTERS[1] }>,
546 t: Linear<{ T::BenchmarkingConfig::TARGETS[0] }, { T::BenchmarkingConfig::TARGETS[1] }>,
548 a: Linear<
551 { T::BenchmarkingConfig::ACTIVE_VOTERS[0] },
552 { T::BenchmarkingConfig::ACTIVE_VOTERS[1] },
553 >,
554 d: Linear<
556 { T::BenchmarkingConfig::DESIRED_TARGETS[0] },
557 { T::BenchmarkingConfig::DESIRED_TARGETS[1] },
558 >,
559 f: Linear<0, 95>,
561 ) -> Result<(), BenchmarkError> {
562 let witness = SolutionOrSnapshotSize { voters: v, targets: t };
565 let RawSolution { solution, .. } = solution_with_size::<T>(witness, a, d)?;
566 let RoundSnapshot { voters, targets } = Snapshot::<T>::get().ok_or("snapshot missing")?;
567 let voter_at = helpers::voter_at_fn::<T::MinerConfig>(&voters);
568 let target_at = helpers::target_at_fn::<T::MinerConfig>(&targets);
569 let mut assignments = solution
570 .into_assignment(voter_at, target_at)
571 .expect("solution generated by `solution_with_size` must be valid.");
572
573 let cache = helpers::generate_voter_cache::<T::MinerConfig>(&voters);
575 let voter_index = helpers::voter_index_fn::<T::MinerConfig>(&cache);
576 let target_index = helpers::target_index_fn::<T::MinerConfig>(&targets);
577
578 assignments.sort_by_key(|unsigned::Assignment::<T> { who, .. }| {
580 let stake = cache
581 .get(who)
582 .map(|idx| {
583 let (_, stake, _) = voters[*idx];
584 stake
585 })
586 .unwrap_or_default();
587 Reverse(stake)
588 });
589
590 let mut index_assignments = assignments
591 .into_iter()
592 .map(|assignment| IndexAssignment::new(&assignment, &voter_index, &target_index))
593 .collect::<Result<Vec<_>, _>>()
594 .unwrap();
595
596 let encoded_size_of = |assignments: &[IndexAssignmentOf<T::MinerConfig>]| {
597 SolutionOf::<T::MinerConfig>::try_from(assignments)
598 .map(|solution| solution.encoded_size())
599 };
600
601 let desired_size = Percent::from_percent(100 - f.saturated_into::<u8>())
602 .mul_ceil(encoded_size_of(index_assignments.as_slice()).unwrap());
603 log!(trace, "desired_size = {}", desired_size);
604
605 #[block]
606 {
607 Miner::<T::MinerConfig>::trim_assignments_length(
608 desired_size.saturated_into(),
609 &mut index_assignments,
610 &encoded_size_of,
611 )
612 .unwrap();
613 }
614
615 let solution =
616 SolutionOf::<T::MinerConfig>::try_from(index_assignments.as_slice()).unwrap();
617 let encoding = solution.encode();
618 log!(
619 trace,
620 "encoded size prediction = {}",
621 encoded_size_of(index_assignments.as_slice()).unwrap(),
622 );
623 log!(trace, "actual encoded size = {}", encoding.len());
624 assert!(encoding.len() <= desired_size);
625
626 Ok(())
627 }
628
629 impl_benchmark_test_suite! {
630 Pallet,
631 mock::ExtBuilder::default().build_offchainify(10).0,
632 mock::Runtime,
633 }
634}