referrerpolicy=no-referrer-when-downgrade

pallet_ranked_collective/
benchmarking.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Staking pallet benchmarking.
19
20use super::*;
21#[allow(unused_imports)]
22use crate::Pallet as RankedCollective;
23use alloc::vec::Vec;
24use frame_benchmarking::{
25	v1::{account, BenchmarkError},
26	v2::*,
27};
28
29use frame_support::{assert_err, assert_ok, traits::NoOpPoll};
30use frame_system::{pallet_prelude::BlockNumberFor, RawOrigin as SystemOrigin};
31
32const SEED: u32 = 0;
33
34fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::RuntimeEvent) {
35	frame_system::Pallet::<T>::assert_last_event(generic_event.into());
36}
37
38fn assert_has_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::RuntimeEvent) {
39	frame_system::Pallet::<T>::assert_has_event(generic_event.into());
40}
41
42fn make_member<T: Config<I>, I: 'static>(rank: Rank) -> T::AccountId {
43	let who = account::<T::AccountId>("member", MemberCount::<T, I>::get(0), SEED);
44	let who_lookup = T::Lookup::unlookup(who.clone());
45	assert_ok!(Pallet::<T, I>::add_member(
46		T::AddOrigin::try_successful_origin()
47			.expect("AddOrigin has no successful origin required for the benchmark"),
48		who_lookup.clone(),
49	));
50	for _ in 0..rank {
51		assert_ok!(Pallet::<T, I>::promote_member(
52			T::PromoteOrigin::try_successful_origin()
53				.expect("PromoteOrigin has no successful origin required for the benchmark"),
54			who_lookup.clone(),
55		));
56	}
57	who
58}
59
60#[instance_benchmarks(
61where
62	<<T as pallet::Config<I>>::Polls as frame_support::traits::Polling<Tally<T, I, pallet::Pallet<T, I>>>>::Index: From<u8>,
63	<T as frame_system::Config>::RuntimeEvent: TryInto<pallet::Event<T, I>>,
64)]
65mod benchmarks {
66	use super::*;
67
68	#[benchmark]
69	fn add_member() -> Result<(), BenchmarkError> {
70		// Generate a test account for the new member.
71		let who = account::<T::AccountId>("member", 0, SEED);
72		let who_lookup = T::Lookup::unlookup(who.clone());
73
74		// Attempt to get the successful origin for adding a member.
75		let origin =
76			T::AddOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
77
78		#[extrinsic_call]
79		_(origin as T::RuntimeOrigin, who_lookup);
80
81		// Ensure the member count has increased (or is 1 for rank 0).
82		assert_eq!(MemberCount::<T, I>::get(0), 1);
83
84		// Check that the correct event was emitted.
85		assert_last_event::<T, I>(Event::MemberAdded { who }.into());
86
87		Ok(())
88	}
89
90	#[benchmark]
91	fn remove_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
92		// Convert `r` to a rank and create members.
93		let rank = r as u16;
94		let who = make_member::<T, I>(rank);
95		let who_lookup = T::Lookup::unlookup(who.clone());
96		let last = make_member::<T, I>(rank);
97
98		// Collect the index of the `last` member for each rank.
99		let last_index: Vec<_> =
100			(0..=rank).map(|r| IdToIndex::<T, I>::get(r, &last).unwrap()).collect();
101
102		// Fetch the remove origin.
103		let origin =
104			T::RemoveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
105
106		#[extrinsic_call]
107		_(origin as T::RuntimeOrigin, who_lookup, rank);
108
109		for r in 0..=rank {
110			assert_eq!(MemberCount::<T, I>::get(r), 1);
111			assert_ne!(last_index[r as usize], IdToIndex::<T, I>::get(r, &last).unwrap());
112		}
113
114		// Ensure the correct event was emitted for the member removal.
115		assert_last_event::<T, I>(Event::MemberRemoved { who, rank }.into());
116
117		Ok(())
118	}
119
120	#[benchmark]
121	fn promote_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
122		// Convert `r` to a rank and create the member.
123		let rank = r as u16;
124		let who = make_member::<T, I>(rank);
125		let who_lookup = T::Lookup::unlookup(who.clone());
126
127		// Try to fetch the promotion origin.
128		let origin =
129			T::PromoteOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
130
131		#[extrinsic_call]
132		_(origin as T::RuntimeOrigin, who_lookup);
133
134		// Ensure the member's rank has increased by 1.
135		assert_eq!(Members::<T, I>::get(&who).unwrap().rank, rank + 1);
136
137		// Ensure the correct event was emitted for the rank change.
138		assert_last_event::<T, I>(Event::RankChanged { who, rank: rank + 1 }.into());
139
140		Ok(())
141	}
142
143	#[benchmark]
144	fn demote_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
145		// Convert `r` to a rank and create necessary members for the benchmark.
146		let rank = r as u16;
147		let who = make_member::<T, I>(rank);
148		let who_lookup = T::Lookup::unlookup(who.clone());
149		let last = make_member::<T, I>(rank);
150
151		// Get the last index for the member.
152		let last_index = IdToIndex::<T, I>::get(rank, &last).unwrap();
153
154		// Try to fetch the demotion origin.
155		let origin =
156			T::DemoteOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
157
158		#[extrinsic_call]
159		_(origin as T::RuntimeOrigin, who_lookup);
160
161		// Ensure the member's rank has decreased by 1.
162		assert_eq!(Members::<T, I>::get(&who).map(|x| x.rank), rank.checked_sub(1));
163
164		// Ensure the member count remains as expected.
165		assert_eq!(MemberCount::<T, I>::get(rank), 1);
166
167		// Ensure the index of the last member has changed.
168		assert_ne!(last_index, IdToIndex::<T, I>::get(rank, &last).unwrap());
169
170		// Ensure the correct event was emitted depending on the member's rank.
171		assert_last_event::<T, I>(
172			match rank {
173				0 => Event::MemberRemoved { who, rank: 0 },
174				r => Event::RankChanged { who, rank: r - 1 },
175			}
176			.into(),
177		);
178
179		Ok(())
180	}
181
182	#[benchmark]
183	fn vote() -> Result<(), BenchmarkError> {
184		// Get the first available class or set it to None if no class exists.
185		let class = T::Polls::classes().into_iter().next();
186
187		// Convert the class to a rank if it exists, otherwise use the default rank.
188		let rank = class.as_ref().map_or(
189			<Pallet<T, I> as frame_support::traits::RankedMembers>::Rank::default(),
190			|class| T::MinRankOfClass::convert(class.clone()),
191		);
192
193		// Create a caller based on the rank.
194		let caller = make_member::<T, I>(rank);
195
196		// Determine the poll to use: create an ongoing poll if class exists, or use an invalid
197		// poll.
198		let poll = if let Some(ref class) = class {
199			T::Polls::create_ongoing(class.clone())
200				.expect("Poll creation should succeed for rank 0")
201		} else {
202			<NoOpPoll<BlockNumberFor<T>> as Polling<T>>::Index::MAX.into()
203		};
204
205		// Benchmark the vote logic for a positive vote (true).
206		#[block]
207		{
208			let vote_result =
209				Pallet::<T, I>::vote(SystemOrigin::Signed(caller.clone()).into(), poll, true);
210
211			// If the class exists, expect success; otherwise expect a "NotPolling" error.
212			if class.is_some() {
213				assert_ok!(vote_result);
214			} else {
215				assert_err!(vote_result, crate::Error::<T, I>::NotPolling);
216			};
217		}
218
219		// Vote logic for a negative vote (false).
220		let vote_result =
221			Pallet::<T, I>::vote(SystemOrigin::Signed(caller.clone()).into(), poll, false);
222
223		// Check the result of the negative vote.
224		if class.is_some() {
225			assert_ok!(vote_result);
226		} else {
227			assert_err!(vote_result, crate::Error::<T, I>::NotPolling);
228		};
229
230		// If the class exists, verify the vote event and tally.
231		if let Some(_) = class {
232			// Get the actual vote weight from the latest event's VoteRecord::Nay
233			let mut events = frame_system::Pallet::<T>::events();
234			let last_event = events.pop().expect("At least one event should exist");
235			let event: Event<T, I> = last_event
236				.event
237				.try_into()
238				.unwrap_or_else(|_| panic!("Event conversion failed"));
239
240			match event {
241				Event::Voted { vote: VoteRecord::Nay(vote_weight), who, poll: poll2, tally } => {
242					assert_eq!(tally, Tally::from_parts(0, 0, vote_weight));
243					assert_eq!(caller, who);
244					assert_eq!(poll, poll2);
245				},
246				_ => panic!("Invalid event"),
247			};
248		}
249
250		Ok(())
251	}
252
253	#[benchmark]
254	fn cleanup_poll(n: Linear<0, 100>) -> Result<(), BenchmarkError> {
255		let alice: T::AccountId = whitelisted_caller();
256		let origin = SystemOrigin::Signed(alice.clone());
257
258		// Try to retrieve the first class if it exists.
259		let class = T::Polls::classes().into_iter().next();
260
261		// Convert the class to a rank, or use a default rank if no class exists.
262		let rank = class.as_ref().map_or(
263			<Pallet<T, I> as frame_support::traits::RankedMembers>::Rank::default(),
264			|class| T::MinRankOfClass::convert(class.clone()),
265		);
266
267		// Determine the poll to use: create an ongoing poll if class exists, or use an invalid
268		// poll.
269		let poll = if let Some(ref class) = class {
270			T::Polls::create_ongoing(class.clone())
271				.expect("Poll creation should succeed for rank 0")
272		} else {
273			<NoOpPoll<BlockNumberFor<T>> as Polling<T>>::Index::MAX.into()
274		};
275
276		// Simulate voting by `n` members.
277		for _ in 0..n {
278			let voter = make_member::<T, I>(rank);
279			let result = Pallet::<T, I>::vote(SystemOrigin::Signed(voter).into(), poll, true);
280
281			// Check voting results based on class existence.
282			if class.is_some() {
283				assert_ok!(result);
284			} else {
285				assert_err!(result, crate::Error::<T, I>::NotPolling);
286			}
287		}
288
289		// End the poll if the class exists.
290		if class.is_some() {
291			T::Polls::end_ongoing(poll, false)
292				.map_err(|_| BenchmarkError::Stop("Failed to end poll"))?;
293		}
294
295		// Verify the number of votes cast.
296		let expected_votes = if class.is_some() { n as usize } else { 0 };
297		assert_eq!(Voting::<T, I>::iter_prefix(poll).count(), expected_votes);
298
299		// Benchmark the cleanup function.
300		#[extrinsic_call]
301		_(origin, poll, n);
302
303		// Ensure all votes are cleaned up after the extrinsic call.
304		assert_eq!(Voting::<T, I>::iter().count(), 0);
305
306		Ok(())
307	}
308
309	#[benchmark]
310	fn exchange_member() -> Result<(), BenchmarkError> {
311		// Create an existing member.
312		let who = make_member::<T, I>(1);
313		T::BenchmarkSetup::ensure_member(&who);
314		let who_lookup = T::Lookup::unlookup(who.clone());
315
316		// Create a new account for the new member.
317		let new_who = account::<T::AccountId>("new-member", 0, SEED);
318		let new_who_lookup = T::Lookup::unlookup(new_who.clone());
319
320		// Attempt to get the successful origin for exchanging a member.
321		let origin =
322			T::ExchangeOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
323
324		#[extrinsic_call]
325		_(origin as T::RuntimeOrigin, who_lookup, new_who_lookup);
326
327		// Check that the new member was successfully exchanged and holds the correct rank.
328		assert_eq!(Members::<T, I>::get(&new_who).unwrap().rank, 1);
329
330		// Ensure the old member no longer exists.
331		assert_eq!(Members::<T, I>::get(&who), None);
332
333		// Ensure the correct event was emitted.
334		assert_has_event::<T, I>(Event::MemberExchanged { who, new_who }.into());
335
336		Ok(())
337	}
338
339	impl_benchmark_test_suite!(
340		RankedCollective,
341		crate::tests::ExtBuilder::default().build(),
342		crate::tests::Test
343	);
344}