referrerpolicy=no-referrer-when-downgrade

pallet_election_provider_multi_block/signed/
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
18use crate::{
19	signed::{Config, Invulnerables, Pallet, RewardSource, Submissions, MAX_UNPAID_REWARDS},
20	types::PagedRawSolution,
21	unsigned::miner::OffchainWorkerMiner,
22	CurrentPhase, Phase, Round,
23};
24use frame_benchmarking::v2::*;
25use frame_election_provider_support::ElectionProvider;
26use frame_support::{
27	pallet_prelude::*,
28	traits::fungible::{Inspect, Mutate},
29};
30use frame_system::RawOrigin;
31use sp_npos_elections::ElectionScore;
32use sp_runtime::traits::{One, Saturating};
33use sp_std::boxed::Box;
34
35#[benchmarks(where T: crate::Config + crate::verifier::Config + crate::unsigned::Config)]
36mod benchmarks {
37	use super::*;
38
39	#[benchmark(pov_mode = Measured)]
40	fn register_not_full() -> Result<(), BenchmarkError> {
41		CurrentPhase::<T>::put(Phase::Signed(T::SignedPhase::get() - One::one()));
42		let round = Round::<T>::get();
43		let alice = crate::Pallet::<T>::funded_account("alice", 0);
44		let score = ElectionScore::default();
45
46		assert_eq!(Submissions::<T>::sorted_submitters(round).len(), 0);
47		#[block]
48		{
49			Pallet::<T>::register(RawOrigin::Signed(alice).into(), score)?;
50		}
51
52		assert_eq!(Submissions::<T>::sorted_submitters(round).len(), 1);
53		Ok(())
54	}
55
56	#[benchmark(pov_mode = Measured)]
57	fn register_eject() -> Result<(), BenchmarkError> {
58		CurrentPhase::<T>::put(Phase::Signed(T::SignedPhase::get() - One::one()));
59		let round = Round::<T>::get();
60
61		for i in 0..T::MaxSubmissions::get() {
62			let submitter = crate::Pallet::<T>::funded_account("submitter", i);
63			let score = ElectionScore { minimal_stake: i.into(), ..Default::default() };
64			Pallet::<T>::register(RawOrigin::Signed(submitter.clone()).into(), score)?;
65
66			// The first one, which will be ejected, has also submitted all pages
67			if i == 0 {
68				for p in 0..T::Pages::get() {
69					let page = Some(Default::default());
70					Pallet::<T>::submit_page(RawOrigin::Signed(submitter.clone()).into(), p, page)?;
71				}
72			}
73		}
74
75		let who = crate::Pallet::<T>::funded_account("who", 0);
76		let score =
77			ElectionScore { minimal_stake: T::MaxSubmissions::get().into(), ..Default::default() };
78
79		assert_eq!(
80			Submissions::<T>::sorted_submitters(round).len(),
81			T::MaxSubmissions::get() as usize
82		);
83
84		#[block]
85		{
86			Pallet::<T>::register(RawOrigin::Signed(who).into(), score)?;
87		}
88
89		assert_eq!(
90			Submissions::<T>::sorted_submitters(round).len(),
91			T::MaxSubmissions::get() as usize
92		);
93		Ok(())
94	}
95
96	#[benchmark(pov_mode = Measured)]
97	fn submit_page() -> Result<(), BenchmarkError> {
98		#[cfg(test)]
99		crate::mock::ElectionStart::set(sp_runtime::traits::Bounded::max_value());
100		crate::Pallet::<T>::start().unwrap();
101
102		crate::Pallet::<T>::roll_until_matches(|| {
103			matches!(CurrentPhase::<T>::get(), Phase::Signed(_))
104		});
105
106		// mine a full solution
107		let PagedRawSolution { score, solution_pages, .. } =
108			OffchainWorkerMiner::<T>::mine_solution(T::Pages::get(), false).unwrap();
109		let page = Some(Box::new(solution_pages[0].clone()));
110
111		// register alice
112		let alice = crate::Pallet::<T>::funded_account("alice", 0);
113		Pallet::<T>::register(RawOrigin::Signed(alice.clone()).into(), score)?;
114
115		#[block]
116		{
117			Pallet::<T>::submit_page(RawOrigin::Signed(alice).into(), 0, page)?;
118		}
119
120		Ok(())
121	}
122
123	#[benchmark(pov_mode = Measured)]
124	fn unset_page() -> Result<(), BenchmarkError> {
125		#[cfg(test)]
126		crate::mock::ElectionStart::set(sp_runtime::traits::Bounded::max_value());
127		crate::Pallet::<T>::start().unwrap();
128
129		crate::Pallet::<T>::roll_until_matches(|| {
130			matches!(CurrentPhase::<T>::get(), Phase::Signed(_))
131		});
132
133		// mine a full solution
134		let PagedRawSolution { score, solution_pages, .. } =
135			OffchainWorkerMiner::<T>::mine_solution(T::Pages::get(), false).unwrap();
136		let page = Some(Box::new(solution_pages[0].clone()));
137
138		// register alice
139		let alice = crate::Pallet::<T>::funded_account("alice", 0);
140		Pallet::<T>::register(RawOrigin::Signed(alice.clone()).into(), score)?;
141
142		// submit page
143		Pallet::<T>::submit_page(RawOrigin::Signed(alice.clone()).into(), 0, page)?;
144
145		#[block]
146		{
147			Pallet::<T>::submit_page(RawOrigin::Signed(alice).into(), 0, None)?;
148		}
149
150		Ok(())
151	}
152
153	#[benchmark(pov_mode = Measured)]
154	fn bail() -> Result<(), BenchmarkError> {
155		CurrentPhase::<T>::put(Phase::Signed(T::SignedPhase::get() - One::one()));
156		let alice = crate::Pallet::<T>::funded_account("alice", 0);
157
158		// register alice
159		let score = ElectionScore::default();
160		Pallet::<T>::register(RawOrigin::Signed(alice.clone()).into(), score)?;
161
162		// submit all pages
163		for p in 0..T::Pages::get() {
164			let page = Some(Default::default());
165			Pallet::<T>::submit_page(RawOrigin::Signed(alice.clone()).into(), p, page)?;
166		}
167
168		#[block]
169		{
170			Pallet::<T>::bail(RawOrigin::Signed(alice).into())?;
171		}
172
173		Ok(())
174	}
175
176	#[benchmark(pov_mode = Measured)]
177	fn clear_old_round_data(p: Linear<1, { T::Pages::get() }>) -> Result<(), BenchmarkError> {
178		// set signed phase and alice ready to submit
179		CurrentPhase::<T>::put(Phase::Signed(T::SignedPhase::get() - One::one()));
180		let alice = crate::Pallet::<T>::funded_account("alice", 0);
181
182		// worst case: an invulnerable is also refunded their tx-fee upon clearing.
183		Invulnerables::<T>::put(BoundedVec::truncate_from(sp_std::vec![alice.clone()]));
184
185		// register alice
186		let score = ElectionScore::default();
187		Pallet::<T>::register(RawOrigin::Signed(alice.clone()).into(), score)?;
188
189		// submit a solution with p pages.
190		for pp in 0..p {
191			let page = Some(Default::default());
192			Pallet::<T>::submit_page(RawOrigin::Signed(alice.clone()).into(), pp, page)?;
193		}
194
195		// force rotate to the next round.
196		let prev_round = Round::<T>::get();
197		crate::Pallet::<T>::rotate_round();
198
199		let source_and_balance_before = if let Some(source) = T::RewardSource::account() {
200			let funds =
201				<T as Config>::MaxFeeRefund::get().saturating_add(T::Currency::minimum_balance());
202			T::Currency::mint_into(&source, funds)?;
203			Some((source.clone(), T::Currency::balance(&source)))
204		} else {
205			None
206		};
207
208		#[block]
209		{
210			Pallet::<T>::clear_old_round_data(RawOrigin::Signed(alice).into(), prev_round, p)?;
211		}
212
213		if let Some((source, balance_before)) = source_and_balance_before {
214			assert!(
215				T::Currency::balance(&source) < balance_before,
216				"fee refund must have transferred out of the configured RewardSource pot"
217			);
218		}
219		Ok(())
220	}
221
222	#[benchmark(pov_mode = Measured)]
223	fn claim_unpaid_reward() -> Result<(), BenchmarkError> {
224		// Worst case: UnpaidRewards is full, and the claimed entry is the last one scanned.
225		for i in 0..MAX_UNPAID_REWARDS {
226			let who = crate::Pallet::<T>::funded_account("filler", i);
227			let entry = crate::signed::UnpaidReward::<T> {
228				round: i,
229				who,
230				amount: <T as Config>::RewardBase::get(),
231			};
232			crate::signed::UnpaidRewards::<T>::try_mutate(|unpaid| unpaid.try_push(entry))
233				.map_err(|_| BenchmarkError::Stop("UnpaidRewards is full"))?;
234		}
235		let target_round = MAX_UNPAID_REWARDS - 1;
236
237		// The claim pays out of `RewardSource`, so it must be able to cover one entry and still
238		// hold ED afterwards, as the payout uses `Preservation::Preserve`. A `None` source mints
239		// and needs no funding.
240		let source_and_balance_before = if let Some(source) = T::RewardSource::account() {
241			let funds =
242				<T as Config>::RewardBase::get().saturating_add(T::Currency::minimum_balance());
243			T::Currency::mint_into(&source, funds)?;
244			Some((source.clone(), T::Currency::balance(&source)))
245		} else {
246			None
247		};
248
249		let caller = crate::Pallet::<T>::funded_account("caller", 0);
250
251		#[block]
252		{
253			Pallet::<T>::claim_unpaid_reward(RawOrigin::Signed(caller).into(), target_round)?;
254		}
255
256		assert_eq!(crate::signed::UnpaidRewards::<T>::get().len(), MAX_UNPAID_REWARDS as usize - 1);
257		// Guard against silently measuring the mint fallback instead of the real transfer: if a
258		// pot is configured, its balance must have dropped by the claimed amount.
259		if let Some((source, balance_before)) = source_and_balance_before {
260			assert!(
261				T::Currency::balance(&source) < balance_before,
262				"claim must have transferred out of the configured RewardSource pot"
263			);
264		}
265		Ok(())
266	}
267
268	impl_benchmark_test_suite!(
269		Pallet,
270		crate::mock::ExtBuilder::signed().build_unchecked(),
271		crate::mock::Runtime
272	);
273}