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 <<T as pallet::Config<I>>::Polls as frame_support::traits::Polling<Tally<T, I, pallet::Pallet<T, I>>>>::Index: From<u8>
62)]
63mod benchmarks {
64	use super::*;
65
66	#[benchmark]
67	fn add_member() -> Result<(), BenchmarkError> {
68		// Generate a test account for the new member.
69		let who = account::<T::AccountId>("member", 0, SEED);
70		let who_lookup = T::Lookup::unlookup(who.clone());
71
72		// Attempt to get the successful origin for adding a member.
73		let origin =
74			T::AddOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
75
76		#[extrinsic_call]
77		_(origin as T::RuntimeOrigin, who_lookup);
78
79		// Ensure the member count has increased (or is 1 for rank 0).
80		assert_eq!(MemberCount::<T, I>::get(0), 1);
81
82		// Check that the correct event was emitted.
83		assert_last_event::<T, I>(Event::MemberAdded { who }.into());
84
85		Ok(())
86	}
87
88	#[benchmark]
89	fn remove_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
90		// Convert `r` to a rank and create members.
91		let rank = r as u16;
92		let who = make_member::<T, I>(rank);
93		let who_lookup = T::Lookup::unlookup(who.clone());
94		let last = make_member::<T, I>(rank);
95
96		// Collect the index of the `last` member for each rank.
97		let last_index: Vec<_> =
98			(0..=rank).map(|r| IdToIndex::<T, I>::get(r, &last).unwrap()).collect();
99
100		// Fetch the remove origin.
101		let origin =
102			T::RemoveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
103
104		#[extrinsic_call]
105		_(origin as T::RuntimeOrigin, who_lookup, rank);
106
107		for r in 0..=rank {
108			assert_eq!(MemberCount::<T, I>::get(r), 1);
109			assert_ne!(last_index[r as usize], IdToIndex::<T, I>::get(r, &last).unwrap());
110		}
111
112		// Ensure the correct event was emitted for the member removal.
113		assert_last_event::<T, I>(Event::MemberRemoved { who, rank }.into());
114
115		Ok(())
116	}
117
118	#[benchmark]
119	fn promote_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
120		// Convert `r` to a rank and create the member.
121		let rank = r as u16;
122		let who = make_member::<T, I>(rank);
123		let who_lookup = T::Lookup::unlookup(who.clone());
124
125		// Try to fetch the promotion origin.
126		let origin =
127			T::PromoteOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
128
129		#[extrinsic_call]
130		_(origin as T::RuntimeOrigin, who_lookup);
131
132		// Ensure the member's rank has increased by 1.
133		assert_eq!(Members::<T, I>::get(&who).unwrap().rank, rank + 1);
134
135		// Ensure the correct event was emitted for the rank change.
136		assert_last_event::<T, I>(Event::RankChanged { who, rank: rank + 1 }.into());
137
138		Ok(())
139	}
140
141	#[benchmark]
142	fn demote_member(r: Linear<0, 10>) -> Result<(), BenchmarkError> {
143		// Convert `r` to a rank and create necessary members for the benchmark.
144		let rank = r as u16;
145		let who = make_member::<T, I>(rank);
146		let who_lookup = T::Lookup::unlookup(who.clone());
147		let last = make_member::<T, I>(rank);
148
149		// Get the last index for the member.
150		let last_index = IdToIndex::<T, I>::get(rank, &last).unwrap();
151
152		// Try to fetch the demotion origin.
153		let origin =
154			T::DemoteOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
155
156		#[extrinsic_call]
157		_(origin as T::RuntimeOrigin, who_lookup);
158
159		// Ensure the member's rank has decreased by 1.
160		assert_eq!(Members::<T, I>::get(&who).map(|x| x.rank), rank.checked_sub(1));
161
162		// Ensure the member count remains as expected.
163		assert_eq!(MemberCount::<T, I>::get(rank), 1);
164
165		// Ensure the index of the last member has changed.
166		assert_ne!(last_index, IdToIndex::<T, I>::get(rank, &last).unwrap());
167
168		// Ensure the correct event was emitted depending on the member's rank.
169		assert_last_event::<T, I>(
170			match rank {
171				0 => Event::MemberRemoved { who, rank: 0 },
172				r => Event::RankChanged { who, rank: r - 1 },
173			}
174			.into(),
175		);
176
177		Ok(())
178	}
179
180	#[benchmark]
181	fn vote() -> Result<(), BenchmarkError> {
182		// Get the first available class or set it to None if no class exists.
183		let class = T::Polls::classes().into_iter().next();
184
185		// Convert the class to a rank if it exists, otherwise use the default rank.
186		let rank = class.as_ref().map_or(
187			<Pallet<T, I> as frame_support::traits::RankedMembers>::Rank::default(),
188			|class| T::MinRankOfClass::convert(class.clone()),
189		);
190
191		// Create a caller based on the rank.
192		let caller = make_member::<T, I>(rank);
193
194		// Determine the poll to use: create an ongoing poll if class exists, or use an invalid
195		// poll.
196		let poll = if let Some(ref class) = class {
197			T::Polls::create_ongoing(class.clone())
198				.expect("Poll creation should succeed for rank 0")
199		} else {
200			<NoOpPoll<BlockNumberFor<T>> as Polling<T>>::Index::MAX.into()
201		};
202
203		// Benchmark the vote logic for a positive vote (true).
204		#[block]
205		{
206			let vote_result =
207				Pallet::<T, I>::vote(SystemOrigin::Signed(caller.clone()).into(), poll, true);
208
209			// If the class exists, expect success; otherwise expect a "NotPolling" error.
210			if class.is_some() {
211				assert_ok!(vote_result);
212			} else {
213				assert_err!(vote_result, crate::Error::<T, I>::NotPolling);
214			};
215		}
216
217		// Vote logic for a negative vote (false).
218		let vote_result =
219			Pallet::<T, I>::vote(SystemOrigin::Signed(caller.clone()).into(), poll, false);
220
221		// Check the result of the negative vote.
222		if class.is_some() {
223			assert_ok!(vote_result);
224		} else {
225			assert_err!(vote_result, crate::Error::<T, I>::NotPolling);
226		};
227
228		// If the class exists, verify the vote event and tally.
229		if let Some(_) = class {
230			let tally = Tally::from_parts(0, 0, 1);
231			let vote_event = Event::Voted { who: caller, poll, vote: VoteRecord::Nay(1), tally };
232			assert_last_event::<T, I>(vote_event.into());
233		}
234
235		Ok(())
236	}
237
238	#[benchmark]
239	fn cleanup_poll(n: Linear<0, 100>) -> Result<(), BenchmarkError> {
240		let alice: T::AccountId = whitelisted_caller();
241		let origin = SystemOrigin::Signed(alice.clone());
242
243		// Try to retrieve the first class if it exists.
244		let class = T::Polls::classes().into_iter().next();
245
246		// Convert the class to a rank, or use a default rank if no class exists.
247		let rank = class.as_ref().map_or(
248			<Pallet<T, I> as frame_support::traits::RankedMembers>::Rank::default(),
249			|class| T::MinRankOfClass::convert(class.clone()),
250		);
251
252		// Determine the poll to use: create an ongoing poll if class exists, or use an invalid
253		// poll.
254		let poll = if let Some(ref class) = class {
255			T::Polls::create_ongoing(class.clone())
256				.expect("Poll creation should succeed for rank 0")
257		} else {
258			<NoOpPoll<BlockNumberFor<T>> as Polling<T>>::Index::MAX.into()
259		};
260
261		// Simulate voting by `n` members.
262		for _ in 0..n {
263			let voter = make_member::<T, I>(rank);
264			let result = Pallet::<T, I>::vote(SystemOrigin::Signed(voter).into(), poll, true);
265
266			// Check voting results based on class existence.
267			if class.is_some() {
268				assert_ok!(result);
269			} else {
270				assert_err!(result, crate::Error::<T, I>::NotPolling);
271			}
272		}
273
274		// End the poll if the class exists.
275		if class.is_some() {
276			T::Polls::end_ongoing(poll, false)
277				.map_err(|_| BenchmarkError::Stop("Failed to end poll"))?;
278		}
279
280		// Verify the number of votes cast.
281		let expected_votes = if class.is_some() { n as usize } else { 0 };
282		assert_eq!(Voting::<T, I>::iter_prefix(poll).count(), expected_votes);
283
284		// Benchmark the cleanup function.
285		#[extrinsic_call]
286		_(origin, poll, n);
287
288		// Ensure all votes are cleaned up after the extrinsic call.
289		assert_eq!(Voting::<T, I>::iter().count(), 0);
290
291		Ok(())
292	}
293
294	#[benchmark]
295	fn exchange_member() -> Result<(), BenchmarkError> {
296		// Create an existing member.
297		let who = make_member::<T, I>(1);
298		T::BenchmarkSetup::ensure_member(&who);
299		let who_lookup = T::Lookup::unlookup(who.clone());
300
301		// Create a new account for the new member.
302		let new_who = account::<T::AccountId>("new-member", 0, SEED);
303		let new_who_lookup = T::Lookup::unlookup(new_who.clone());
304
305		// Attempt to get the successful origin for exchanging a member.
306		let origin =
307			T::ExchangeOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
308
309		#[extrinsic_call]
310		_(origin as T::RuntimeOrigin, who_lookup, new_who_lookup);
311
312		// Check that the new member was successfully exchanged and holds the correct rank.
313		assert_eq!(Members::<T, I>::get(&new_who).unwrap().rank, 1);
314
315		// Ensure the old member no longer exists.
316		assert_eq!(Members::<T, I>::get(&who), None);
317
318		// Ensure the correct event was emitted.
319		assert_has_event::<T, I>(Event::MemberExchanged { who, new_who }.into());
320
321		Ok(())
322	}
323
324	impl_benchmark_test_suite!(
325		RankedCollective,
326		crate::tests::ExtBuilder::default().build(),
327		crate::tests::Test
328	);
329}