referrerpolicy=no-referrer-when-downgrade

pallet_collator_selection/
benchmarking.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Benchmarking setup for pallet-collator-selection
17
18#![cfg(feature = "runtime-benchmarks")]
19
20use super::*;
21
22#[allow(unused)]
23use crate::Pallet as CollatorSelection;
24use alloc::vec::Vec;
25use core::cmp;
26use cumulus_pallet_session_benchmarking as session_benchmarking;
27use frame_benchmarking::{account, v2::*, whitelisted_caller, BenchmarkError};
28use frame_support::traits::{Currency, EnsureOrigin, Get, ReservableCurrency};
29use frame_system::{pallet_prelude::BlockNumberFor, EventRecord, RawOrigin};
30use pallet_authorship::EventHandler;
31use pallet_session::{self as session, SessionManager};
32
33pub type BalanceOf<T> =
34	<<T as super::Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
35
36const SEED: u32 = 0;
37
38fn assert_last_event<T: Config>(generic_event: <T as super::Config>::RuntimeEvent) {
39	let events = frame_system::Pallet::<T>::events();
40	let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
41	// compare to the last event record
42	let EventRecord { event, .. } = &events[events.len() - 1];
43	assert_eq!(event, &system_event);
44}
45
46fn create_funded_user<T: Config>(
47	string: &'static str,
48	n: u32,
49	balance_factor: u32,
50) -> T::AccountId {
51	let user = account(string, n, SEED);
52	let balance = <T as pallet::Config>::Currency::minimum_balance() * balance_factor.into();
53	let _ = <T as pallet::Config>::Currency::make_free_balance_be(&user, balance);
54	user
55}
56
57fn validator<T: Config + session_benchmarking::Config>(
58	c: u32,
59) -> (T::AccountId, <T as session::Config>::Keys, Vec<u8>) {
60	let validator = create_funded_user::<T>("candidate", c, 1000);
61	let (keys, proof) = T::generate_session_keys_and_proof(validator.clone());
62
63	(validator, keys, proof)
64}
65
66fn register_validators<T: Config + session_benchmarking::Config>(count: u32) -> Vec<T::AccountId> {
67	let validators = (0..count).map(|c| validator::<T>(c)).collect::<Vec<_>>();
68
69	for (who, keys, proof) in validators.clone() {
70		<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
71		<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, proof).unwrap();
72	}
73
74	validators.into_iter().map(|(who, _, _)| who).collect()
75}
76
77fn register_candidates<T: Config>(count: u32) {
78	let candidates = (0..count).map(|c| account("candidate", c, SEED)).collect::<Vec<_>>();
79	assert!(CandidacyBond::<T>::get() > 0u32.into(), "Bond cannot be zero!");
80
81	for who in candidates {
82		<T as pallet::Config>::Currency::make_free_balance_be(
83			&who,
84			CandidacyBond::<T>::get() * 3u32.into(),
85		);
86		<CollatorSelection<T>>::register_as_candidate(RawOrigin::Signed(who).into()).unwrap();
87	}
88}
89
90fn min_candidates<T: Config>() -> u32 {
91	let min_collators = T::MinEligibleCollators::get();
92	let invulnerable_length = Invulnerables::<T>::get().len();
93	min_collators.saturating_sub(invulnerable_length.try_into().unwrap())
94}
95
96fn min_invulnerables<T: Config>() -> u32 {
97	let min_collators = T::MinEligibleCollators::get();
98	let candidates_length = CandidateList::<T>::decode_len()
99		.unwrap_or_default()
100		.try_into()
101		.unwrap_or_default();
102	min_collators.saturating_sub(candidates_length)
103}
104
105#[benchmarks(where T: pallet_authorship::Config + session_benchmarking::Config)]
106mod benchmarks {
107	use super::*;
108
109	#[benchmark]
110	fn set_invulnerables(
111		b: Linear<1, { T::MaxInvulnerables::get() }>,
112	) -> Result<(), BenchmarkError> {
113		let origin =
114			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
115
116		let new_invulnerables = register_validators::<T>(b);
117		let mut sorted_new_invulnerables = new_invulnerables.clone();
118		sorted_new_invulnerables.sort();
119
120		#[extrinsic_call]
121		_(origin as T::RuntimeOrigin, new_invulnerables.clone());
122
123		// assert that it comes out sorted
124		assert_last_event::<T>(
125			Event::NewInvulnerables { invulnerables: sorted_new_invulnerables }.into(),
126		);
127		Ok(())
128	}
129
130	#[benchmark]
131	fn add_invulnerable(
132		b: Linear<1, { T::MaxInvulnerables::get() - 1 }>,
133		c: Linear<1, { T::MaxCandidates::get() - 1 }>,
134	) -> Result<(), BenchmarkError> {
135		let origin =
136			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
137
138		// need to fill up candidates
139		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
140		DesiredCandidates::<T>::put(c);
141		// get accounts and keys for the `c` candidates
142		let mut candidates = (0..c).map(|cc| validator::<T>(cc)).collect::<Vec<_>>();
143		// add one more to the list. should not be in `b` (invulnerables) because it's the account
144		// we will _add_ to invulnerables. we want it to be in `candidates` because we need the
145		// weight associated with removing it.
146		let (new_invulnerable, new_invulnerable_keys, new_proof) = validator::<T>(b.max(c) + 1);
147		candidates.push((new_invulnerable.clone(), new_invulnerable_keys, new_proof));
148		// set their keys ...
149		for (who, keys, proof) in candidates.clone() {
150			<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
151			<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, proof).unwrap();
152		}
153		// ... and register them.
154		for (who, _, _) in candidates.iter() {
155			let deposit = CandidacyBond::<T>::get();
156			<T as pallet::Config>::Currency::make_free_balance_be(who, deposit * 1000_u32.into());
157			CandidateList::<T>::try_mutate(|list| {
158				list.try_push(CandidateInfo { who: who.clone(), deposit }).unwrap();
159				Ok::<(), BenchmarkError>(())
160			})
161			.unwrap();
162			<T as pallet::Config>::Currency::reserve(who, deposit)?;
163			LastAuthoredBlock::<T>::insert(
164				who.clone(),
165				frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
166			);
167		}
168
169		// now we need to fill up invulnerables
170		let mut invulnerables = register_validators::<T>(b);
171		invulnerables.sort();
172		let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> =
173			frame_support::BoundedVec::try_from(invulnerables).unwrap();
174		Invulnerables::<T>::put(invulnerables);
175
176		#[extrinsic_call]
177		_(origin as T::RuntimeOrigin, new_invulnerable.clone());
178
179		assert_last_event::<T>(Event::InvulnerableAdded { account_id: new_invulnerable }.into());
180		Ok(())
181	}
182
183	#[benchmark]
184	fn remove_invulnerable(
185		b: Linear<{ min_invulnerables::<T>() + 1 }, { T::MaxInvulnerables::get() }>,
186	) -> Result<(), BenchmarkError> {
187		let origin =
188			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
189		let mut invulnerables = register_validators::<T>(b);
190		invulnerables.sort();
191		let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> =
192			frame_support::BoundedVec::try_from(invulnerables).unwrap();
193		Invulnerables::<T>::put(invulnerables);
194		let to_remove = Invulnerables::<T>::get().first().unwrap().clone();
195
196		#[extrinsic_call]
197		_(origin as T::RuntimeOrigin, to_remove.clone());
198
199		assert_last_event::<T>(Event::InvulnerableRemoved { account_id: to_remove }.into());
200		Ok(())
201	}
202
203	#[benchmark]
204	fn set_desired_candidates() -> Result<(), BenchmarkError> {
205		let max: u32 = T::MaxCandidates::get();
206		let origin =
207			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
208
209		#[extrinsic_call]
210		_(origin as T::RuntimeOrigin, max);
211
212		assert_last_event::<T>(Event::NewDesiredCandidates { desired_candidates: max }.into());
213		Ok(())
214	}
215
216	#[benchmark]
217	fn set_candidacy_bond(
218		c: Linear<0, { T::MaxCandidates::get() }>,
219		k: Linear<0, { T::MaxCandidates::get() }>,
220	) -> Result<(), BenchmarkError> {
221		let initial_bond_amount: BalanceOf<T> =
222			<T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
223		CandidacyBond::<T>::put(initial_bond_amount);
224		register_validators::<T>(c);
225		register_candidates::<T>(c);
226		let kicked = cmp::min(k, c);
227		let origin =
228			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
229		let bond_amount = if k > 0 {
230			CandidateList::<T>::mutate(|candidates| {
231				for info in candidates.iter_mut().skip(kicked as usize) {
232					info.deposit = <T as pallet::Config>::Currency::minimum_balance() * 3u32.into();
233				}
234			});
235			<T as pallet::Config>::Currency::minimum_balance() * 3u32.into()
236		} else {
237			<T as pallet::Config>::Currency::minimum_balance()
238		};
239
240		#[extrinsic_call]
241		_(origin as T::RuntimeOrigin, bond_amount);
242
243		assert_last_event::<T>(Event::NewCandidacyBond { bond_amount }.into());
244		Ok(())
245	}
246
247	#[benchmark]
248	fn update_bond(
249		c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>,
250	) -> Result<(), BenchmarkError> {
251		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
252		DesiredCandidates::<T>::put(c);
253
254		register_validators::<T>(c);
255		register_candidates::<T>(c);
256
257		let caller = CandidateList::<T>::get()[0].who.clone();
258		v2::whitelist!(caller);
259
260		let bond_amount: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() +
261			<T as pallet::Config>::Currency::minimum_balance();
262
263		#[extrinsic_call]
264		_(RawOrigin::Signed(caller.clone()), bond_amount);
265
266		assert_last_event::<T>(
267			Event::CandidateBondUpdated { account_id: caller, deposit: bond_amount }.into(),
268		);
269		assert!(
270			CandidateList::<T>::get().iter().last().unwrap().deposit ==
271				<T as pallet::Config>::Currency::minimum_balance() * 2u32.into()
272		);
273		Ok(())
274	}
275
276	// worse case is when we have all the max-candidate slots filled except one, and we fill that
277	// one.
278	#[benchmark]
279	fn register_as_candidate(c: Linear<1, { T::MaxCandidates::get() - 1 }>) {
280		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
281		DesiredCandidates::<T>::put(c + 1);
282
283		register_validators::<T>(c);
284		register_candidates::<T>(c);
285
286		let caller: T::AccountId = whitelisted_caller();
287		let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
288		<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
289		let (keys, proof) = T::generate_session_keys_and_proof(caller.clone());
290
291		<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
292		<session::Pallet<T>>::set_keys(RawOrigin::Signed(caller.clone()).into(), keys, proof)
293			.unwrap();
294
295		#[extrinsic_call]
296		_(RawOrigin::Signed(caller.clone()));
297
298		assert_last_event::<T>(
299			Event::CandidateAdded { account_id: caller, deposit: bond / 2u32.into() }.into(),
300		);
301	}
302
303	#[benchmark]
304	fn take_candidate_slot(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
305		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
306		DesiredCandidates::<T>::put(1);
307
308		register_validators::<T>(c);
309		register_candidates::<T>(c);
310
311		let caller: T::AccountId = whitelisted_caller();
312		let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 10u32.into();
313		<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
314
315		let (keys, proof) = T::generate_session_keys_and_proof(caller.clone());
316
317		<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
318		<session::Pallet<T>>::set_keys(RawOrigin::Signed(caller.clone()).into(), keys, proof)
319			.unwrap();
320
321		let target = CandidateList::<T>::get().iter().last().unwrap().who.clone();
322
323		#[extrinsic_call]
324		_(RawOrigin::Signed(caller.clone()), bond / 2u32.into(), target.clone());
325
326		assert_last_event::<T>(
327			Event::CandidateReplaced { old: target, new: caller, deposit: bond / 2u32.into() }
328				.into(),
329		);
330	}
331
332	// worse case is the last candidate leaving.
333	#[benchmark]
334	fn leave_intent(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
335		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
336		DesiredCandidates::<T>::put(c);
337
338		register_validators::<T>(c);
339		register_candidates::<T>(c);
340
341		let leaving = CandidateList::<T>::get().iter().last().unwrap().who.clone();
342		v2::whitelist!(leaving);
343
344		#[extrinsic_call]
345		_(RawOrigin::Signed(leaving.clone()));
346
347		assert_last_event::<T>(Event::CandidateRemoved { account_id: leaving }.into());
348	}
349
350	// worse case is paying a non-existing candidate account.
351	#[benchmark]
352	fn note_author() {
353		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
354		<T as pallet::Config>::Currency::make_free_balance_be(
355			&<CollatorSelection<T>>::account_id(),
356			<T as pallet::Config>::Currency::minimum_balance() * 4u32.into(),
357		);
358		let author = account("author", 0, SEED);
359		let new_block: BlockNumberFor<T> = 10u32.into();
360
361		frame_system::Pallet::<T>::set_block_number(new_block);
362		assert!(<T as pallet::Config>::Currency::free_balance(&author) == 0u32.into());
363
364		#[block]
365		{
366			<CollatorSelection<T> as EventHandler<_, _>>::note_author(author.clone())
367		}
368
369		assert!(<T as pallet::Config>::Currency::free_balance(&author) > 0u32.into());
370		assert_eq!(frame_system::Pallet::<T>::block_number(), new_block);
371	}
372
373	// worst case for new session.
374	#[benchmark]
375	fn new_session(
376		r: Linear<1, { T::MaxCandidates::get() }>,
377		c: Linear<1, { T::MaxCandidates::get() }>,
378	) {
379		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
380		DesiredCandidates::<T>::put(c);
381		frame_system::Pallet::<T>::set_block_number(0u32.into());
382
383		register_validators::<T>(c);
384		register_candidates::<T>(c);
385
386		let new_block: BlockNumberFor<T> = T::KickThreshold::get();
387		let zero_block: BlockNumberFor<T> = 0u32.into();
388		let candidates: Vec<T::AccountId> = CandidateList::<T>::get()
389			.iter()
390			.map(|candidate_info| candidate_info.who.clone())
391			.collect();
392
393		let non_removals = c.saturating_sub(r);
394
395		for i in 0..c {
396			LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), zero_block);
397		}
398
399		if non_removals > 0 {
400			for i in 0..non_removals {
401				LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
402			}
403		} else {
404			for i in 0..c {
405				LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
406			}
407		}
408
409		let min_candidates = min_candidates::<T>();
410		let pre_length = CandidateList::<T>::decode_len().unwrap_or_default();
411
412		frame_system::Pallet::<T>::set_block_number(new_block);
413
414		let current_length: u32 = CandidateList::<T>::decode_len()
415			.unwrap_or_default()
416			.try_into()
417			.unwrap_or_default();
418		assert!(c == current_length);
419		#[block]
420		{
421			<CollatorSelection<T> as SessionManager<_>>::new_session(0);
422		}
423
424		if c > r && non_removals >= min_candidates {
425			// candidates > removals and remaining candidates > min candidates
426			// => remaining candidates should be shorter than before removal, i.e. some were
427			//    actually removed.
428			assert!(CandidateList::<T>::decode_len().unwrap_or_default() < pre_length);
429		} else if c > r && non_removals < min_candidates {
430			// candidates > removals and remaining candidates would be less than min candidates
431			// => remaining candidates should equal min candidates, i.e. some were removed up to
432			//    the minimum, but then any more were "forced" to stay in candidates.
433			let current_length: u32 = CandidateList::<T>::decode_len()
434				.unwrap_or_default()
435				.try_into()
436				.unwrap_or_default();
437			assert!(min_candidates == current_length);
438		} else {
439			// removals >= candidates, non removals must == 0
440			// can't remove more than exist
441			assert!(CandidateList::<T>::decode_len().unwrap_or_default() == pre_length);
442		}
443	}
444
445	impl_benchmark_test_suite!(CollatorSelection, crate::mock::new_test_ext(), crate::mock::Test);
446}