referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
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::*;
21use crate::{
22	asset,
23	session_rotation::{Eras, Rotator},
24	Pallet as Staking,
25};
26use alloc::collections::BTreeMap;
27pub use frame_benchmarking::{
28	impl_benchmark_test_suite, v2::*, whitelist_account, whitelisted_caller, BenchmarkError,
29};
30use frame_election_provider_support::SortedListProvider;
31use frame_support::{
32	assert_ok,
33	pallet_prelude::*,
34	storage::bounded_vec::BoundedVec,
35	traits::{fungible::Inspect, TryCollect},
36};
37use frame_system::RawOrigin;
38use pallet_staking_async_rc_client as rc_client;
39use sp_runtime::{
40	traits::{Bounded, One, StaticLookup, Zero},
41	Perbill, Percent, Saturating,
42};
43use sp_staking::currency_to_vote::CurrencyToVote;
44use testing_utils::*;
45
46const SEED: u32 = 0;
47
48// This function clears all existing validators and nominators from the set, and generates one new
49// validator being nominated by n nominators, and returns the validator stash account and the
50// nominators' stash and controller. It also starts plans a new era with this new stakers, and
51// returns the planned era index.
52pub(crate) fn create_validator_with_nominators<T: Config>(
53	n: u32,
54	upper_bound: u32,
55	dead_controller: bool,
56	unique_controller: bool,
57	destination: RewardDestination<T::AccountId>,
58) -> Result<(T::AccountId, Vec<(T::AccountId, T::AccountId)>, EraIndex), &'static str> {
59	// TODO: this can be replaced with `testing_utils` version?
60	// Clean up any existing state.
61	clear_validators_and_nominators::<T>();
62
63	// Disable legacy minting so benchmarks always exercise the reward-pot path.
64	DisableMintingGuard::<T>::put(0);
65	let mut points_total = 0;
66	let mut points_individual = Vec::new();
67
68	let (v_stash, v_controller) = if unique_controller {
69		create_unique_stash_controller::<T>(0, 100, destination.clone(), false)?
70	} else {
71		create_stash_controller::<T>(0, 100, destination.clone())?
72	};
73
74	let validator_prefs =
75		ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() };
76	Staking::<T>::validate(RawOrigin::Signed(v_controller).into(), validator_prefs)?;
77	let stash_lookup = T::Lookup::unlookup(v_stash.clone());
78
79	points_total += 10;
80	points_individual.push((v_stash.clone(), 10));
81
82	let original_nominator_count = Nominators::<T>::count();
83	let mut nominators = Vec::new();
84
85	// Give the validator n nominators, but keep total users in the system the same.
86	for i in 0..upper_bound {
87		let (n_stash, n_controller) = if !dead_controller {
88			create_stash_controller::<T>(u32::MAX - i, 100, destination.clone())?
89		} else {
90			create_unique_stash_controller::<T>(u32::MAX - i, 100, destination.clone(), true)?
91		};
92		if i < n {
93			Staking::<T>::nominate(
94				RawOrigin::Signed(n_controller.clone()).into(),
95				vec![stash_lookup.clone()],
96			)?;
97			nominators.push((n_stash, n_controller));
98		}
99	}
100
101	ValidatorCount::<T>::put(1);
102
103	// Start a new Era
104	let new_validators = Rotator::<T>::legacy_insta_plan_era();
105	let planned_era = CurrentEra::<T>::get().unwrap_or_default();
106
107	assert_eq!(new_validators.len(), 1, "New validators is not 1");
108	assert_eq!(new_validators[0], v_stash, "Our validator was not selected");
109	assert_ne!(Validators::<T>::count(), 0, "New validators count wrong");
110	assert_eq!(
111		Nominators::<T>::count(),
112		original_nominator_count + nominators.len() as u32,
113		"New nominators count wrong"
114	);
115
116	// Give Era Points
117	let reward = EraRewardPoints::<T> {
118		total: points_total,
119		individual: points_individual.into_iter().try_collect()?,
120	};
121
122	ErasRewardPoints::<T>::insert(planned_era, reward);
123
124	// Create reward pool
125	let total_payout = asset::existential_deposit::<T>()
126		.saturating_mul(upper_bound.into())
127		.saturating_mul(1000u32.into());
128	<ErasValidatorReward<T>>::insert(planned_era, total_payout);
129
130	// Create and fund the era reward pot so payout_stakers can transfer from it.
131	let era_pot =
132		crate::reward::EraRewardManager::<T>::create(planned_era, RewardKind::StakerRewards);
133	let _ = asset::mint_creating::<T>(&era_pot, total_payout);
134
135	// Set up validator incentive so payout benchmarks include the incentive transfer cost.
136	OptimumSelfStake::<T>::put(BalanceOf::<T>::from(30_000u64));
137	HardCapSelfStake::<T>::put(BalanceOf::<T>::from(100_000u64));
138	SelfStakeSlopeFactor::<T>::put(Perbill::from_percent(50));
139
140	let incentive_payout = total_payout / 10u32.into(); // 10% of total as incentive budget
141	let incentive_pot =
142		crate::reward::EraRewardManager::<T>::create(planned_era, RewardKind::ValidatorSelfStake);
143	let _ = asset::mint_creating::<T>(&incentive_pot, incentive_payout);
144	ErasValidatorIncentiveBudget::<T>::insert(planned_era, incentive_payout);
145
146	// Single-validator benchmark setup: sum == this validator's weight.
147	let incentive_weight = BalanceOf::<T>::from(100u64);
148	ErasValidatorIncentiveWeight::<T>::insert(planned_era, &v_stash, incentive_weight);
149	ErasSumValidatorIncentiveWeight::<T>::insert(planned_era, incentive_weight);
150
151	Ok((v_stash, nominators, planned_era))
152}
153
154struct ListScenario<T: Config> {
155	/// Stash that is expected to be moved.
156	origin_stash1: T::AccountId,
157	/// Controller of the Stash that is expected to be moved.
158	origin_controller1: T::AccountId,
159	dest_weight: BalanceOf<T>,
160}
161
162impl<T: Config> ListScenario<T> {
163	/// An expensive scenario for bags-list implementation:
164	///
165	/// - the node to be updated (r) is the head of a bag that has at least one other node. The bag
166	///   itself will need to be read and written to update its head. The node pointed to by r.next
167	///   will need to be read and written as it will need to have its prev pointer updated. Note
168	///   that there are two other worst case scenarios for bag removal: 1) the node is a tail and
169	///   2) the node is a middle node with prev and next; all scenarios end up with the same number
170	///   of storage reads and writes.
171	///
172	/// - the destination bag has at least one node, which will need its next pointer updated.
173	///
174	/// NOTE: while this scenario specifically targets a worst case for the bags-list, it should
175	/// also elicit a worst case for other known `VoterList` implementations; although
176	/// this may not be true against unknown `VoterList` implementations.
177	fn new(origin_weight: BalanceOf<T>, is_increase: bool) -> Result<Self, &'static str> {
178		ensure!(!origin_weight.is_zero(), "origin weight must be greater than 0");
179
180		// burn the entire issuance.
181		let i = asset::burn::<T>(asset::total_issuance::<T>());
182		core::mem::forget(i);
183
184		// make sure `account("random_validator", 0, SEED)` is a validator
185		let validator_account = account("random_validator", 0, SEED);
186		let validator_stake = asset::existential_deposit::<T>() * 1000u32.into();
187		asset::set_stakeable_balance::<T>(&validator_account, validator_stake);
188		assert_ok!(Staking::<T>::bond(
189			RawOrigin::Signed(validator_account.clone()).into(),
190			validator_stake / 2u32.into(),
191			RewardDestination::Staked
192		));
193		assert_ok!(Staking::<T>::validate(
194			RawOrigin::Signed(validator_account.clone()).into(),
195			Default::default()
196		));
197
198		// create accounts with the origin weight
199		let (origin_stash1, origin_controller1) = create_stash_controller_with_balance::<T>(
200			USER_SEED + 2,
201			origin_weight,
202			RewardDestination::Staked,
203		)?;
204		Staking::<T>::nominate(
205			RawOrigin::Signed(origin_controller1.clone()).into(),
206			// NOTE: these don't really need to be validators.
207			vec![T::Lookup::unlookup(validator_account.clone())],
208		)?;
209
210		let (_origin_stash2, origin_controller2) = create_stash_controller_with_balance::<T>(
211			USER_SEED + 3,
212			origin_weight,
213			RewardDestination::Staked,
214		)?;
215		Staking::<T>::nominate(
216			RawOrigin::Signed(origin_controller2).into(),
217			vec![T::Lookup::unlookup(validator_account.clone())],
218		)?;
219
220		// find a destination weight that will trigger the worst case scenario
221		let dest_weight_as_vote =
222			T::VoterList::score_update_worst_case(&origin_stash1, is_increase);
223
224		let total_issuance = asset::total_issuance::<T>();
225
226		let dest_weight =
227			T::CurrencyToVote::to_currency(dest_weight_as_vote as u128, total_issuance);
228
229		// create an account with the worst case destination weight
230		let (_dest_stash1, dest_controller1) = create_stash_controller_with_balance::<T>(
231			USER_SEED + 1,
232			dest_weight,
233			RewardDestination::Staked,
234		)?;
235		Staking::<T>::nominate(
236			RawOrigin::Signed(dest_controller1).into(),
237			vec![T::Lookup::unlookup(validator_account)],
238		)?;
239
240		Ok(ListScenario { origin_stash1, origin_controller1, dest_weight })
241	}
242}
243
244const USER_SEED: u32 = 999666;
245
246#[benchmarks]
247mod benchmarks {
248	use super::*;
249	use alloc::format;
250
251	#[benchmark]
252	fn bond() {
253		let stash = create_funded_user::<T>("stash", USER_SEED, 100);
254		let reward_destination = RewardDestination::Staked;
255		let amount = asset::existential_deposit::<T>() * 10u32.into();
256		whitelist_account!(stash);
257
258		#[extrinsic_call]
259		_(RawOrigin::Signed(stash.clone()), amount, reward_destination);
260
261		assert!(Bonded::<T>::contains_key(stash.clone()));
262		assert!(Ledger::<T>::contains_key(stash));
263	}
264
265	#[benchmark]
266	fn bond_extra() -> Result<(), BenchmarkError> {
267		// clean up any existing state.
268		clear_validators_and_nominators::<T>();
269
270		let origin_weight = Staking::<T>::min_nominator_bond();
271
272		// setup the worst case list scenario.
273
274		// the weight the nominator will start at.
275		let scenario = ListScenario::<T>::new(origin_weight, true)?;
276
277		let max_additional = scenario.dest_weight - origin_weight;
278
279		let stash = scenario.origin_stash1.clone();
280		let controller = scenario.origin_controller1;
281		let original_bonded: BalanceOf<T> = Ledger::<T>::get(&controller)
282			.map(|l| l.active)
283			.ok_or("ledger not created after")?;
284
285		let _ = asset::mint_into_existing::<T>(
286			&stash,
287			max_additional + asset::existential_deposit::<T>(),
288		)
289		.unwrap();
290
291		whitelist_account!(stash);
292
293		#[extrinsic_call]
294		_(RawOrigin::Signed(stash), max_additional);
295
296		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created after")?;
297		let new_bonded: BalanceOf<T> = ledger.active;
298		assert!(original_bonded < new_bonded);
299
300		Ok(())
301	}
302
303	#[benchmark]
304	fn unbond() -> Result<(), BenchmarkError> {
305		// clean up any existing state.
306		clear_validators_and_nominators::<T>();
307
308		// the weight the nominator will start at. The value used here is expected to be
309		// significantly higher than the first position in a list (e.g. the first bag threshold).
310		let origin_weight = BalanceOf::<T>::try_from(952_994_955_240_703u128)
311			.map_err(|_| "balance expected to be a u128")
312			.unwrap();
313		let scenario = ListScenario::<T>::new(origin_weight, false)?;
314
315		let controller = scenario.origin_controller1.clone();
316		let amount = origin_weight - scenario.dest_weight;
317		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created before")?;
318		let original_bonded: BalanceOf<T> = ledger.active;
319
320		whitelist_account!(controller);
321
322		#[extrinsic_call]
323		_(RawOrigin::Signed(controller.clone()), amount);
324
325		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created after")?;
326		let new_bonded: BalanceOf<T> = ledger.active;
327		assert!(original_bonded > new_bonded);
328
329		Ok(())
330	}
331
332	#[benchmark]
333	// Withdraw only updates the ledger
334	fn withdraw_unbonded_update() -> Result<(), BenchmarkError> {
335		let (_, controller) = create_stash_controller::<T>(0, 100, RewardDestination::Staked)?;
336		let amount = asset::existential_deposit::<T>() * 5u32.into(); // Half of total
337		Staking::<T>::unbond(RawOrigin::Signed(controller.clone()).into(), amount)?;
338		set_active_era::<T>(EraIndex::max_value());
339		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created before")?;
340		let original_total: BalanceOf<T> = ledger.total;
341		whitelist_account!(controller);
342
343		#[extrinsic_call]
344		withdraw_unbonded(RawOrigin::Signed(controller.clone()), 0);
345
346		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created after")?;
347		let new_total: BalanceOf<T> = ledger.total;
348		assert!(original_total > new_total);
349
350		Ok(())
351	}
352
353	#[benchmark]
354	// Worst case scenario, everything is removed after the bonding duration
355	fn withdraw_unbonded_kill() -> Result<(), BenchmarkError> {
356		// clean up any existing state.
357		clear_validators_and_nominators::<T>();
358
359		let origin_weight = Staking::<T>::min_nominator_bond();
360
361		// setup a worst case list scenario. Note that we don't care about the setup of the
362		// destination position because we are doing a removal from the list but no insert.
363		let scenario = ListScenario::<T>::new(origin_weight, true)?;
364		let controller = scenario.origin_controller1.clone();
365		let stash = scenario.origin_stash1;
366		assert!(T::VoterList::contains(&stash));
367
368		let ed = asset::existential_deposit::<T>();
369		let mut ledger = Ledger::<T>::get(&controller).unwrap();
370		ledger.active = ed - One::one();
371		Ledger::<T>::insert(&controller, ledger);
372		set_active_era::<T>(EraIndex::max_value());
373
374		whitelist_account!(controller);
375
376		#[extrinsic_call]
377		withdraw_unbonded(RawOrigin::Signed(controller.clone()), 0);
378
379		assert!(!Ledger::<T>::contains_key(controller));
380		assert!(!T::VoterList::contains(&stash));
381
382		Ok(())
383	}
384
385	#[benchmark]
386	fn validate() -> Result<(), BenchmarkError> {
387		let (stash, controller) = create_stash_controller::<T>(
388			MaxNominationsOf::<T>::get() - 1,
389			100,
390			RewardDestination::Staked,
391		)?;
392		// because it is chilled.
393		assert!(!T::VoterList::contains(&stash));
394
395		let prefs = ValidatorPrefs::default();
396		whitelist_account!(controller);
397
398		#[extrinsic_call]
399		_(RawOrigin::Signed(controller), prefs);
400
401		assert!(Validators::<T>::contains_key(&stash));
402		assert!(T::VoterList::contains(&stash));
403
404		Ok(())
405	}
406
407	#[benchmark]
408	fn kick(
409		// scenario: we want to kick `k` nominators from nominating us (we are a validator).
410		// we'll assume that `k` is under 128 for the purposes of determining the slope.
411		// each nominator should have `T::MaxNominations::get()` validators nominated, and our
412		// validator should be somewhere in there.
413		k: Linear<1, 128>,
414	) -> Result<(), BenchmarkError> {
415		// these are the other validators; there are `T::MaxNominations::get() - 1` of them, so
416		// there are a total of `T::MaxNominations::get()` validators in the system.
417		let rest_of_validators =
418			create_validators_with_seed::<T>(MaxNominationsOf::<T>::get() - 1, 100, 415)?;
419
420		// this is the validator that will be kicking.
421		let (stash, controller) = create_stash_controller::<T>(
422			MaxNominationsOf::<T>::get() - 1,
423			100,
424			RewardDestination::Staked,
425		)?;
426		let stash_lookup = T::Lookup::unlookup(stash.clone());
427
428		// they start validating.
429		Staking::<T>::validate(RawOrigin::Signed(controller.clone()).into(), Default::default())?;
430
431		// we now create the nominators. there will be `k` of them; each will nominate all
432		// validators. we will then kick each of the `k` nominators from the main validator.
433		let mut nominator_stashes = Vec::with_capacity(k as usize);
434		for i in 0..k {
435			// create a nominator stash.
436			let (n_stash, n_controller) = create_stash_controller::<T>(
437				MaxNominationsOf::<T>::get() + i,
438				100,
439				RewardDestination::Staked,
440			)?;
441
442			// bake the nominations; we first clone them from the rest of the validators.
443			let mut nominations = rest_of_validators.clone();
444			// then insert "our" validator somewhere in there (we vary it) to avoid accidental
445			// optimisations/pessimisations.
446			nominations.insert(i as usize % (nominations.len() + 1), stash_lookup.clone());
447			// then we nominate.
448			Staking::<T>::nominate(RawOrigin::Signed(n_controller.clone()).into(), nominations)?;
449
450			nominator_stashes.push(n_stash);
451		}
452
453		// all nominators now should be nominating our validator...
454		for n in nominator_stashes.iter() {
455			assert!(Nominators::<T>::get(n).unwrap().targets.contains(&stash));
456		}
457
458		// we need the unlookuped version of the nominator stash for the kick.
459		let kicks = nominator_stashes
460			.iter()
461			.map(|n| T::Lookup::unlookup(n.clone()))
462			.collect::<Vec<_>>();
463
464		whitelist_account!(controller);
465
466		#[extrinsic_call]
467		_(RawOrigin::Signed(controller), kicks);
468
469		// all nominators now should *not* be nominating our validator...
470		for n in nominator_stashes.iter() {
471			assert!(!Nominators::<T>::get(n).unwrap().targets.contains(&stash));
472		}
473
474		Ok(())
475	}
476
477	#[benchmark]
478	// Worst case scenario, T::MaxNominations::get()
479	fn nominate(n: Linear<1, { MaxNominationsOf::<T>::get() }>) -> Result<(), BenchmarkError> {
480		// clean up any existing state.
481		clear_validators_and_nominators::<T>();
482
483		let origin_weight = Staking::<T>::min_nominator_bond();
484
485		// setup a worst case list scenario. Note we don't care about the destination position,
486		// because we are just doing an insert into the origin position.
487		ListScenario::<T>::new(origin_weight, true)?;
488		let (stash, controller) = create_stash_controller_with_balance::<T>(
489			SEED + MaxNominationsOf::<T>::get() + 1, /* make sure the account does not conflict
490			                                          * with others */
491			origin_weight,
492			RewardDestination::Staked,
493		)
494		.unwrap();
495
496		assert!(!Nominators::<T>::contains_key(&stash));
497		assert!(!T::VoterList::contains(&stash));
498
499		let validators = create_validators::<T>(n, 100).unwrap();
500		whitelist_account!(controller);
501
502		#[extrinsic_call]
503		_(RawOrigin::Signed(controller), validators);
504
505		assert!(Nominators::<T>::contains_key(&stash));
506		assert!(T::VoterList::contains(&stash));
507
508		Ok(())
509	}
510
511	#[benchmark]
512	fn chill() -> Result<(), BenchmarkError> {
513		// clean up any existing state.
514		clear_validators_and_nominators::<T>();
515
516		let origin_weight = Staking::<T>::min_nominator_bond();
517
518		// setup a worst case list scenario. Note that we don't care about the setup of the
519		// destination position because we are doing a removal from the list but no insert.
520		let scenario = ListScenario::<T>::new(origin_weight, true)?;
521		let controller = scenario.origin_controller1.clone();
522		let stash = scenario.origin_stash1;
523		assert!(T::VoterList::contains(&stash));
524
525		whitelist_account!(controller);
526
527		#[extrinsic_call]
528		_(RawOrigin::Signed(controller));
529
530		assert!(!T::VoterList::contains(&stash));
531
532		Ok(())
533	}
534
535	#[benchmark]
536	fn set_payee() -> Result<(), BenchmarkError> {
537		let (stash, controller) =
538			create_stash_controller::<T>(USER_SEED, 100, RewardDestination::Staked)?;
539		assert_eq!(Payee::<T>::get(&stash), Some(RewardDestination::Staked));
540		whitelist_account!(controller);
541
542		#[extrinsic_call]
543		_(RawOrigin::Signed(controller.clone()), RewardDestination::Account(controller.clone()));
544
545		assert_eq!(Payee::<T>::get(&stash), Some(RewardDestination::Account(controller)));
546
547		Ok(())
548	}
549
550	#[benchmark]
551	fn update_payee() -> Result<(), BenchmarkError> {
552		let (stash, controller) =
553			create_stash_controller::<T>(USER_SEED, 100, RewardDestination::Staked)?;
554		Payee::<T>::insert(&stash, {
555			#[allow(deprecated)]
556			RewardDestination::Controller
557		});
558		whitelist_account!(controller);
559
560		#[extrinsic_call]
561		_(RawOrigin::Signed(controller.clone()), controller.clone());
562
563		assert_eq!(Payee::<T>::get(&stash), Some(RewardDestination::Account(controller)));
564
565		Ok(())
566	}
567
568	#[benchmark]
569	fn set_controller() -> Result<(), BenchmarkError> {
570		let (stash, ctlr) =
571			create_unique_stash_controller::<T>(9000, 100, RewardDestination::Staked, false)?;
572		// ensure `ctlr` is the currently stored controller.
573		assert!(!Ledger::<T>::contains_key(&stash));
574		assert!(Ledger::<T>::contains_key(&ctlr));
575		assert_eq!(Bonded::<T>::get(&stash), Some(ctlr.clone()));
576
577		whitelist_account!(stash);
578
579		#[extrinsic_call]
580		_(RawOrigin::Signed(stash.clone()));
581
582		assert!(Ledger::<T>::contains_key(&stash));
583
584		Ok(())
585	}
586
587	#[benchmark]
588	fn set_validator_count() {
589		let validator_count = T::MaxValidatorSet::get() - 1;
590
591		#[extrinsic_call]
592		_(RawOrigin::Root, validator_count);
593
594		assert_eq!(ValidatorCount::<T>::get(), validator_count);
595	}
596
597	#[benchmark]
598	fn force_no_eras() {
599		#[extrinsic_call]
600		_(RawOrigin::Root);
601
602		assert_eq!(ForceEra::<T>::get(), Forcing::ForceNone);
603	}
604
605	#[benchmark]
606	fn force_new_era() {
607		#[extrinsic_call]
608		_(RawOrigin::Root);
609
610		assert_eq!(ForceEra::<T>::get(), Forcing::ForceNew);
611	}
612
613	#[benchmark]
614	fn force_new_era_always() {
615		#[extrinsic_call]
616		_(RawOrigin::Root);
617
618		assert_eq!(ForceEra::<T>::get(), Forcing::ForceAlways);
619	}
620
621	#[benchmark]
622	fn deprecate_controller_batch(
623		// We pass a dynamic number of controllers to the benchmark, up to
624		// `MaxControllersInDeprecationBatch`.
625		u: Linear<0, { T::MaxControllersInDeprecationBatch::get() }>,
626	) -> Result<(), BenchmarkError> {
627		let mut controllers: Vec<_> = vec![];
628		let mut stashes: Vec<_> = vec![];
629		for i in 0..u as u32 {
630			let (stash, controller) =
631				create_unique_stash_controller::<T>(i, 100, RewardDestination::Staked, false)?;
632			controllers.push(controller);
633			stashes.push(stash);
634		}
635		let bounded_controllers: BoundedVec<_, T::MaxControllersInDeprecationBatch> =
636			BoundedVec::try_from(controllers.clone()).unwrap();
637
638		#[extrinsic_call]
639		_(RawOrigin::Root, bounded_controllers);
640
641		for i in 0..u as u32 {
642			let stash = &stashes[i as usize];
643			let controller = &controllers[i as usize];
644			// Ledger no longer keyed by controller.
645			assert_eq!(Ledger::<T>::get(controller), None);
646			// Bonded now maps to the stash.
647			assert_eq!(Bonded::<T>::get(stash), Some(stash.clone()));
648			// Ledger is now keyed by stash.
649			assert_eq!(Ledger::<T>::get(stash).unwrap().stash, *stash);
650		}
651
652		Ok(())
653	}
654
655	#[benchmark]
656	fn force_unstake() -> Result<(), BenchmarkError> {
657		// Clean up any existing state.
658		clear_validators_and_nominators::<T>();
659
660		let origin_weight = Staking::<T>::min_nominator_bond();
661
662		// setup a worst case list scenario. Note that we don't care about the setup of the
663		// destination position because we are doing a removal from the list but no insert.
664		let scenario = ListScenario::<T>::new(origin_weight, true)?;
665		let controller = scenario.origin_controller1.clone();
666		let stash = scenario.origin_stash1;
667		assert!(T::VoterList::contains(&stash));
668
669		#[extrinsic_call]
670		_(RawOrigin::Root, stash.clone(), 0);
671
672		assert!(!Ledger::<T>::contains_key(&controller));
673		assert!(!T::VoterList::contains(&stash));
674
675		Ok(())
676	}
677
678	#[benchmark]
679	fn cancel_deferred_slash(s: Linear<1, { T::MaxValidatorSet::get() }>) {
680		let era = EraIndex::one();
681
682		// Create validators and insert slashes
683		let validators: Vec<_> = (0..s)
684			.map(|i| {
685				let validator: T::AccountId = account("validator", i, SEED);
686
687				// Insert slash for this validator
688				let slash_key = (validator.clone(), Perbill::from_percent(10), 0);
689				let unapplied_slash = UnappliedSlash::<T> {
690					validator: validator.clone(),
691					own: Zero::zero(),
692					others: WeakBoundedVec::default(),
693					reporter: Default::default(),
694					payout: Zero::zero(),
695				};
696				UnappliedSlashes::<T>::insert(era, slash_key, unapplied_slash);
697
698				validator
699			})
700			.collect();
701
702		// Convert validators to tuples with 10% slash fraction (matching the slashes created above)
703		let validator_slashes: Vec<_> =
704			validators.into_iter().map(|v| (v, Perbill::from_percent(10))).collect();
705
706		#[extrinsic_call]
707		_(RawOrigin::Root, era, validator_slashes.clone());
708
709		// Ensure cancelled slashes are stored correctly
710		let cancelled_slashes = CancelledSlashes::<T>::get(era);
711		assert_eq!(cancelled_slashes.len(), s as usize);
712	}
713
714	#[benchmark]
715	fn payout_stakers_alive_staked(
716		n: Linear<0, { T::MaxExposurePageSize::get() as u32 }>,
717	) -> Result<(), BenchmarkError> {
718		let (validator, nominators, current_era) = create_validator_with_nominators::<T>(
719			n,
720			T::MaxExposurePageSize::get() as u32,
721			false,
722			true,
723			RewardDestination::Staked,
724		)?;
725
726		// set the commission for this particular era as well.
727		<ErasValidatorPrefs<T>>::insert(
728			current_era,
729			validator.clone(),
730			Validators::<T>::get(&validator),
731		);
732
733		let caller = whitelisted_caller();
734		let balance_before = asset::stakeable_balance::<T>(&validator);
735		let mut nominator_balances_before = Vec::new();
736		for (stash, _) in &nominators {
737			let balance = asset::stakeable_balance::<T>(stash);
738			nominator_balances_before.push(balance);
739		}
740
741		#[extrinsic_call]
742		payout_stakers(RawOrigin::Signed(caller), validator.clone(), current_era);
743
744		let balance_after = asset::stakeable_balance::<T>(&validator);
745		ensure!(
746			balance_before < balance_after,
747			"Balance of validator stash should have increased after payout.",
748		);
749		for ((stash, _), balance_before) in nominators.iter().zip(nominator_balances_before.iter())
750		{
751			let balance_after = asset::stakeable_balance::<T>(stash);
752			ensure!(
753				balance_before < &balance_after,
754				"Balance of nominator stash should have increased after payout.",
755			);
756		}
757
758		Ok(())
759	}
760
761	#[benchmark]
762	fn rebond(l: Linear<1, { T::MaxUnlockingChunks::get() as u32 }>) -> Result<(), BenchmarkError> {
763		// clean up any existing state.
764		clear_validators_and_nominators::<T>();
765
766		let origin_weight = Pallet::<T>::min_nominator_bond()
767			// we use 100 to play friendly with the list threshold values in the mock
768			.max(100u32.into());
769
770		// setup a worst case list scenario.
771		let scenario = ListScenario::<T>::new(origin_weight, true)?;
772		let dest_weight = scenario.dest_weight;
773
774		// rebond an amount that will give the user dest_weight
775		let rebond_amount = dest_weight - origin_weight;
776
777		// spread that amount to rebond across `l` unlocking chunks,
778		let value = rebond_amount / l.into();
779		// if `value` is zero, we need a greater delta between dest <=> origin weight
780		assert_ne!(value, Zero::zero());
781		// so the sum of unlocking chunks puts voter into the dest bag.
782		assert!(value * l.into() + origin_weight > origin_weight);
783		assert!(value * l.into() + origin_weight <= dest_weight);
784		let unlock_chunk = UnlockChunk::<BalanceOf<T>> { value, era: EraIndex::zero() };
785
786		let controller = scenario.origin_controller1;
787		let mut staking_ledger = Ledger::<T>::get(controller.clone()).unwrap();
788
789		for _ in 0..l {
790			staking_ledger.unlocking.try_push(unlock_chunk.clone()).unwrap()
791		}
792		Ledger::<T>::insert(controller.clone(), staking_ledger.clone());
793		let original_bonded: BalanceOf<T> = staking_ledger.active;
794
795		whitelist_account!(controller);
796
797		#[extrinsic_call]
798		_(RawOrigin::Signed(controller.clone()), rebond_amount);
799
800		let ledger = Ledger::<T>::get(&controller).ok_or("ledger not created after")?;
801		let new_bonded: BalanceOf<T> = ledger.active;
802		assert!(original_bonded < new_bonded);
803
804		Ok(())
805	}
806
807	#[benchmark]
808	fn reap_stash() -> Result<(), BenchmarkError> {
809		// clean up any existing state.
810		clear_validators_and_nominators::<T>();
811
812		let origin_weight = Staking::<T>::min_nominator_bond();
813
814		// setup a worst case list scenario. Note that we don't care about the setup of the
815		// destination position because we are doing a removal from the list but no insert.
816		let scenario = ListScenario::<T>::new(origin_weight, true)?;
817		let controller = scenario.origin_controller1.clone();
818		let stash = scenario.origin_stash1;
819
820		let l =
821			StakingLedger::<T>::new(stash.clone(), asset::existential_deposit::<T>() - One::one());
822		Ledger::<T>::insert(&controller, l);
823
824		assert!(Bonded::<T>::contains_key(&stash));
825		assert!(T::VoterList::contains(&stash));
826
827		whitelist_account!(controller);
828
829		#[extrinsic_call]
830		_(RawOrigin::Signed(controller), stash.clone(), 0);
831
832		assert!(!Bonded::<T>::contains_key(&stash));
833		assert!(!T::VoterList::contains(&stash));
834
835		Ok(())
836	}
837
838	#[benchmark]
839	fn set_staking_configs_all_set() {
840		#[extrinsic_call]
841		set_staking_configs(
842			RawOrigin::Root,
843			ConfigOp::Set(BalanceOf::<T>::max_value()),
844			ConfigOp::Set(BalanceOf::<T>::max_value()),
845			ConfigOp::Set(u32::MAX),
846			ConfigOp::Set(u32::MAX),
847			ConfigOp::Set(Percent::max_value()),
848			ConfigOp::Set(Perbill::max_value()),
849			ConfigOp::Set(Percent::max_value()),
850			ConfigOp::Set(false),
851		);
852
853		assert_eq!(MinNominatorBond::<T>::get(), BalanceOf::<T>::max_value());
854		assert_eq!(MinValidatorBond::<T>::get(), BalanceOf::<T>::max_value());
855		assert_eq!(MaxNominatorsCount::<T>::get(), Some(u32::MAX));
856		assert_eq!(MaxValidatorsCount::<T>::get(), Some(u32::MAX));
857		assert_eq!(ChillThreshold::<T>::get(), Some(Percent::from_percent(100)));
858		assert_eq!(MinCommission::<T>::get(), Perbill::from_percent(100));
859		assert_eq!(MaxStakedRewards::<T>::get(), Some(Percent::from_percent(100)));
860		assert_eq!(AreNominatorsSlashable::<T>::get(), false);
861	}
862
863	#[benchmark]
864	fn set_staking_configs_all_remove() {
865		#[extrinsic_call]
866		set_staking_configs(
867			RawOrigin::Root,
868			ConfigOp::Remove,
869			ConfigOp::Remove,
870			ConfigOp::Remove,
871			ConfigOp::Remove,
872			ConfigOp::Remove,
873			ConfigOp::Remove,
874			ConfigOp::Remove,
875			ConfigOp::Remove,
876		);
877
878		assert!(!MinNominatorBond::<T>::exists());
879		assert!(!MinValidatorBond::<T>::exists());
880		assert!(!MaxNominatorsCount::<T>::exists());
881		assert!(!MaxValidatorsCount::<T>::exists());
882		assert!(!ChillThreshold::<T>::exists());
883		assert!(!MinCommission::<T>::exists());
884		assert!(!MaxStakedRewards::<T>::exists());
885		assert!(!AreNominatorsSlashable::<T>::exists());
886	}
887
888	#[benchmark]
889	fn chill_other() -> Result<(), BenchmarkError> {
890		// clean up any existing state.
891		clear_validators_and_nominators::<T>();
892
893		let origin_weight = Staking::<T>::min_nominator_bond();
894
895		// setup a worst case list scenario. Note that we don't care about the setup of the
896		// destination position because we are doing a removal from the list but no insert.
897		let scenario = ListScenario::<T>::new(origin_weight, true)?;
898		let stash = scenario.origin_stash1;
899		assert!(T::VoterList::contains(&stash));
900
901		Staking::<T>::set_staking_configs(
902			RawOrigin::Root.into(),
903			ConfigOp::Set(BalanceOf::<T>::max_value()),
904			ConfigOp::Set(BalanceOf::<T>::max_value()),
905			ConfigOp::Set(0),
906			ConfigOp::Set(0),
907			ConfigOp::Set(Percent::from_percent(0)),
908			ConfigOp::Set(Zero::zero()),
909			ConfigOp::Noop,
910			ConfigOp::Noop,
911		)?;
912
913		let caller = whitelisted_caller();
914
915		#[extrinsic_call]
916		_(RawOrigin::Signed(caller), stash.clone());
917
918		assert!(!T::VoterList::contains(&stash));
919
920		Ok(())
921	}
922
923	#[benchmark]
924	fn force_apply_min_commission() -> Result<(), BenchmarkError> {
925		// Clean up any existing state
926		clear_validators_and_nominators::<T>();
927
928		// Create a validator with a commission of 50%. `balance_factor = 100` gives a bonded
929		// amount of `ED * 10`, above `min_chilled_bond` under the mock defaults.
930		let (stash, controller) = create_stash_controller::<T>(1, 100, RewardDestination::Staked)?;
931		let validator_prefs =
932			ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() };
933		Staking::<T>::validate(RawOrigin::Signed(controller).into(), validator_prefs)?;
934
935		// Sanity check
936		assert_eq!(
937			Validators::<T>::get(&stash),
938			ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() }
939		);
940
941		// Set the min commission to 75%
942		MinCommission::<T>::set(Perbill::from_percent(75));
943		let caller = whitelisted_caller();
944
945		#[extrinsic_call]
946		_(RawOrigin::Signed(caller), stash.clone());
947
948		// The validators commission has been bumped to 75%
949		assert_eq!(
950			Validators::<T>::get(&stash),
951			ValidatorPrefs { commission: Perbill::from_percent(75), ..Default::default() }
952		);
953
954		Ok(())
955	}
956
957	#[benchmark]
958	fn set_min_commission() {
959		let min_commission = Perbill::max_value();
960
961		#[extrinsic_call]
962		_(RawOrigin::Root, min_commission);
963
964		assert_eq!(MinCommission::<T>::get(), Perbill::from_percent(100));
965	}
966
967	#[benchmark]
968	fn set_max_commission() {
969		let max_commission = Perbill::max_value();
970
971		#[extrinsic_call]
972		_(RawOrigin::Root, max_commission);
973
974		assert_eq!(MaxCommission::<T>::get(), Perbill::from_percent(100));
975	}
976
977	#[benchmark]
978	fn set_validator_self_stake_incentive_config() {
979		#[extrinsic_call]
980		_(
981			RawOrigin::Root,
982			ConfigOp::Set(30_000u32.into()),
983			ConfigOp::Set(100_000u32.into()),
984			ConfigOp::Set(Perbill::from_percent(50)),
985		);
986
987		assert_eq!(OptimumSelfStake::<T>::get(), 30_000u32.into());
988		assert_eq!(HardCapSelfStake::<T>::get(), 100_000u32.into());
989		assert_eq!(SelfStakeSlopeFactor::<T>::get(), Perbill::from_percent(50));
990	}
991
992	#[benchmark]
993	fn restore_ledger() -> Result<(), BenchmarkError> {
994		let (stash, controller) = create_stash_controller::<T>(0, 100, RewardDestination::Staked)?;
995		// corrupt ledger.
996		Ledger::<T>::remove(controller);
997
998		#[extrinsic_call]
999		_(RawOrigin::Root, stash.clone(), None, None, None);
1000
1001		assert_eq!(Staking::<T>::inspect_bond_state(&stash), Ok(LedgerIntegrityState::Ok));
1002
1003		Ok(())
1004	}
1005
1006	#[benchmark]
1007	fn migrate_currency() -> Result<(), BenchmarkError> {
1008		let (stash, _ctrl) =
1009			create_stash_controller::<T>(USER_SEED, 100, RewardDestination::Staked)?;
1010		let stake = asset::staked::<T>(&stash);
1011		migrate_to_old_currency::<T>(stash.clone());
1012		// no holds
1013		assert!(asset::staked::<T>(&stash).is_zero());
1014		whitelist_account!(stash);
1015
1016		#[extrinsic_call]
1017		_(RawOrigin::Signed(stash.clone()), stash.clone());
1018
1019		assert_eq!(asset::staked::<T>(&stash), stake);
1020		Ok(())
1021	}
1022
1023	#[benchmark]
1024	fn apply_slash(
1025		n: Linear<0, { T::MaxExposurePageSize::get() as u32 }>,
1026	) -> Result<(), BenchmarkError> {
1027		let era = EraIndex::one();
1028		ActiveEra::<T>::put(ActiveEraInfo { index: era, start: None });
1029		let (validator, nominators, _current_era) = create_validator_with_nominators::<T>(
1030			T::MaxExposurePageSize::get() as u32,
1031			T::MaxExposurePageSize::get() as u32,
1032			false,
1033			true,
1034			RewardDestination::Staked,
1035		)?;
1036		let slash_fraction = Perbill::from_percent(10);
1037		let page_index = 0;
1038		let slashed_balance = BalanceOf::<T>::from(10u32);
1039
1040		let slash_key = (validator.clone(), slash_fraction, page_index);
1041		// Only include `n` nominators in the slash (to benchmark variable nominator counts)
1042		let slashed_nominators = nominators
1043			.iter()
1044			.take(n as usize)
1045			.map(|(nom, _)| (nom.clone(), slashed_balance))
1046			.collect::<Vec<_>>();
1047
1048		let unapplied_slash = UnappliedSlash::<T> {
1049			validator: validator.clone(),
1050			own: slashed_balance,
1051			others: WeakBoundedVec::force_from(slashed_nominators, None),
1052			reporter: Default::default(),
1053			payout: Zero::zero(),
1054		};
1055
1056		// Insert an unapplied slash to be processed.
1057		UnappliedSlashes::<T>::insert(era, slash_key.clone(), unapplied_slash);
1058
1059		#[extrinsic_call]
1060		_(RawOrigin::Signed(validator.clone()), era, slash_key.clone());
1061
1062		// Ensure the slash has been applied and removed.
1063		assert!(UnappliedSlashes::<T>::get(era, &slash_key).is_none());
1064
1065		Ok(())
1066	}
1067
1068	#[benchmark]
1069	fn process_offence_queue() -> Result<(), BenchmarkError> {
1070		// in tests, it is likely that `SlashDeferDuration` is zero and this will also insta-apply
1071		// the slash. Remove this just in case.
1072		#[cfg(test)]
1073		crate::mock::SlashDeferDuration::set(77);
1074
1075		// create at least one validator with a full page of exposure, as per `MaxExposurePageSize`.
1076		let all_validators = crate::testing_utils::create_validators_with_nominators_for_era::<T>(
1077			// we create more validators, but all of the nominators will back the first one
1078			ValidatorCount::<T>::get().max(1),
1079			// create two full exposure pages
1080			2 * T::MaxExposurePageSize::get(),
1081			16,
1082			false,
1083			Some(1),
1084		)?;
1085		let offender =
1086			T::Lookup::lookup(all_validators.first().cloned().expect("must exist")).unwrap();
1087
1088		// plan an era with this set
1089		let _new_validators = Rotator::<T>::legacy_insta_plan_era();
1090		// activate the previous one
1091		Rotator::<T>::start_era(
1092			crate::ActiveEraInfo { index: Rotator::<T>::planned_era() - 1, start: Some(1) },
1093			42, // start session index doesn't really matter,
1094			2,  // timestamp doesn't really matter
1095		);
1096
1097		// ensure our offender has at least a full exposure page
1098		let offender_exposure =
1099			Eras::<T>::get_full_exposure(Rotator::<T>::planned_era(), &offender);
1100		ensure!(
1101			offender_exposure.others.len() as u32 >= T::MaxExposurePageSize::get(),
1102			"exposure not created"
1103		);
1104
1105		// create an offence for this validator
1106		let slash_session = 42;
1107		let offences = vec![rc_client::Offence {
1108			offender: offender.clone(),
1109			reporters: Default::default(),
1110			slash_fraction: Perbill::from_percent(50),
1111		}];
1112		<crate::Pallet<T> as rc_client::AHStakingInterface>::on_new_offences(
1113			slash_session,
1114			offences,
1115		);
1116
1117		// ensure offence is submitted
1118		ensure!(
1119			ValidatorSlashInEra::<T>::contains_key(Rotator::<T>::active_era(), offender),
1120			"offence not submitted"
1121		);
1122		ensure!(
1123			OffenceQueueEras::<T>::get().unwrap_or_default() == vec![Rotator::<T>::active_era()],
1124			"offence should be queued"
1125		);
1126
1127		#[block]
1128		{
1129			slashing::process_offence::<T>();
1130		}
1131
1132		ensure!(OffenceQueueEras::<T>::get().is_none(), "offence should not be queued");
1133
1134		Ok(())
1135	}
1136
1137	#[benchmark]
1138	fn rc_on_offence(
1139		v: Linear<2, { T::MaxValidatorSet::get() / 2 }>,
1140	) -> Result<(), BenchmarkError> {
1141		let initial_era = Rotator::<T>::planned_era();
1142		let _ = crate::testing_utils::create_validators_with_nominators_for_era::<T>(
1143			2 * v,
1144			// number of nominators is irrelevant here, so we hardcode these
1145			1000,
1146			16,
1147			false,
1148			None,
1149		)?;
1150
1151		// plan new era
1152		let new_validators = Rotator::<T>::legacy_insta_plan_era();
1153		ensure!(Rotator::<T>::planned_era() == initial_era + 1, "era should be incremented");
1154		// activate the previous one
1155		Rotator::<T>::start_era(
1156			crate::ActiveEraInfo { index: initial_era, start: Some(1) },
1157			42, // start session index doesn't really matter,
1158			2,  // timestamp doesn't really matter
1159		);
1160
1161		// this is needed in the slashing code, and is a sign that `initial_era + 1` is planned!
1162		ensure!(Rotator::<T>::active_era_start_session_index() == 42, "BondedEra not set");
1163
1164		// slash the first half of the validators
1165		let to_slash_count = new_validators.len() / 2;
1166		let to_slash = new_validators.into_iter().take(to_slash_count).collect::<Vec<_>>();
1167		let one_slashed = to_slash.first().cloned().unwrap();
1168		let offences = to_slash
1169			.into_iter()
1170			.map(|offender| rc_client::Offence {
1171				offender,
1172				reporters: Default::default(),
1173				slash_fraction: Perbill::from_percent(50),
1174			})
1175			.collect::<Vec<_>>();
1176		let slash_session = 42;
1177
1178		// has not pending slash for these guys now
1179		ensure!(
1180			!ValidatorSlashInEra::<T>::contains_key(initial_era + 1, &one_slashed),
1181			"offence submitted???"
1182		);
1183
1184		#[block]
1185		{
1186			<crate::Pallet<T> as rc_client::AHStakingInterface>::on_new_offences(
1187				slash_session,
1188				offences,
1189			);
1190		}
1191
1192		// ensure offence is recorded
1193		ensure!(
1194			ValidatorSlashInEra::<T>::contains_key(initial_era + 1, one_slashed),
1195			"offence not submitted"
1196		);
1197
1198		Ok(())
1199	}
1200
1201	#[benchmark]
1202	fn rc_on_session_report() -> Result<(), BenchmarkError> {
1203		let initial_planned_era = Rotator::<T>::planned_era();
1204		let initial_active_era = Rotator::<T>::active_era();
1205
1206		// create a small, arbitrary number of stakers. This is just for sanity of the era planning,
1207		// numbers don't matter.
1208		crate::testing_utils::create_validators_with_nominators_for_era::<T>(
1209			10, 50, 2, false, None,
1210		)?;
1211
1212		// plan new era
1213		let _new_validators = Rotator::<T>::legacy_insta_plan_era();
1214		ensure!(
1215			CurrentEra::<T>::get().unwrap() == initial_planned_era + 1,
1216			"era should be incremented"
1217		);
1218
1219		//  receive a session report with timestamp that actives the previous one.
1220		let validator_points = (0..T::MaxValidatorSet::get())
1221			.map(|v| (account::<T::AccountId>("random", v, SEED), v))
1222			.collect::<Vec<_>>();
1223		let activation_timestamp = Some((1u64, initial_planned_era + 1));
1224		let report = rc_client::SessionReport {
1225			end_index: 42,
1226			leftover: false,
1227			validator_points,
1228			activation_timestamp,
1229		};
1230
1231		#[block]
1232		{
1233			<crate::Pallet<T> as rc_client::AHStakingInterface>::on_relay_session_report(report);
1234		}
1235
1236		ensure!(Rotator::<T>::active_era() == initial_active_era + 1, "active era not bumped");
1237		Ok(())
1238	}
1239
1240	// Helper function to set up era data for pruning benchmarks
1241	fn setup_era_for_pruning<T: Config>(v: u32) -> EraIndex {
1242		let validators = v;
1243		let era = 7;
1244
1245		// Set active era to make era 7 prunable
1246		// Era is prunable if: era <= active_era - history_depth - 1
1247		let history_depth = T::HistoryDepth::get();
1248		let active_era = era + history_depth + 1;
1249		crate::ActiveEra::<T>::put(crate::ActiveEraInfo { index: active_era, start: Some(0) });
1250
1251		let max_total_nominators_per_validator =
1252			<T::ElectionProvider as ElectionProvider>::MaxBackersPerWinnerFinal::get();
1253		let exposed_nominators_per_validator = max_total_nominators_per_validator / validators;
1254
1255		let pages: WeakBoundedVec<_, _> = (0..crate::ClaimedRewardsBound::<T>::get())
1256			.collect::<Vec<_>>()
1257			.try_into()
1258			.unwrap();
1259
1260		// 33% slashed — realistic worst-case under BFT assumptions.
1261		let slashed_validators = validators / 3;
1262
1263		let mut reward_points_individual = BTreeMap::new();
1264		let mut total_incentive_weight = BalanceOf::<T>::zero();
1265
1266		for i in 0..validators {
1267			let validator = account::<T::AccountId>("validator", i, SEED);
1268
1269			// ValidatorPrefs
1270			ErasValidatorPrefs::<T>::insert(era, validator.clone(), ValidatorPrefs::default());
1271
1272			// ClaimedRewards
1273			ClaimedRewards::<T>::insert(era, validator.clone(), pages.clone());
1274
1275			// ErasStakersPaged + ErasStakersOverview
1276			let exposure = sp_staking::Exposure::<T::AccountId, BalanceOf<T>> {
1277				own: T::Currency::minimum_balance(),
1278				total: T::Currency::minimum_balance() *
1279					(exposed_nominators_per_validator + 1).into(),
1280				others: (0..exposed_nominators_per_validator)
1281					.map(|n| {
1282						let nominator = account::<T::AccountId>("nominator", n, SEED);
1283						IndividualExposure { who: nominator, value: T::Currency::minimum_balance() }
1284					})
1285					.collect::<Vec<_>>(),
1286			};
1287			Eras::<T>::upsert_exposure(era, &validator, exposure);
1288
1289			// ErasRewardPoints (individual)
1290			reward_points_individual.insert(validator.clone(), 7u32);
1291
1292			// ValidatorSlashInEra (first 33%)
1293			if i < slashed_validators {
1294				crate::ValidatorSlashInEra::<T>::insert(
1295					era,
1296					validator.clone(),
1297					(Perbill::from_percent(10), BalanceOf::<T>::max_value() / 10u32.into()),
1298				);
1299			}
1300
1301			// ErasValidatorIncentiveWeight
1302			let incentive_weight = BalanceOf::<T>::from(100u64);
1303			ErasValidatorIncentiveWeight::<T>::insert(era, validator, incentive_weight);
1304			total_incentive_weight += incentive_weight;
1305		}
1306
1307		// Single-entry storages
1308		ErasValidatorReward::<T>::insert(era, BalanceOf::<T>::max_value());
1309		ErasRewardPoints::<T>::insert(
1310			era,
1311			crate::EraRewardPoints::<T> {
1312				total: 77777,
1313				individual: reward_points_individual.try_into().unwrap(),
1314			},
1315		);
1316		ErasTotalStake::<T>::insert(era, BalanceOf::<T>::max_value());
1317		ErasNominatorsSlashable::<T>::insert(era, true);
1318		ErasSumValidatorIncentiveWeight::<T>::insert(era, total_incentive_weight);
1319		ErasValidatorIncentiveBudget::<T>::insert(era, BalanceOf::<T>::from(1_000_000u64));
1320
1321		era
1322	}
1323
1324	/// Validates that the weight consumption of a pruning operation stays within expected limits.
1325	fn validate_pruning_weight<T: Config>(
1326		result: &frame_support::dispatch::DispatchResultWithPostInfo,
1327		step_name: &str,
1328		validator_count: u32,
1329	) {
1330		assert!(
1331			result.is_ok(),
1332			"Benchmark {} should succeed with v={}",
1333			step_name,
1334			validator_count
1335		);
1336
1337		let post_info = result.unwrap();
1338		let actual_ref_time = post_info
1339			.actual_weight
1340			.expect(&format!(
1341				"Should report actual weight for {} with v={}",
1342				step_name, validator_count
1343			))
1344			.ref_time();
1345
1346		assert!(
1347			actual_ref_time > 0,
1348			"Should report non-zero ref_time for {} with v={}",
1349			step_name,
1350			validator_count
1351		);
1352		// No need to validate against MaxPruningItems since we use item-based limiting
1353	}
1354
1355	// Benchmark pruning ErasStakersPaged (first step)
1356	#[benchmark(pov_mode = Measured)]
1357	fn prune_era_stakers_paged(
1358		v: Linear<1, { T::MaxValidatorSet::get() }>,
1359	) -> Result<(), BenchmarkError> {
1360		let era = setup_era_for_pruning::<T>(v);
1361		EraPruningState::<T>::insert(era, PruningStep::ErasStakersPaged);
1362
1363		let caller: T::AccountId = whitelisted_caller();
1364
1365		let result;
1366		#[block]
1367		{
1368			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1369		}
1370
1371		validate_pruning_weight::<T>(&result, "ErasStakersPaged", v);
1372
1373		Ok(())
1374	}
1375
1376	// Benchmark pruning ErasStakersOverview (second step)
1377	#[benchmark(pov_mode = Measured)]
1378	fn prune_era_stakers_overview(
1379		v: Linear<1, { T::MaxValidatorSet::get() }>,
1380	) -> Result<(), BenchmarkError> {
1381		let era = setup_era_for_pruning::<T>(v);
1382		EraPruningState::<T>::insert(era, PruningStep::ErasStakersOverview);
1383
1384		let caller: T::AccountId = whitelisted_caller();
1385
1386		let result;
1387		#[block]
1388		{
1389			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1390		}
1391
1392		validate_pruning_weight::<T>(&result, "ErasStakersOverview", v);
1393
1394		Ok(())
1395	}
1396
1397	// Benchmark pruning ErasValidatorPrefs (third step)
1398	#[benchmark(pov_mode = Measured)]
1399	fn prune_era_validator_prefs(
1400		v: Linear<1, { T::MaxValidatorSet::get() }>,
1401	) -> Result<(), BenchmarkError> {
1402		let era = setup_era_for_pruning::<T>(v);
1403		EraPruningState::<T>::insert(era, PruningStep::ErasValidatorPrefs);
1404
1405		let caller: T::AccountId = whitelisted_caller();
1406
1407		let result;
1408		#[block]
1409		{
1410			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1411		}
1412
1413		validate_pruning_weight::<T>(&result, "ErasValidatorPrefs", v);
1414
1415		Ok(())
1416	}
1417
1418	// Benchmark pruning ClaimedRewards (fourth step)
1419	#[benchmark(pov_mode = Measured)]
1420	fn prune_era_claimed_rewards(
1421		v: Linear<1, { T::MaxValidatorSet::get() }>,
1422	) -> Result<(), BenchmarkError> {
1423		let era = setup_era_for_pruning::<T>(v);
1424		EraPruningState::<T>::insert(era, PruningStep::ClaimedRewards);
1425
1426		let caller: T::AccountId = whitelisted_caller();
1427
1428		let result;
1429		#[block]
1430		{
1431			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1432		}
1433
1434		validate_pruning_weight::<T>(&result, "ClaimedRewards", v);
1435
1436		Ok(())
1437	}
1438
1439	// Benchmark pruning ErasValidatorReward (fifth step)
1440	#[benchmark(pov_mode = Measured)]
1441	fn prune_era_validator_reward() -> Result<(), BenchmarkError> {
1442		let era = setup_era_for_pruning::<T>(1);
1443		EraPruningState::<T>::insert(era, PruningStep::ErasValidatorReward);
1444
1445		let caller: T::AccountId = whitelisted_caller();
1446
1447		let result;
1448		#[block]
1449		{
1450			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1451		}
1452
1453		validate_pruning_weight::<T>(&result, "ErasValidatorReward", 1);
1454
1455		Ok(())
1456	}
1457
1458	// Benchmark pruning ErasRewardPoints (sixth step)
1459	#[benchmark(pov_mode = Measured)]
1460	fn prune_era_reward_points() -> Result<(), BenchmarkError> {
1461		let era = setup_era_for_pruning::<T>(1);
1462		EraPruningState::<T>::insert(era, PruningStep::ErasRewardPoints);
1463
1464		let caller: T::AccountId = whitelisted_caller();
1465
1466		let result;
1467		#[block]
1468		{
1469			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1470		}
1471
1472		validate_pruning_weight::<T>(&result, "ErasRewardPoints", 1);
1473
1474		Ok(())
1475	}
1476
1477	// Benchmark pruning single-entry cleanups (seventh step)
1478	#[benchmark(pov_mode = Measured)]
1479	fn prune_era_single_entry_cleanups() -> Result<(), BenchmarkError> {
1480		let era = setup_era_for_pruning::<T>(1);
1481		EraPruningState::<T>::insert(era, PruningStep::SingleEntryCleanups);
1482
1483		let caller: T::AccountId = whitelisted_caller();
1484
1485		let result;
1486		#[block]
1487		{
1488			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1489		}
1490
1491		validate_pruning_weight::<T>(&result, "SingleEntryCleanups", 1);
1492
1493		Ok(())
1494	}
1495
1496	// Benchmark pruning ValidatorSlashInEra (eighth step)
1497	#[benchmark(pov_mode = Measured)]
1498	fn prune_era_validator_slash_in_era(
1499		v: Linear<1, { T::MaxValidatorSet::get() }>,
1500	) -> Result<(), BenchmarkError> {
1501		let era = setup_era_for_pruning::<T>(v);
1502		EraPruningState::<T>::insert(era, PruningStep::ValidatorSlashInEra);
1503
1504		let caller: T::AccountId = whitelisted_caller();
1505
1506		let result;
1507		#[block]
1508		{
1509			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1510		}
1511
1512		validate_pruning_weight::<T>(&result, "ValidatorSlashInEra", v);
1513
1514		Ok(())
1515	}
1516
1517	#[benchmark(pov_mode = Measured)]
1518	fn prune_era_validator_incentive_weight(
1519		v: Linear<1, { T::MaxValidatorSet::get() }>,
1520	) -> Result<(), BenchmarkError> {
1521		let era = setup_era_for_pruning::<T>(v);
1522		EraPruningState::<T>::insert(era, PruningStep::ErasValidatorIncentiveWeight);
1523
1524		let caller: T::AccountId = whitelisted_caller();
1525
1526		let result;
1527		#[block]
1528		{
1529			result = Pallet::<T>::prune_era_step(RawOrigin::Signed(caller).into(), era);
1530		}
1531
1532		validate_pruning_weight::<T>(&result, "ErasValidatorIncentiveWeight", v);
1533
1534		Ok(())
1535	}
1536
1537	impl_benchmark_test_suite!(
1538		Staking,
1539		crate::mock::ExtBuilder::default().has_stakers(true),
1540		crate::mock::Test,
1541		exec_name = build_and_execute
1542	);
1543}
1544
1545#[cfg(test)]
1546mod tests {
1547	use super::*;
1548	use crate::mock::{ExtBuilder, RuntimeOrigin, Staking, Test};
1549	use frame_support::assert_ok;
1550
1551	#[test]
1552	fn create_validators_with_nominators_for_era_works() {
1553		ExtBuilder::default().build_and_execute(|| {
1554			let v = 10;
1555			let n = 100;
1556
1557			create_validators_with_nominators_for_era::<Test>(
1558				v,
1559				n,
1560				MaxNominationsOf::<Test>::get() as usize,
1561				false,
1562				None,
1563			)
1564			.unwrap();
1565
1566			let count_validators = Validators::<Test>::iter().count();
1567			let count_nominators = Nominators::<Test>::iter().count();
1568
1569			assert_eq!(count_validators, Validators::<Test>::count() as usize);
1570			assert_eq!(count_nominators, Nominators::<Test>::count() as usize);
1571
1572			assert_eq!(count_validators, v as usize);
1573			assert_eq!(count_nominators, n as usize);
1574		});
1575	}
1576
1577	#[test]
1578	fn create_validator_with_nominators_works() {
1579		ExtBuilder::default().build_and_execute(|| {
1580			let n = 10;
1581
1582			let (validator_stash, nominators, current_era) =
1583				create_validator_with_nominators::<Test>(
1584					n,
1585					<<Test as Config>::MaxExposurePageSize as Get<_>>::get(),
1586					false,
1587					false,
1588					RewardDestination::Staked,
1589				)
1590				.unwrap();
1591
1592			assert_eq!(nominators.len() as u32, n);
1593
1594			let original_stakeable_balance = asset::stakeable_balance::<Test>(&validator_stash);
1595			assert_ok!(Staking::payout_stakers_by_page(
1596				RuntimeOrigin::signed(1337),
1597				validator_stash,
1598				current_era,
1599				0
1600			));
1601			let new_stakeable_balance = asset::stakeable_balance::<Test>(&validator_stash);
1602
1603			// reward increases stakeable balance
1604			assert!(original_stakeable_balance < new_stakeable_balance);
1605		});
1606	}
1607}