// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Staking pallet benchmarking. use super::*; use crate::{asset, ConfigOp, Pallet as Staking}; use testing_utils::*; use codec::Decode; use pezframe_election_provider_support::{bounds::DataProviderBounds, SortedListProvider}; use pezframe_support::{ pezpallet_prelude::*, storage::bounded_vec::BoundedVec, traits::{Get, Imbalance, UnfilteredDispatchable}, }; use pezsp_runtime::{ traits::{Bounded, One, StaticLookup, TrailingZeroInput, Zero}, Perbill, Percent, Saturating, }; use pezsp_staking::{currency_to_vote::CurrencyToVote, SessionIndex}; pub use pezframe_benchmarking::{ impl_benchmark_test_suite, v2::*, whitelist_account, whitelisted_caller, BenchmarkError, }; use pezframe_system::RawOrigin; const SEED: u32 = 0; const MAX_SPANS: u32 = 100; const MAX_SLASHES: u32 = 1000; type MaxValidators = <::BenchmarkingConfig as BenchmarkingConfig>::MaxValidators; type MaxNominators = <::BenchmarkingConfig as BenchmarkingConfig>::MaxNominators; // Add slashing spans to a user account. Not relevant for actual use, only to benchmark // read and write operations. pub fn add_slashing_spans(who: &T::AccountId, spans: u32) { if spans == 0 { return; } // For the first slashing span, we initialize let mut slashing_spans = crate::slashing::SlashingSpans::new(0); SpanSlash::::insert((who, 0), crate::slashing::SpanRecord::default()); for i in 1..spans { assert!(slashing_spans.end_span(i)); SpanSlash::::insert((who, i), crate::slashing::SpanRecord::default()); } SlashingSpans::::insert(who, slashing_spans); } // 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 an era and creates pending payouts. 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)>), &'static str> { // 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 = Staking::::try_trigger_new_era(SessionIndex::one(), true).unwrap(); assert_eq!(new_validators.len(), 1); assert_eq!(new_validators[0], v_stash, "Our validator was not selected!"); assert_ne!(Validators::::count(), 0); assert_eq!(Nominators::::count(), original_nominator_count + nominators.len() as u32); // Give Era Points let reward = EraRewardPoints:: { total: points_total, individual: points_individual.into_iter().collect(), }; let current_era = CurrentEra::::get().unwrap(); ErasRewardPoints::::insert(current_era, reward); // Create reward pool let total_payout = asset::existential_deposit::() .saturating_mul(upper_bound.into()) .saturating_mul(1000u32.into()); >::insert(current_era, total_payout); Ok((v_stash, nominators)) } struct ListScenario { /// Stash that is expected to be moved. origin_stash1: T::AccountId, /// Controller of the Stash that is expected to be moved. origin_controller1: T::AccountId, dest_weight: BalanceOf, } impl ListScenario { /// An expensive scenario for bags-list implementation: /// /// - the node to be updated (r) is the head of a bag that has at least one other node. The bag /// itself will need to be read and written to update its head. The node pointed to by r.next /// will need to be read and written as it will need to have its prev pointer updated. Note /// that there are two other worst case scenarios for bag removal: 1) the node is a tail and /// 2) the node is a middle node with prev and next; all scenarios end up with the same number /// of storage reads and writes. /// /// - the destination bag has at least one node, which will need its next pointer updated. /// /// NOTE: while this scenario specifically targets a worst case for the bags-list, it should /// also elicit a worst case for other known `VoterList` implementations; although /// this may not be true against unknown `VoterList` implementations. fn new(origin_weight: BalanceOf, is_increase: bool) -> Result { ensure!(!origin_weight.is_zero(), "origin weight must be greater than 0"); // burn the entire issuance. let i = asset::burn::(asset::total_issuance::()); core::mem::forget(i); // create accounts with the origin weight let (origin_stash1, origin_controller1) = create_stash_controller_with_balance::( USER_SEED + 2, origin_weight, RewardDestination::Staked, )?; Staking::::nominate( RawOrigin::Signed(origin_controller1.clone()).into(), // NOTE: these don't really need to be validators. vec![T::Lookup::unlookup(account("random_validator", 0, SEED))], )?; let (_origin_stash2, origin_controller2) = create_stash_controller_with_balance::( USER_SEED + 3, origin_weight, RewardDestination::Staked, )?; Staking::::nominate( RawOrigin::Signed(origin_controller2).into(), vec![T::Lookup::unlookup(account("random_validator", 0, SEED))], )?; // find a destination weight that will trigger the worst case scenario let dest_weight_as_vote = T::VoterList::score_update_worst_case(&origin_stash1, is_increase); let total_issuance = asset::total_issuance::(); let dest_weight = T::CurrencyToVote::to_currency(dest_weight_as_vote as u128, total_issuance); // create an account with the worst case destination weight let (_dest_stash1, dest_controller1) = create_stash_controller_with_balance::( USER_SEED + 1, dest_weight, RewardDestination::Staked, )?; Staking::::nominate( RawOrigin::Signed(dest_controller1).into(), vec![T::Lookup::unlookup(account("random_validator", 0, SEED))], )?; Ok(ListScenario { origin_stash1, origin_controller1, dest_weight }) } } const USER_SEED: u32 = 999666; #[benchmarks] mod benchmarks { use super::*; #[benchmark] fn bond() { let stash = create_funded_user::("stash", USER_SEED, 100); let reward_destination = RewardDestination::Staked; let amount = asset::existential_deposit::() * 10u32.into(); whitelist_account!(stash); #[extrinsic_call] _(RawOrigin::Signed(stash.clone()), amount, reward_destination); assert!(Bonded::::contains_key(stash.clone())); assert!(Ledger::::contains_key(stash)); } #[benchmark] fn bond_extra() -> Result<(), BenchmarkError> { // clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup the worst case list scenario. // the weight the nominator will start at. let scenario = ListScenario::::new(origin_weight, true)?; let max_additional = scenario.dest_weight - origin_weight; let stash = scenario.origin_stash1.clone(); let controller = scenario.origin_controller1; let original_bonded: BalanceOf = Ledger::::get(&controller) .map(|l| l.active) .ok_or("ledger not created after")?; let _ = asset::mint_into_existing::( &stash, max_additional + asset::existential_deposit::(), ) .unwrap(); whitelist_account!(stash); #[extrinsic_call] _(RawOrigin::Signed(stash), max_additional); let ledger = Ledger::::get(&controller).ok_or("ledger not created after")?; let new_bonded: BalanceOf = ledger.active; assert!(original_bonded < new_bonded); Ok(()) } #[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) .map_err(|_| "balance expected to be a u128") .unwrap(); let scenario = ListScenario::::new(origin_weight, false)?; let controller = scenario.origin_controller1.clone(); let amount = origin_weight - scenario.dest_weight; let ledger = Ledger::::get(&controller).ok_or("ledger not created before")?; let original_bonded: BalanceOf = ledger.active; whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller.clone()), amount); let ledger = Ledger::::get(&controller).ok_or("ledger not created after")?; let new_bonded: BalanceOf = ledger.active; assert!(original_bonded > new_bonded); Ok(()) } #[benchmark] // Withdraw only updates the ledger fn withdraw_unbonded_update( // Slashing Spans s: Linear<0, MAX_SPANS>, ) -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::(0, 100, RewardDestination::Staked)?; add_slashing_spans::(&stash, s); let amount = asset::existential_deposit::() * 5u32.into(); // Half of total Staking::::unbond(RawOrigin::Signed(controller.clone()).into(), amount)?; CurrentEra::::put(EraIndex::max_value()); let ledger = Ledger::::get(&controller).ok_or("ledger not created before")?; let original_total: BalanceOf = ledger.total; whitelist_account!(controller); #[extrinsic_call] withdraw_unbonded(RawOrigin::Signed(controller.clone()), s); let ledger = Ledger::::get(&controller).ok_or("ledger not created after")?; let new_total: BalanceOf = ledger.total; assert!(original_total > new_total); Ok(()) } #[benchmark] // Worst case scenario, everything is removed after the bonding duration fn withdraw_unbonded_kill( // Slashing Spans s: Linear<0, MAX_SPANS>, ) -> Result<(), BenchmarkError> { // clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note that we don't care about the setup of the // destination position because we are doing a removal from the list but no insert. let scenario = ListScenario::::new(origin_weight, true)?; let controller = scenario.origin_controller1.clone(); let stash = scenario.origin_stash1; add_slashing_spans::(&stash, s); assert!(T::VoterList::contains(&stash)); let ed = asset::existential_deposit::(); let mut ledger = Ledger::::get(&controller).unwrap(); ledger.active = ed - One::one(); Ledger::::insert(&controller, ledger); CurrentEra::::put(EraIndex::max_value()); whitelist_account!(controller); #[extrinsic_call] withdraw_unbonded(RawOrigin::Signed(controller.clone()), s); assert!(!Ledger::::contains_key(controller)); assert!(!T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn validate() -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::( MaxNominationsOf::::get() - 1, 100, RewardDestination::Staked, )?; // because it is chilled. assert!(!T::VoterList::contains(&stash)); let prefs = ValidatorPrefs::default(); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller), prefs); assert!(Validators::::contains_key(&stash)); assert!(T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn kick( // scenario: we want to kick `k` nominators from nominating us (we are a validator). // we'll assume that `k` is under 128 for the purposes of determining the slope. // each nominator should have `T::MaxNominations::get()` validators nominated, and our // validator should be somewhere in there. k: Linear<1, 128>, ) -> Result<(), BenchmarkError> { // these are the other validators; there are `T::MaxNominations::get() - 1` of them, so // there are a total of `T::MaxNominations::get()` validators in the system. let rest_of_validators = create_validators_with_seed::(MaxNominationsOf::::get() - 1, 100, 415)?; // this is the validator that will be kicking. let (stash, controller) = create_stash_controller::( MaxNominationsOf::::get() - 1, 100, RewardDestination::Staked, )?; let stash_lookup = T::Lookup::unlookup(stash.clone()); // they start validating. Staking::::validate(RawOrigin::Signed(controller.clone()).into(), Default::default())?; // we now create the nominators. there will be `k` of them; each will nominate all // validators. we will then kick each of the `k` nominators from the main validator. let mut nominator_stashes = Vec::with_capacity(k as usize); for i in 0..k { // create a nominator stash. let (n_stash, n_controller) = create_stash_controller::( MaxNominationsOf::::get() + i, 100, RewardDestination::Staked, )?; // bake the nominations; we first clone them from the rest of the validators. let mut nominations = rest_of_validators.clone(); // then insert "our" validator somewhere in there (we vary it) to avoid accidental // optimisations/pessimisations. nominations.insert(i as usize % (nominations.len() + 1), stash_lookup.clone()); // then we nominate. Staking::::nominate(RawOrigin::Signed(n_controller.clone()).into(), nominations)?; nominator_stashes.push(n_stash); } // all nominators now should be nominating our validator... for n in nominator_stashes.iter() { assert!(Nominators::::get(n).unwrap().targets.contains(&stash)); } // we need the unlookuped version of the nominator stash for the kick. let kicks = nominator_stashes .iter() .map(|n| T::Lookup::unlookup(n.clone())) .collect::>(); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller), kicks); // all nominators now should *not* be nominating our validator... for n in nominator_stashes.iter() { assert!(!Nominators::::get(n).unwrap().targets.contains(&stash)); } Ok(()) } #[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 = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note we don't care about the destination position, // because we are just doing an insert into the origin position. ListScenario::::new(origin_weight, true)?; let (stash, controller) = create_stash_controller_with_balance::( SEED + MaxNominationsOf::::get() + 1, /* make sure the account does not conflict * with others */ origin_weight, RewardDestination::Staked, ) .unwrap(); assert!(!Nominators::::contains_key(&stash)); assert!(!T::VoterList::contains(&stash)); let validators = create_validators::(n, 100).unwrap(); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller), validators); assert!(Nominators::::contains_key(&stash)); assert!(T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn chill() -> Result<(), BenchmarkError> { // clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note that we don't care about the setup of the // destination position because we are doing a removal from the list but no insert. let scenario = ListScenario::::new(origin_weight, true)?; let controller = scenario.origin_controller1.clone(); let stash = scenario.origin_stash1; assert!(T::VoterList::contains(&stash)); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller)); assert!(!T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn set_payee() -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::(USER_SEED, 100, RewardDestination::Staked)?; assert_eq!(Payee::::get(&stash), Some(RewardDestination::Staked)); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller.clone()), RewardDestination::Account(controller.clone())); assert_eq!(Payee::::get(&stash), Some(RewardDestination::Account(controller))); Ok(()) } #[benchmark] fn update_payee() -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::(USER_SEED, 100, RewardDestination::Staked)?; Payee::::insert(&stash, { #[allow(deprecated)] RewardDestination::Controller }); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller.clone()), controller.clone()); assert_eq!(Payee::::get(&stash), Some(RewardDestination::Account(controller))); Ok(()) } #[benchmark] fn set_controller() -> Result<(), BenchmarkError> { let (stash, ctlr) = create_unique_stash_controller::(9000, 100, RewardDestination::Staked, false)?; // ensure `ctlr` is the currently stored controller. assert!(!Ledger::::contains_key(&stash)); assert!(Ledger::::contains_key(&ctlr)); assert_eq!(Bonded::::get(&stash), Some(ctlr.clone())); whitelist_account!(stash); #[extrinsic_call] _(RawOrigin::Signed(stash.clone())); assert!(Ledger::::contains_key(&stash)); Ok(()) } #[benchmark] fn set_validator_count() { let validator_count = MaxValidators::::get(); #[extrinsic_call] _(RawOrigin::Root, validator_count); assert_eq!(ValidatorCount::::get(), validator_count); } #[benchmark] fn force_no_eras() { #[extrinsic_call] _(RawOrigin::Root); assert_eq!(ForceEra::::get(), Forcing::ForceNone); } #[benchmark] fn force_new_era() { #[extrinsic_call] _(RawOrigin::Root); assert_eq!(ForceEra::::get(), Forcing::ForceNew); } #[benchmark] fn force_new_era_always() { #[extrinsic_call] _(RawOrigin::Root); assert_eq!(ForceEra::::get(), Forcing::ForceAlways); } #[benchmark] // Worst case scenario, the list of invulnerables is very long. fn set_invulnerables(v: Linear<0, { MaxValidators::::get() }>) { let mut invulnerables = Vec::new(); for i in 0..v { invulnerables.push(account("invulnerable", i, SEED)); } #[extrinsic_call] _(RawOrigin::Root, invulnerables); assert_eq!(Invulnerables::::get().len(), v as usize); } #[benchmark] fn deprecate_controller_batch( // We pass a dynamic number of controllers to the benchmark, up to // `MaxControllersInDeprecationBatch`. u: Linear<0, { T::MaxControllersInDeprecationBatch::get() }>, ) -> Result<(), BenchmarkError> { let mut controllers: Vec<_> = vec![]; let mut stashes: Vec<_> = vec![]; for i in 0..u as u32 { let (stash, controller) = create_unique_stash_controller::(i, 100, RewardDestination::Staked, false)?; controllers.push(controller); stashes.push(stash); } let bounded_controllers: BoundedVec<_, T::MaxControllersInDeprecationBatch> = BoundedVec::try_from(controllers.clone()).unwrap(); #[extrinsic_call] _(RawOrigin::Root, bounded_controllers); for i in 0..u as u32 { let stash = &stashes[i as usize]; let controller = &controllers[i as usize]; // Ledger no longer keyed by controller. assert_eq!(Ledger::::get(controller), None); // Bonded now maps to the stash. assert_eq!(Bonded::::get(stash), Some(stash.clone())); // Ledger is now keyed by stash. assert_eq!(Ledger::::get(stash).unwrap().stash, *stash); } Ok(()) } #[benchmark] fn force_unstake( // Slashing Spans s: Linear<0, MAX_SPANS>, ) -> Result<(), BenchmarkError> { // Clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note that we don't care about the setup of the // destination position because we are doing a removal from the list but no insert. let scenario = ListScenario::::new(origin_weight, true)?; let controller = scenario.origin_controller1.clone(); let stash = scenario.origin_stash1; assert!(T::VoterList::contains(&stash)); add_slashing_spans::(&stash, s); #[extrinsic_call] _(RawOrigin::Root, stash.clone(), s); assert!(!Ledger::::contains_key(&controller)); assert!(!T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn cancel_deferred_slash(s: Linear<1, MAX_SLASHES>) { let mut unapplied_slashes = Vec::new(); let era = EraIndex::one(); let dummy = || T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap(); for _ in 0..MAX_SLASHES { unapplied_slashes .push(UnappliedSlash::>::default_from(dummy())); } UnappliedSlashes::::insert(era, &unapplied_slashes); let slash_indices: Vec = (0..s).collect(); #[extrinsic_call] _(RawOrigin::Root, era, slash_indices); assert_eq!(UnappliedSlashes::::get(&era).len(), (MAX_SLASHES - s) as usize); } #[benchmark] fn payout_stakers_alive_staked( n: Linear<0, { T::MaxExposurePageSize::get() as u32 }>, ) -> Result<(), BenchmarkError> { let (validator, nominators) = create_validator_with_nominators::( n, T::MaxExposurePageSize::get() as u32, false, true, RewardDestination::Staked, )?; let current_era = CurrentEra::::get().unwrap(); // set the commission for this particular era as well. >::insert( current_era, validator.clone(), Validators::::get(&validator), ); let caller = whitelisted_caller(); let balance_before = asset::stakeable_balance::(&validator); let mut nominator_balances_before = Vec::new(); for (stash, _) in &nominators { let balance = asset::stakeable_balance::(stash); nominator_balances_before.push(balance); } #[extrinsic_call] payout_stakers(RawOrigin::Signed(caller), validator.clone(), current_era); let balance_after = asset::stakeable_balance::(&validator); ensure!( balance_before < balance_after, "Balance of validator stash should have increased after payout.", ); for ((stash, _), balance_before) in nominators.iter().zip(nominator_balances_before.iter()) { let balance_after = asset::stakeable_balance::(stash); ensure!( balance_before < &balance_after, "Balance of nominator stash should have increased after payout.", ); } Ok(()) } #[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 = MinNominatorBond::::get() .max(asset::existential_deposit::()) // we use 100 to play friendly with the list threshold values in the mock .max(100u32.into()); // setup a worst case list scenario. let scenario = ListScenario::::new(origin_weight, true)?; let dest_weight = scenario.dest_weight; // rebond an amount that will give the user dest_weight let rebond_amount = dest_weight - origin_weight; // spread that amount to rebond across `l` unlocking chunks, let value = rebond_amount / l.into(); // if `value` is zero, we need a greater delta between dest <=> origin weight assert_ne!(value, Zero::zero()); // so the sum of unlocking chunks puts voter into the dest bag. assert!(value * l.into() + origin_weight > origin_weight); assert!(value * l.into() + origin_weight <= dest_weight); let unlock_chunk = UnlockChunk::> { value, era: EraIndex::zero() }; let controller = scenario.origin_controller1; let mut staking_ledger = Ledger::::get(controller.clone()).unwrap(); for _ in 0..l { staking_ledger.unlocking.try_push(unlock_chunk.clone()).unwrap() } Ledger::::insert(controller.clone(), staking_ledger.clone()); let original_bonded: BalanceOf = staking_ledger.active; whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller.clone()), rebond_amount); let ledger = Ledger::::get(&controller).ok_or("ledger not created after")?; let new_bonded: BalanceOf = ledger.active; assert!(original_bonded < new_bonded); Ok(()) } #[benchmark] fn reap_stash(s: Linear<1, MAX_SPANS>) -> Result<(), BenchmarkError> { // clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note that we don't care about the setup of the // destination position because we are doing a removal from the list but no insert. let scenario = ListScenario::::new(origin_weight, true)?; let controller = scenario.origin_controller1.clone(); let stash = scenario.origin_stash1; add_slashing_spans::(&stash, s); let l = StakingLedger::::new(stash.clone(), asset::existential_deposit::() - One::one()); Ledger::::insert(&controller, l); assert!(Bonded::::contains_key(&stash)); assert!(T::VoterList::contains(&stash)); whitelist_account!(controller); #[extrinsic_call] _(RawOrigin::Signed(controller), stash.clone(), s); assert!(!Bonded::::contains_key(&stash)); assert!(!T::VoterList::contains(&stash)); Ok(()) } #[benchmark] fn new_era(v: Linear<1, 10>, n: Linear<0, 100>) -> Result<(), BenchmarkError> { create_validators_with_nominators_for_era::( v, n, MaxNominationsOf::::get() as usize, false, None, )?; let session_index = SessionIndex::one(); let validators; #[block] { validators = Staking::::try_trigger_new_era(session_index, true).ok_or("`new_era` failed")?; } assert!(validators.len() == v as usize); Ok(()) } #[benchmark(extra)] fn payout_all(v: Linear<1, 10>, n: Linear<0, 100>) -> Result<(), BenchmarkError> { create_validators_with_nominators_for_era::( v, n, MaxNominationsOf::::get() as usize, false, None, )?; // Start a new Era let new_validators = Staking::::try_trigger_new_era(SessionIndex::one(), true).unwrap(); assert!(new_validators.len() == v as usize); let current_era = CurrentEra::::get().unwrap(); let mut points_total = 0; let mut points_individual = Vec::new(); let mut payout_calls_arg = Vec::new(); for validator in new_validators.iter() { points_total += 10; points_individual.push((validator.clone(), 10)); payout_calls_arg.push((validator.clone(), current_era)); } // Give Era Points let reward = EraRewardPoints:: { total: points_total, individual: points_individual.into_iter().collect(), }; ErasRewardPoints::::insert(current_era, reward); // Create reward pool let total_payout = asset::existential_deposit::() * 1000u32.into(); >::insert(current_era, total_payout); let caller: T::AccountId = whitelisted_caller(); let origin = RawOrigin::Signed(caller); let calls: Vec<_> = payout_calls_arg .iter() .map(|arg| { Call::::payout_stakers_by_page { validator_stash: arg.0.clone(), era: arg.1, page: 0, } .encode() }) .collect(); #[block] { for call in calls { as Decode>::decode(&mut &*call) .expect("call is encoded above, encoding must be correct") .dispatch_bypass_filter(origin.clone().into())?; } } Ok(()) } #[benchmark(extra)] fn do_slash( l: Linear<1, { T::MaxUnlockingChunks::get() as u32 }>, ) -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::(0, 100, RewardDestination::Staked)?; let mut staking_ledger = Ledger::::get(controller.clone()).unwrap(); let unlock_chunk = UnlockChunk::> { value: 1u32.into(), era: EraIndex::zero() }; for _ in 0..l { staking_ledger.unlocking.try_push(unlock_chunk.clone()).unwrap(); } Ledger::::insert(controller, staking_ledger); let slash_amount = asset::existential_deposit::() * 10u32.into(); let balance_before = asset::stakeable_balance::(&stash); #[block] { crate::slashing::do_slash::( &stash, slash_amount, &mut BalanceOf::::zero(), &mut NegativeImbalanceOf::::zero(), EraIndex::zero(), ); } let balance_after = asset::stakeable_balance::(&stash); assert!(balance_before > balance_after); Ok(()) } #[benchmark] fn get_npos_voters( // number of validator intention. we will iterate all of them. v: Linear<{ MaxValidators::::get() / 2 }, { MaxValidators::::get() }>, // number of nominator intention. we will iterate all of them. n: Linear<{ MaxNominators::::get() / 2 }, { MaxNominators::::get() }>, ) -> Result<(), BenchmarkError> { create_validators_with_nominators_for_era::( v, n, MaxNominationsOf::::get() as usize, false, None, )?; assert_eq!(Validators::::count(), v); assert_eq!(Nominators::::count(), n); let num_voters = (v + n) as usize; // default bounds are unbounded. let voters; #[block] { voters = >::get_npos_voters(DataProviderBounds::default()); } assert_eq!(voters.len(), num_voters); Ok(()) } #[benchmark] fn get_npos_targets( // number of validator intention. v: Linear<{ MaxValidators::::get() / 2 }, { MaxValidators::::get() }>, ) -> Result<(), BenchmarkError> { // number of nominator intention. let n = MaxNominators::::get(); create_validators_with_nominators_for_era::( v, n, MaxNominationsOf::::get() as usize, false, None, )?; let targets; #[block] { // default bounds are unbounded. targets = >::get_npos_targets(DataProviderBounds::default()); } assert_eq!(targets.len() as u32, v); Ok(()) } #[benchmark] fn set_staking_configs_all_set() { #[extrinsic_call] set_staking_configs( RawOrigin::Root, ConfigOp::Set(BalanceOf::::max_value()), ConfigOp::Set(BalanceOf::::max_value()), ConfigOp::Set(u32::MAX), ConfigOp::Set(u32::MAX), ConfigOp::Set(Percent::max_value()), ConfigOp::Set(Perbill::max_value()), ConfigOp::Set(Percent::max_value()), ); assert_eq!(MinNominatorBond::::get(), BalanceOf::::max_value()); assert_eq!(MinValidatorBond::::get(), BalanceOf::::max_value()); assert_eq!(MaxNominatorsCount::::get(), Some(u32::MAX)); assert_eq!(MaxValidatorsCount::::get(), Some(u32::MAX)); assert_eq!(ChillThreshold::::get(), Some(Percent::from_percent(100))); assert_eq!(MinCommission::::get(), Perbill::from_percent(100)); assert_eq!(MaxStakedRewards::::get(), Some(Percent::from_percent(100))); } #[benchmark] fn set_staking_configs_all_remove() { #[extrinsic_call] set_staking_configs( RawOrigin::Root, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ConfigOp::Remove, ); assert!(!MinNominatorBond::::exists()); assert!(!MinValidatorBond::::exists()); assert!(!MaxNominatorsCount::::exists()); assert!(!MaxValidatorsCount::::exists()); assert!(!ChillThreshold::::exists()); assert!(!MinCommission::::exists()); assert!(!MaxStakedRewards::::exists()); } #[benchmark] fn chill_other() -> Result<(), BenchmarkError> { // clean up any existing state. clear_validators_and_nominators::(); let origin_weight = MinNominatorBond::::get().max(asset::existential_deposit::()); // setup a worst case list scenario. Note that we don't care about the setup of the // destination position because we are doing a removal from the list but no insert. let scenario = ListScenario::::new(origin_weight, true)?; let stash = scenario.origin_stash1; assert!(T::VoterList::contains(&stash)); Staking::::set_staking_configs( RawOrigin::Root.into(), ConfigOp::Set(BalanceOf::::max_value()), ConfigOp::Set(BalanceOf::::max_value()), ConfigOp::Set(0), ConfigOp::Set(0), ConfigOp::Set(Percent::from_percent(0)), ConfigOp::Set(Zero::zero()), ConfigOp::Noop, )?; let caller = whitelisted_caller(); #[extrinsic_call] _(RawOrigin::Signed(caller), stash.clone()); assert!(!T::VoterList::contains(&stash)); Ok(()) } #[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)?; let validator_prefs = ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() }; Staking::::validate(RawOrigin::Signed(controller).into(), validator_prefs)?; // Sanity check assert_eq!( Validators::::get(&stash), ValidatorPrefs { commission: Perbill::from_percent(50), ..Default::default() } ); // Set the min commission to 75% MinCommission::::set(Perbill::from_percent(75)); let caller = whitelisted_caller(); #[extrinsic_call] _(RawOrigin::Signed(caller), stash.clone()); // The validators commission has been bumped to 75% assert_eq!( Validators::::get(&stash), ValidatorPrefs { commission: Perbill::from_percent(75), ..Default::default() } ); Ok(()) } #[benchmark] fn set_min_commission() { let min_commission = Perbill::max_value(); #[extrinsic_call] _(RawOrigin::Root, min_commission); assert_eq!(MinCommission::::get(), Perbill::from_percent(100)); } #[benchmark] fn restore_ledger() -> Result<(), BenchmarkError> { let (stash, controller) = create_stash_controller::(0, 100, RewardDestination::Staked)?; // corrupt ledger. Ledger::::remove(controller); #[extrinsic_call] _(RawOrigin::Root, stash.clone(), None, None, None); assert_eq!(Staking::::inspect_bond_state(&stash), Ok(LedgerIntegrityState::Ok)); Ok(()) } #[benchmark] fn migrate_currency() -> Result<(), BenchmarkError> { let (stash, _ctrl) = create_stash_controller::(USER_SEED, 100, RewardDestination::Staked)?; let stake = asset::staked::(&stash); migrate_to_old_currency::(stash.clone()); // no holds assert!(asset::staked::(&stash).is_zero()); whitelist_account!(stash); #[extrinsic_call] _(RawOrigin::Signed(stash.clone()), stash.clone()); assert_eq!(asset::staked::(&stash), stake); Ok(()) } #[benchmark] fn manual_slash() -> Result<(), BenchmarkError> { // Create a validator with nominators // This will add exposure for our validator in the current era. let (validator_stash, _nominators) = create_validator_with_nominators::( T::MaxExposurePageSize::get() as u32, T::MaxExposurePageSize::get() as u32, false, true, RewardDestination::Staked, )?; let era = CurrentEra::::get().unwrap(); ActiveEra::::put(ActiveEraInfo { index: era, start: None }); let slash_fraction = Perbill::from_percent(10); #[extrinsic_call] _(RawOrigin::Root, validator_stash.clone(), era, slash_fraction); assert!(ValidatorSlashInEra::::get(era, &validator_stash).is_some()); Ok(()) } impl_benchmark_test_suite!( Staking, crate::mock::ExtBuilder::default().has_stakers(true), crate::mock::Test, exec_name = build_and_execute ); } #[cfg(test)] mod tests { use super::*; use crate::mock::{ExtBuilder, RuntimeOrigin, Staking, Test}; use pezframe_support::assert_ok; #[test] fn create_validators_with_nominators_for_era_works() { ExtBuilder::default().build_and_execute(|| { let v = 10; let n = 100; create_validators_with_nominators_for_era::( v, n, MaxNominationsOf::::get() as usize, false, None, ) .unwrap(); let count_validators = Validators::::iter().count(); let count_nominators = Nominators::::iter().count(); assert_eq!(count_validators, Validators::::count() as usize); assert_eq!(count_nominators, Nominators::::count() as usize); assert_eq!(count_validators, v as usize); assert_eq!(count_nominators, n as usize); }); } #[test] fn create_validator_with_nominators_works() { ExtBuilder::default().build_and_execute(|| { let n = 10; let (validator_stash, nominators) = create_validator_with_nominators::( n, <::MaxExposurePageSize as Get<_>>::get(), false, false, RewardDestination::Staked, ) .unwrap(); assert_eq!(nominators.len() as u32, n); let current_era = CurrentEra::::get().unwrap(); let original_stakeable_balance = asset::stakeable_balance::(&validator_stash); assert_ok!(Staking::payout_stakers_by_page( RuntimeOrigin::signed(1337), validator_stash, current_era, 0 )); let new_stakeable_balance = asset::stakeable_balance::(&validator_stash); // reward increases stakeable balance assert!(original_stakeable_balance < new_stakeable_balance); }); } #[test] fn add_slashing_spans_works() { ExtBuilder::default().build_and_execute(|| { let n = 10; let (validator_stash, _nominators) = create_validator_with_nominators::( n, <::MaxExposurePageSize as Get<_>>::get(), false, false, RewardDestination::Staked, ) .unwrap(); // Add 20 slashing spans let num_of_slashing_spans = 20; add_slashing_spans::(&validator_stash, num_of_slashing_spans); let slashing_spans = SlashingSpans::::get(&validator_stash).unwrap(); assert_eq!(slashing_spans.iter().count(), num_of_slashing_spans as usize); for i in 0..num_of_slashing_spans { assert!(SpanSlash::::contains_key((&validator_stash, i))); } // Test everything is cleaned up assert_ok!(Staking::kill_stash(&validator_stash, num_of_slashing_spans)); assert!(SlashingSpans::::get(&validator_stash).is_none()); for i in 0..num_of_slashing_spans { assert!(!SpanSlash::::contains_key((&validator_stash, i))); } }); } }