diff --git a/prdoc/pr_10822.prdoc b/prdoc/pr_10822.prdoc new file mode 100644 index 0000000000000..584ef07556fe7 --- /dev/null +++ b/prdoc/pr_10822.prdoc @@ -0,0 +1,8 @@ +title: 'staking-async: improve benchmarking' +doc: +- audience: Runtime Dev + description: |- + Avoid bulk deletion of validators and nominators when not needed. +crates: +- name: pallet-staking-async + bump: none diff --git a/substrate/frame/staking-async/src/benchmarking.rs b/substrate/frame/staking-async/src/benchmarking.rs index da0e14c554225..e432e605ce8df 100644 --- a/substrate/frame/staking-async/src/benchmarking.rs +++ b/substrate/frame/staking-async/src/benchmarking.rs @@ -29,104 +29,19 @@ pub use frame_benchmarking::{ }; use frame_election_provider_support::SortedListProvider; use frame_support::{ - assert_ok, - pallet_prelude::*, - storage::bounded_vec::BoundedVec, - traits::{fungible::Inspect, TryCollect}, + assert_ok, pallet_prelude::*, storage::bounded_vec::BoundedVec, traits::fungible::Inspect, }; use frame_system::RawOrigin; use pallet_staking_async_rc_client as rc_client; use sp_runtime::{ traits::{Bounded, One, StaticLookup, Zero}, - Perbill, Percent, Saturating, + Perbill, Percent, }; use sp_staking::currency_to_vote::CurrencyToVote; use testing_utils::*; const SEED: u32 = 0; -// This function clears all existing validators and nominators from the set, and generates one new -// validator being nominated by n nominators, and returns the validator stash account and the -// nominators' stash and controller. It also starts plans a new era with this new stakers, and -// returns the planned era index. -pub(crate) fn create_validator_with_nominators( - n: u32, - upper_bound: u32, - dead_controller: bool, - unique_controller: bool, - destination: RewardDestination, -) -> Result<(T::AccountId, Vec<(T::AccountId, T::AccountId)>, EraIndex), &'static str> { - // TODO: this can be replaced with `testing_utils` version? - // Clean up any existing state. - clear_validators_and_nominators::(); - let mut points_total = 0; - let mut points_individual = Vec::new(); - - let (v_stash, v_controller) = if unique_controller { - create_unique_stash_controller::(0, 100, destination.clone(), false)? - } else { - create_stash_controller::(0, 100, destination.clone())? - }; - - let validator_prefs = - ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() }; - Staking::::validate(RawOrigin::Signed(v_controller).into(), validator_prefs)?; - let stash_lookup = T::Lookup::unlookup(v_stash.clone()); - - points_total += 10; - points_individual.push((v_stash.clone(), 10)); - - let original_nominator_count = Nominators::::count(); - let mut nominators = Vec::new(); - - // Give the validator n nominators, but keep total users in the system the same. - for i in 0..upper_bound { - let (n_stash, n_controller) = if !dead_controller { - create_stash_controller::(u32::MAX - i, 100, destination.clone())? - } else { - create_unique_stash_controller::(u32::MAX - i, 100, destination.clone(), true)? - }; - if i < n { - Staking::::nominate( - RawOrigin::Signed(n_controller.clone()).into(), - vec![stash_lookup.clone()], - )?; - nominators.push((n_stash, n_controller)); - } - } - - ValidatorCount::::put(1); - - // Start a new Era - let new_validators = Rotator::::legacy_insta_plan_era(); - let planned_era = CurrentEra::::get().unwrap_or_default(); - - assert_eq!(new_validators.len(), 1, "New validators is not 1"); - assert_eq!(new_validators[0], v_stash, "Our validator was not selected"); - assert_ne!(Validators::::count(), 0, "New validators count wrong"); - assert_eq!( - Nominators::::count(), - original_nominator_count + nominators.len() as u32, - "New nominators count wrong" - ); - - // Give Era Points - let reward = EraRewardPoints:: { - total: points_total, - individual: points_individual.into_iter().try_collect()?, - }; - - ErasRewardPoints::::insert(planned_era, reward); - - // Create reward pool - let total_payout = asset::existential_deposit::() - .saturating_mul(upper_bound.into()) - .saturating_mul(1000u32.into()); - >::insert(planned_era, total_payout); - - Ok((v_stash, nominators, planned_era)) -} - struct ListScenario { /// Stash that is expected to be moved. origin_stash1: T::AccountId, @@ -240,9 +155,6 @@ mod benchmarks { #[benchmark] fn bond_extra() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup the worst case list scenario. @@ -278,9 +190,6 @@ mod benchmarks { #[benchmark] fn unbond() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - // the weight the nominator will start at. The value used here is expected to be // significantly higher than the first position in a list (e.g. the first bag threshold). let origin_weight = BalanceOf::::try_from(952_994_955_240_703u128) @@ -329,9 +238,6 @@ mod benchmarks { #[benchmark] // Worst case scenario, everything is removed after the bonding duration fn withdraw_unbonded_kill() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note that we don't care about the setup of the @@ -453,9 +359,6 @@ mod benchmarks { #[benchmark] // Worst case scenario, T::MaxNominations::get() fn nominate(n: Linear<1, { MaxNominationsOf::::get() }>) -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note we don't care about the destination position, @@ -486,9 +389,6 @@ mod benchmarks { #[benchmark] fn chill() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note that we don't care about the setup of the @@ -630,9 +530,6 @@ mod benchmarks { #[benchmark] fn force_unstake() -> Result<(), BenchmarkError> { - // Clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note that we don't care about the setup of the @@ -736,9 +633,6 @@ mod benchmarks { #[benchmark] fn rebond(l: Linear<1, { T::MaxUnlockingChunks::get() as u32 }>) -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Pallet::::min_nominator_bond() // we use 100 to play friendly with the list threshold values in the mock .max(100u32.into()); @@ -782,9 +676,6 @@ mod benchmarks { #[benchmark] fn reap_stash() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note that we don't care about the setup of the @@ -863,9 +754,6 @@ mod benchmarks { #[benchmark] fn chill_other() -> Result<(), BenchmarkError> { - // clean up any existing state. - clear_validators_and_nominators::(); - let origin_weight = Staking::::min_nominator_bond(); // setup a worst case list scenario. Note that we don't care about the setup of the @@ -898,11 +786,16 @@ mod benchmarks { #[benchmark] fn force_apply_min_commission() -> Result<(), BenchmarkError> { - // Clean up any existing state - clear_validators_and_nominators::(); - - // Create a validator with a commission of 50% - let (stash, controller) = create_stash_controller::(1, 1, RewardDestination::Staked)?; + // Use existing validator or create new one - no clearing needed + let (stash, controller) = if let Some(stash) = Validators::::iter_keys().next() { + let controller = Bonded::::get(&stash).ok_or( + "validator not + bonded", + )?; + (stash, controller) + } else { + create_stash_controller::(1, 1, RewardDestination::Staked)? + }; let validator_prefs = ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() }; Staking::::validate(RawOrigin::Signed(controller).into(), validator_prefs)?; diff --git a/substrate/frame/staking-async/src/testing_utils.rs b/substrate/frame/staking-async/src/testing_utils.rs index 3028b3a4e55aa..9be2f94823cd1 100644 --- a/substrate/frame/staking-async/src/testing_utils.rs +++ b/substrate/frame/staking-async/src/testing_utils.rs @@ -20,17 +20,23 @@ use crate::{Pallet as Staking, *}; use frame_benchmarking::account; +use frame_election_provider_support::SortedListProvider; +use frame_support::pallet_prelude::*; use frame_system::RawOrigin; use rand_chacha::{ rand_core::{RngCore, SeedableRng}, ChaChaRng, }; use sp_io::hashing::blake2_256; - -use frame_election_provider_support::SortedListProvider; -use frame_support::pallet_prelude::*; use sp_runtime::{traits::StaticLookup, Perbill}; +#[cfg(feature = "runtime-benchmarks")] +use crate::session_rotation::{Eras, Rotator}; +#[cfg(feature = "runtime-benchmarks")] +use frame_support::traits::TryCollect; +#[cfg(feature = "runtime-benchmarks")] +use sp_runtime::traits::Zero; + const SEED: u32 = 0; /// This function removes all validators and nominators from storage. @@ -237,6 +243,137 @@ pub fn create_validators_with_nominators_for_era( Ok(validator_chosen) } +/// This function clears all existing validators and nominators from the set, and generates one new +/// validator being nominated by n nominators, and returns the validator stash account and the +/// nominators' stash and controller. It also starts plans a new era with this new stakers, and +/// returns the planned era index. +#[cfg(feature = "runtime-benchmarks")] +pub fn create_validator_with_nominators( + n: u32, + upper_bound: u32, + dead_controller: bool, + unique_controller: bool, + destination: RewardDestination, +) -> Result<(T::AccountId, Vec<(T::AccountId, T::AccountId)>, EraIndex), &'static str> { + // For payout to work, we need an era that has ended (< ActiveEra). + // Check if there's a claimable era with valid exposure. + let active_era = ActiveEra::::get().map(|e| e.index).unwrap_or(0); + + // Try to find a claimable era (active_era - 1 if it exists) + if active_era > 0 { + let claimable_era = active_era - 1; + + // Try to find an existing validator with sufficient exposure in the claimable era + // Must have actual stake (not just an empty default exposure) + let existing_validator = Validators::::iter_keys().find(|v| { + let exposure = Eras::::get_full_exposure(claimable_era, v); + !exposure.total.is_zero() && exposure.others.len() >= n as usize + }); + + if let Some(v_stash) = existing_validator { + // Use existing validator and its nominators + let exposure = Eras::::get_full_exposure(claimable_era, &v_stash); + let nominators: Vec<(T::AccountId, T::AccountId)> = exposure + .others + .iter() + .take(n as usize) + .map(|ind| { + let controller = Bonded::::get(&ind.who).unwrap_or_else(|| ind.who.clone()); + (ind.who.clone(), controller) + }) + .collect(); + + // Set up era points if not already present + if ErasRewardPoints::::get(claimable_era).total == 0 { + let reward = EraRewardPoints:: { + total: 10, + individual: vec![(v_stash.clone(), 10)].into_iter().try_collect()?, + }; + ErasRewardPoints::::insert(claimable_era, reward); + } + + // Set up validator reward if not already present + if ErasValidatorReward::::get(claimable_era).is_none() { + let total_payout = asset::existential_deposit::() + .saturating_mul(upper_bound.into()) + .saturating_mul(1000u32.into()); + >::insert(claimable_era, total_payout); + } + + return Ok((v_stash, nominators, claimable_era)); + } + } + + // Fall back to clearing and creating fresh state + clear_validators_and_nominators::(); + let mut points_total = 0; + let mut points_individual = Vec::new(); + + let (v_stash, v_controller) = if unique_controller { + create_unique_stash_controller::(0, 100, destination.clone(), false)? + } else { + create_stash_controller::(0, 100, destination.clone())? + }; + + let validator_prefs = + ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() }; + Staking::::validate(RawOrigin::Signed(v_controller).into(), validator_prefs)?; + let stash_lookup = T::Lookup::unlookup(v_stash.clone()); + + points_total += 10; + points_individual.push((v_stash.clone(), 10)); + + let original_nominator_count = Nominators::::count(); + let mut nominators = Vec::new(); + + // Give the validator n nominators, but keep total users in the system the same. + for i in 0..upper_bound { + let (n_stash, n_controller) = if !dead_controller { + create_stash_controller::(u32::MAX - i, 100, destination.clone())? + } else { + create_unique_stash_controller::(u32::MAX - i, 100, destination.clone(), true)? + }; + if i < n { + Staking::::nominate( + RawOrigin::Signed(n_controller.clone()).into(), + vec![stash_lookup.clone()], + )?; + nominators.push((n_stash, n_controller)); + } + } + + ValidatorCount::::put(1); + + // Start a new Era + let new_validators = Rotator::::legacy_insta_plan_era(); + let new_planned_era = CurrentEra::::get().unwrap_or_default(); + + assert_eq!(new_validators.len(), 1, "New validators is not 1"); + assert_eq!(new_validators[0], v_stash, "Our validator was not selected"); + assert_ne!(Validators::::count(), 0, "New validators count wrong"); + assert_eq!( + Nominators::::count(), + original_nominator_count + nominators.len() as u32, + "New nominators count wrong" + ); + + // Give Era Points + let reward = EraRewardPoints:: { + total: points_total, + individual: points_individual.into_iter().try_collect()?, + }; + + ErasRewardPoints::::insert(new_planned_era, reward); + + // Create reward pool + let total_payout = asset::existential_deposit::() + .saturating_mul(upper_bound.into()) + .saturating_mul(1000u32.into()); + >::insert(new_planned_era, total_payout); + + Ok((v_stash, nominators, new_planned_era)) +} + /// get the current era. pub fn current_era() -> EraIndex { CurrentEra::::get().unwrap_or(0)