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 codec::Decode;
26use core::cmp;
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 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 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 keys<T: Config + session::Config>(c: u32) -> <T as session::Config>::Keys {
58	use rand::{RngCore, SeedableRng};
59
60	let keys = {
61		let mut keys = [0u8; 128];
62
63		if c > 0 {
64			let mut rng = rand::rngs::StdRng::seed_from_u64(c as u64);
65			rng.fill_bytes(&mut keys);
66		}
67
68		keys
69	};
70
71	Decode::decode(&mut &keys[..]).unwrap()
72}
73
74fn validator<T: Config + session::Config>(c: u32) -> (T::AccountId, <T as session::Config>::Keys) {
75	(create_funded_user::<T>("candidate", c, 1000), keys::<T>(c))
76}
77
78fn register_validators<T: Config + session::Config>(count: u32) -> Vec<T::AccountId> {
79	let validators = (0..count).map(|c| validator::<T>(c)).collect::<Vec<_>>();
80
81	for (who, keys) in validators.clone() {
82		<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
83		<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new()).unwrap();
84	}
85
86	validators.into_iter().map(|(who, _)| who).collect()
87}
88
89fn register_candidates<T: Config>(count: u32) {
90	let candidates = (0..count).map(|c| account("candidate", c, SEED)).collect::<Vec<_>>();
91	assert!(CandidacyBond::<T>::get() > 0u32.into(), "Bond cannot be zero!");
92
93	for who in candidates {
94		<T as pallet::Config>::Currency::make_free_balance_be(
95			&who,
96			CandidacyBond::<T>::get() * 3u32.into(),
97		);
98		<CollatorSelection<T>>::register_as_candidate(RawOrigin::Signed(who).into()).unwrap();
99	}
100}
101
102fn min_candidates<T: Config>() -> u32 {
103	let min_collators = T::MinEligibleCollators::get();
104	let invulnerable_length = Invulnerables::<T>::get().len();
105	min_collators.saturating_sub(invulnerable_length.try_into().unwrap())
106}
107
108fn min_invulnerables<T: Config>() -> u32 {
109	let min_collators = T::MinEligibleCollators::get();
110	let candidates_length = CandidateList::<T>::decode_len()
111		.unwrap_or_default()
112		.try_into()
113		.unwrap_or_default();
114	min_collators.saturating_sub(candidates_length)
115}
116
117#[benchmarks(where T: pallet_authorship::Config + session::Config)]
118mod benchmarks {
119	use super::*;
120
121	#[benchmark]
122	fn set_invulnerables(
123		b: Linear<1, { T::MaxInvulnerables::get() }>,
124	) -> Result<(), BenchmarkError> {
125		let origin =
126			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
127
128		let new_invulnerables = register_validators::<T>(b);
129		let mut sorted_new_invulnerables = new_invulnerables.clone();
130		sorted_new_invulnerables.sort();
131
132		#[extrinsic_call]
133		_(origin as T::RuntimeOrigin, new_invulnerables.clone());
134
135		// assert that it comes out sorted
136		assert_last_event::<T>(
137			Event::NewInvulnerables { invulnerables: sorted_new_invulnerables }.into(),
138		);
139		Ok(())
140	}
141
142	#[benchmark]
143	fn add_invulnerable(
144		b: Linear<1, { T::MaxInvulnerables::get() - 1 }>,
145		c: Linear<1, { T::MaxCandidates::get() - 1 }>,
146	) -> Result<(), BenchmarkError> {
147		let origin =
148			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
149
150		// need to fill up candidates
151		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
152		DesiredCandidates::<T>::put(c);
153		// get accounts and keys for the `c` candidates
154		let mut candidates = (0..c).map(|cc| validator::<T>(cc)).collect::<Vec<_>>();
155		// add one more to the list. should not be in `b` (invulnerables) because it's the account
156		// we will _add_ to invulnerables. we want it to be in `candidates` because we need the
157		// weight associated with removing it.
158		let (new_invulnerable, new_invulnerable_keys) = validator::<T>(b.max(c) + 1);
159		candidates.push((new_invulnerable.clone(), new_invulnerable_keys));
160		// set their keys ...
161		for (who, keys) in candidates.clone() {
162			<session::Pallet<T>>::ensure_can_pay_key_deposit(&who).unwrap();
163			<session::Pallet<T>>::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new())
164				.unwrap();
165		}
166		// ... and register them.
167		for (who, _) in candidates.iter() {
168			let deposit = CandidacyBond::<T>::get();
169			<T as pallet::Config>::Currency::make_free_balance_be(who, deposit * 1000_u32.into());
170			CandidateList::<T>::try_mutate(|list| {
171				list.try_push(CandidateInfo { who: who.clone(), deposit }).unwrap();
172				Ok::<(), BenchmarkError>(())
173			})
174			.unwrap();
175			<T as pallet::Config>::Currency::reserve(who, deposit)?;
176			LastAuthoredBlock::<T>::insert(
177				who.clone(),
178				frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
179			);
180		}
181
182		// now we need to fill up invulnerables
183		let mut invulnerables = register_validators::<T>(b);
184		invulnerables.sort();
185		let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> =
186			frame_support::BoundedVec::try_from(invulnerables).unwrap();
187		Invulnerables::<T>::put(invulnerables);
188
189		#[extrinsic_call]
190		_(origin as T::RuntimeOrigin, new_invulnerable.clone());
191
192		assert_last_event::<T>(Event::InvulnerableAdded { account_id: new_invulnerable }.into());
193		Ok(())
194	}
195
196	#[benchmark]
197	fn remove_invulnerable(
198		b: Linear<{ min_invulnerables::<T>() + 1 }, { T::MaxInvulnerables::get() }>,
199	) -> Result<(), BenchmarkError> {
200		let origin =
201			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
202		let mut invulnerables = register_validators::<T>(b);
203		invulnerables.sort();
204		let invulnerables: frame_support::BoundedVec<_, T::MaxInvulnerables> =
205			frame_support::BoundedVec::try_from(invulnerables).unwrap();
206		Invulnerables::<T>::put(invulnerables);
207		let to_remove = Invulnerables::<T>::get().first().unwrap().clone();
208
209		#[extrinsic_call]
210		_(origin as T::RuntimeOrigin, to_remove.clone());
211
212		assert_last_event::<T>(Event::InvulnerableRemoved { account_id: to_remove }.into());
213		Ok(())
214	}
215
216	#[benchmark]
217	fn set_desired_candidates() -> Result<(), BenchmarkError> {
218		let max: u32 = T::MaxCandidates::get();
219		let origin =
220			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
221
222		#[extrinsic_call]
223		_(origin as T::RuntimeOrigin, max);
224
225		assert_last_event::<T>(Event::NewDesiredCandidates { desired_candidates: max }.into());
226		Ok(())
227	}
228
229	#[benchmark]
230	fn set_candidacy_bond(
231		c: Linear<0, { T::MaxCandidates::get() }>,
232		k: Linear<0, { T::MaxCandidates::get() }>,
233	) -> Result<(), BenchmarkError> {
234		let initial_bond_amount: BalanceOf<T> =
235			<T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
236		CandidacyBond::<T>::put(initial_bond_amount);
237		register_validators::<T>(c);
238		register_candidates::<T>(c);
239		let kicked = cmp::min(k, c);
240		let origin =
241			T::UpdateOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
242		let bond_amount = if k > 0 {
243			CandidateList::<T>::mutate(|candidates| {
244				for info in candidates.iter_mut().skip(kicked as usize) {
245					info.deposit = <T as pallet::Config>::Currency::minimum_balance() * 3u32.into();
246				}
247			});
248			<T as pallet::Config>::Currency::minimum_balance() * 3u32.into()
249		} else {
250			<T as pallet::Config>::Currency::minimum_balance()
251		};
252
253		#[extrinsic_call]
254		_(origin as T::RuntimeOrigin, bond_amount);
255
256		assert_last_event::<T>(Event::NewCandidacyBond { bond_amount }.into());
257		Ok(())
258	}
259
260	#[benchmark]
261	fn update_bond(
262		c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>,
263	) -> Result<(), BenchmarkError> {
264		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
265		DesiredCandidates::<T>::put(c);
266
267		register_validators::<T>(c);
268		register_candidates::<T>(c);
269
270		let caller = CandidateList::<T>::get()[0].who.clone();
271		v2::whitelist!(caller);
272
273		let bond_amount: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() +
274			<T as pallet::Config>::Currency::minimum_balance();
275
276		#[extrinsic_call]
277		_(RawOrigin::Signed(caller.clone()), bond_amount);
278
279		assert_last_event::<T>(
280			Event::CandidateBondUpdated { account_id: caller, deposit: bond_amount }.into(),
281		);
282		assert!(
283			CandidateList::<T>::get().iter().last().unwrap().deposit ==
284				<T as pallet::Config>::Currency::minimum_balance() * 2u32.into()
285		);
286		Ok(())
287	}
288
289	// worse case is when we have all the max-candidate slots filled except one, and we fill that
290	// one.
291	#[benchmark]
292	fn register_as_candidate(c: Linear<1, { T::MaxCandidates::get() - 1 }>) {
293		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
294		DesiredCandidates::<T>::put(c + 1);
295
296		register_validators::<T>(c);
297		register_candidates::<T>(c);
298
299		let caller: T::AccountId = whitelisted_caller();
300		let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 2u32.into();
301		<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
302
303		<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
304		<session::Pallet<T>>::set_keys(
305			RawOrigin::Signed(caller.clone()).into(),
306			keys::<T>(c + 1),
307			Vec::new(),
308		)
309		.unwrap();
310
311		#[extrinsic_call]
312		_(RawOrigin::Signed(caller.clone()));
313
314		assert_last_event::<T>(
315			Event::CandidateAdded { account_id: caller, deposit: bond / 2u32.into() }.into(),
316		);
317	}
318
319	#[benchmark]
320	fn take_candidate_slot(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
321		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
322		DesiredCandidates::<T>::put(1);
323
324		register_validators::<T>(c);
325		register_candidates::<T>(c);
326
327		let caller: T::AccountId = whitelisted_caller();
328		let bond: BalanceOf<T> = <T as pallet::Config>::Currency::minimum_balance() * 10u32.into();
329		<T as pallet::Config>::Currency::make_free_balance_be(&caller, bond);
330
331		<session::Pallet<T>>::ensure_can_pay_key_deposit(&caller).unwrap();
332		<session::Pallet<T>>::set_keys(
333			RawOrigin::Signed(caller.clone()).into(),
334			keys::<T>(c + 1),
335			Vec::new(),
336		)
337		.unwrap();
338
339		let target = CandidateList::<T>::get().iter().last().unwrap().who.clone();
340
341		#[extrinsic_call]
342		_(RawOrigin::Signed(caller.clone()), bond / 2u32.into(), target.clone());
343
344		assert_last_event::<T>(
345			Event::CandidateReplaced { old: target, new: caller, deposit: bond / 2u32.into() }
346				.into(),
347		);
348	}
349
350	// worse case is the last candidate leaving.
351	#[benchmark]
352	fn leave_intent(c: Linear<{ min_candidates::<T>() + 1 }, { T::MaxCandidates::get() }>) {
353		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
354		DesiredCandidates::<T>::put(c);
355
356		register_validators::<T>(c);
357		register_candidates::<T>(c);
358
359		let leaving = CandidateList::<T>::get().iter().last().unwrap().who.clone();
360		v2::whitelist!(leaving);
361
362		#[extrinsic_call]
363		_(RawOrigin::Signed(leaving.clone()));
364
365		assert_last_event::<T>(Event::CandidateRemoved { account_id: leaving }.into());
366	}
367
368	// worse case is paying a non-existing candidate account.
369	#[benchmark]
370	fn note_author() {
371		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
372		<T as pallet::Config>::Currency::make_free_balance_be(
373			&<CollatorSelection<T>>::account_id(),
374			<T as pallet::Config>::Currency::minimum_balance() * 4u32.into(),
375		);
376		let author = account("author", 0, SEED);
377		let new_block: BlockNumberFor<T> = 10u32.into();
378
379		frame_system::Pallet::<T>::set_block_number(new_block);
380		assert!(<T as pallet::Config>::Currency::free_balance(&author) == 0u32.into());
381
382		#[block]
383		{
384			<CollatorSelection<T> as EventHandler<_, _>>::note_author(author.clone())
385		}
386
387		assert!(<T as pallet::Config>::Currency::free_balance(&author) > 0u32.into());
388		assert_eq!(frame_system::Pallet::<T>::block_number(), new_block);
389	}
390
391	// worst case for new session.
392	#[benchmark]
393	fn new_session(
394		r: Linear<1, { T::MaxCandidates::get() }>,
395		c: Linear<1, { T::MaxCandidates::get() }>,
396	) {
397		CandidacyBond::<T>::put(<T as pallet::Config>::Currency::minimum_balance());
398		DesiredCandidates::<T>::put(c);
399		frame_system::Pallet::<T>::set_block_number(0u32.into());
400
401		register_validators::<T>(c);
402		register_candidates::<T>(c);
403
404		let new_block: BlockNumberFor<T> = T::KickThreshold::get();
405		let zero_block: BlockNumberFor<T> = 0u32.into();
406		let candidates: Vec<T::AccountId> = CandidateList::<T>::get()
407			.iter()
408			.map(|candidate_info| candidate_info.who.clone())
409			.collect();
410
411		let non_removals = c.saturating_sub(r);
412
413		for i in 0..c {
414			LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), zero_block);
415		}
416
417		if non_removals > 0 {
418			for i in 0..non_removals {
419				LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
420			}
421		} else {
422			for i in 0..c {
423				LastAuthoredBlock::<T>::insert(candidates[i as usize].clone(), new_block);
424			}
425		}
426
427		let min_candidates = min_candidates::<T>();
428		let pre_length = CandidateList::<T>::decode_len().unwrap_or_default();
429
430		frame_system::Pallet::<T>::set_block_number(new_block);
431
432		let current_length: u32 = CandidateList::<T>::decode_len()
433			.unwrap_or_default()
434			.try_into()
435			.unwrap_or_default();
436		assert!(c == current_length);
437		#[block]
438		{
439			<CollatorSelection<T> as SessionManager<_>>::new_session(0);
440		}
441
442		if c > r && non_removals >= min_candidates {
443			// candidates > removals and remaining candidates > min candidates
444			// => remaining candidates should be shorter than before removal, i.e. some were
445			//    actually removed.
446			assert!(CandidateList::<T>::decode_len().unwrap_or_default() < pre_length);
447		} else if c > r && non_removals < min_candidates {
448			// candidates > removals and remaining candidates would be less than min candidates
449			// => remaining candidates should equal min candidates, i.e. some were removed up to
450			//    the minimum, but then any more were "forced" to stay in candidates.
451			let current_length: u32 = CandidateList::<T>::decode_len()
452				.unwrap_or_default()
453				.try_into()
454				.unwrap_or_default();
455			assert!(min_candidates == current_length);
456		} else {
457			// removals >= candidates, non removals must == 0
458			// can't remove more than exist
459			assert!(CandidateList::<T>::decode_len().unwrap_or_default() == pre_length);
460		}
461	}
462
463	impl_benchmark_test_suite!(CollatorSelection, crate::mock::new_test_ext(), crate::mock::Test);
464}