// 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. use super::*; use crate::{self as multi_phase, signed::GeometricDepositBase, unsigned::MinerConfig}; use multi_phase::unsigned::{IndexAssignmentOf, VoterOf}; use parking_lot::RwLock; use pezframe_election_provider_support::{ bounds::{DataProviderBounds, ElectionBounds, ElectionBoundsBuilder}, data_provider, onchain, ElectionDataProvider, NposSolution, SequentialPhragmen, }; pub use pezframe_support::derive_impl; use pezframe_support::{ parameter_types, traits::{ConstU32, Hooks}, weights::{constants, Weight}, BoundedVec, }; use pezsp_core::{ offchain::{ testing::{PoolState, TestOffchainExt, TestTransactionPoolExt}, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, }, ConstBool, H256, }; use pezsp_npos_elections::{ assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, BalancingConfig, ElectionResult, EvaluateSupport, }; use pezsp_runtime::{ bounded_vec, testing::Header, traits::{BlakeTwo256, Convert, IdentityLookup}, BuildStorage, PerU16, Percent, }; use std::sync::Arc; pub type Block = pezsp_runtime::generic::Block; pub type UncheckedExtrinsic = pezsp_runtime::generic::UncheckedExtrinsic; pezframe_support::construct_runtime!( pub enum Runtime { System: pezframe_system, Balances: pezpallet_balances, MultiPhase: multi_phase, } ); pub(crate) type Balance = u64; pub(crate) type AccountId = u64; pub(crate) type BlockNumber = u64; pub(crate) type VoterIndex = u32; pub(crate) type TargetIndex = u16; pezframe_election_provider_support::generate_solution_type!( #[compact] pub struct TestNposSolution::< VoterIndex = VoterIndex, TargetIndex = TargetIndex, Accuracy = PerU16, MaxVoters = ConstU32::<2_000> >(16) ); /// All events of this pezpallet. pub(crate) fn multi_phase_events() -> Vec> { System::read_events_for_pallet::>() } /// To from `now` to block `n`. pub fn roll_to(n: BlockNumber) { let now = System::block_number(); for i in now + 1..=n { System::set_block_number(i); MultiPhase::on_initialize(i); } } pub fn roll_to_unsigned() { while !matches!(CurrentPhase::::get(), Phase::Unsigned(_)) { roll_to(System::block_number() + 1); } } pub fn roll_to_signed() { while !matches!(CurrentPhase::::get(), Phase::Signed) { roll_to(System::block_number() + 1); } } pub fn roll_to_with_ocw(n: BlockNumber) { let now = System::block_number(); for i in now + 1..=n { System::set_block_number(i); MultiPhase::on_initialize(i); MultiPhase::offchain_worker(i); } } pub fn roll_to_round(n: u32) { assert!(Round::::get() <= n); while Round::::get() != n { roll_to_signed(); pezframe_support::assert_ok!(MultiPhase::elect(Zero::zero())); } } pub struct TrimHelpers { pub voters: Vec>, pub assignments: Vec>, pub encoded_size_of: Box]) -> Result>, pub voter_index: Box< dyn Fn( &::AccountId, ) -> Option>, >, } /// Helpers for setting up trimming tests. /// /// Assignments are pre-sorted in reverse order of stake. pub fn trim_helpers() -> TrimHelpers { let RoundSnapshot { voters, targets } = Snapshot::::get().unwrap(); let stakes: std::collections::HashMap<_, _> = voters.iter().map(|(id, stake, _)| (*id, *stake)).collect(); // Compute the size of a solution comprised of the selected arguments. // // This function completes in `O(edges)`; it's expensive, but linear. let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf]| { SolutionOf::::try_from(assignments).map(|s| s.encoded_size()) }); let cache = helpers::generate_voter_cache::(&voters); let voter_index = helpers::voter_index_fn_owned::(cache); let target_index = helpers::target_index_fn::(&targets); let desired_targets = crate::DesiredTargets::::get().unwrap(); let ElectionResult::<_, SolutionAccuracyOf> { mut assignments, .. } = seq_phragmen(desired_targets as usize, targets.clone(), voters.clone(), None).unwrap(); // sort by decreasing order of stake assignments.sort_by_key(|assignment| { std::cmp::Reverse(stakes.get(&assignment.who).cloned().unwrap_or_default()) }); // convert to IndexAssignment let assignments = assignments .iter() .map(|assignment| { IndexAssignmentOf::::new(assignment, &voter_index, &target_index) }) .collect::, _>>() .expect("test assignments don't contain any voters with too many votes"); TrimHelpers { voters, assignments, encoded_size_of, voter_index: Box::new(voter_index) } } /// Spit out a verifiable raw solution. /// /// This is a good example of what an offchain miner would do. pub fn raw_solution() -> RawSolution> { let RoundSnapshot { voters, targets } = Snapshot::::get().unwrap(); let desired_targets = crate::DesiredTargets::::get().unwrap(); let ElectionResult::<_, SolutionAccuracyOf> { winners: _, assignments } = seq_phragmen(desired_targets as usize, targets.clone(), voters.clone(), None).unwrap(); // closures let cache = helpers::generate_voter_cache::(&voters); let voter_index = helpers::voter_index_fn_linear::(&voters); let target_index = helpers::target_index_fn_linear::(&targets); let stake_of = helpers::stake_of_fn::(&voters, &cache); let score = { let staked = assignment_ratio_to_staked_normalized(assignments.clone(), &stake_of).unwrap(); to_supports(&staked).evaluate() }; let solution = SolutionOf::::from_assignment(&assignments, &voter_index, &target_index).unwrap(); let round = Round::::get(); RawSolution { solution, score, round } } pub fn witness() -> SolutionOrSnapshotSize { Snapshot::::get() .map(|snap| SolutionOrSnapshotSize { voters: snap.voters.len() as u32, targets: snap.targets.len() as u32, }) .unwrap_or_default() } #[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)] impl pezframe_system::Config for Runtime { type SS58Prefix = (); type BaseCallFilter = pezframe_support::traits::Everything; type RuntimeOrigin = RuntimeOrigin; type Nonce = u64; type RuntimeCall = RuntimeCall; type Hash = H256; type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; type Block = Block; type RuntimeEvent = RuntimeEvent; type BlockHashCount = (); type DbWeight = (); type BlockLength = (); type BlockWeights = BlockWeights; type Version = (); type PalletInfo = PalletInfo; type AccountData = pezpallet_balances::AccountData; type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); type OnSetCode = (); type MaxConsumers = ConstU32<16>; } const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); parameter_types! { pub BlockWeights: pezframe_system::limits::BlockWeights = pezframe_system::limits::BlockWeights ::with_sensible_defaults( Weight::from_parts(2u64 * constants::WEIGHT_REF_TIME_PER_SECOND, u64::MAX), NORMAL_DISPATCH_RATIO, ); } #[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)] impl pezpallet_balances::Config for Runtime { type AccountStore = System; } #[derive(Default, Eq, PartialEq, Debug, Clone, Copy)] pub enum MockedWeightInfo { #[default] Basic, Complex, Real, } parameter_types! { pub static Targets: Vec = vec![10, 20, 30, 40]; pub static Voters: Vec> = vec![ (1, 10, bounded_vec![10, 20]), (2, 10, bounded_vec![30, 40]), (3, 10, bounded_vec![40]), (4, 10, bounded_vec![10, 20, 30, 40]), // self votes. (10, 10, bounded_vec![10]), (20, 20, bounded_vec![20]), (30, 30, bounded_vec![30]), (40, 40, bounded_vec![40]), ]; pub static DesiredTargets: u32 = 2; pub static SignedPhase: BlockNumber = 10; pub static UnsignedPhase: BlockNumber = 5; pub static SignedMaxSubmissions: u32 = 5; pub static SignedMaxRefunds: u32 = 1; // for tests only. if `EnableVariableDepositBase` is true, the deposit base will be calculated // by `Multiphase::DepositBase`. Otherwise the deposit base is `SignedFixedDeposit`. pub static EnableVariableDepositBase: bool = false; pub static SignedFixedDeposit: Balance = 5; pub static SignedDepositIncreaseFactor: Percent = Percent::from_percent(10); pub static SignedDepositByte: Balance = 0; pub static SignedDepositWeight: Balance = 0; pub static SignedRewardBase: Balance = 7; pub static SignedMaxWeight: Weight = BlockWeights::get().max_block; pub static MinerTxPriority: u64 = 100; pub static BetterSignedThreshold: Perbill = Perbill::zero(); pub static OffchainRepeat: BlockNumber = 5; pub static MinerMaxWeight: Weight = BlockWeights::get().max_block; pub static MinerMaxLength: u32 = 256; pub static MockWeightInfo: MockedWeightInfo = MockedWeightInfo::Real; pub static MaxElectingVoters: VoterIndex = u32::max_value(); pub static MaxElectableTargets: TargetIndex = TargetIndex::max_value(); #[derive(Debug)] pub static MaxWinners: u32 = 200; #[derive(Debug)] pub static MaxBackersPerWinner: u32 = 200; // `ElectionBounds` and `OnChainElectionsBounds` are defined separately to set them independently in the tests. pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); pub static OnChainElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); pub static EpochLength: u64 = 30; pub static OnChainFallback: bool = true; } pub struct OnChainSeqPhragmen; impl onchain::Config for OnChainSeqPhragmen { type System = Runtime; type Solver = SequentialPhragmen, Balancing>; type DataProvider = StakingMock; type WeightInfo = (); type MaxWinnersPerPage = MaxWinners; type MaxBackersPerWinner = MaxBackersPerWinner; type Sort = ConstBool; type Bounds = OnChainElectionsBounds; } pub struct MockFallback; impl ElectionProvider for MockFallback { type AccountId = AccountId; type BlockNumber = BlockNumber; type Error = &'static str; type MaxWinnersPerPage = MaxWinners; type MaxBackersPerWinner = MaxBackersPerWinner; type MaxBackersPerWinnerFinal = MaxBackersPerWinner; type Pages = ConstU32<1>; type DataProvider = StakingMock; fn elect(_remaining: PageIndex) -> Result, Self::Error> { unimplemented!() } fn duration() -> Self::BlockNumber { 0 } fn start() -> Result<(), Self::Error> { Ok(()) } fn status() -> Result { Ok(true) } } impl InstantElectionProvider for MockFallback { fn instant_elect( voters: Vec>, targets: Vec, desired_targets: u32, ) -> Result, Self::Error> { if OnChainFallback::get() { onchain::OnChainExecution::::instant_elect( voters, targets, desired_targets, ) .map_err(|_| "onchain::OnChainExecution failed.") } else { Err("NoFallback.") } } fn bother() -> bool { OnChainFallback::get() } } parameter_types! { pub static Balancing: Option = Some( BalancingConfig { iterations: 0, tolerance: 0 } ); } pub struct TestBenchmarkingConfig; impl BenchmarkingConfig for TestBenchmarkingConfig { const VOTERS: [u32; 2] = [400, 600]; const ACTIVE_VOTERS: [u32; 2] = [100, 300]; const TARGETS: [u32; 2] = [200, 400]; const DESIRED_TARGETS: [u32; 2] = [100, 180]; const SNAPSHOT_MAXIMUM_VOTERS: u32 = 1000; const MINER_MAXIMUM_VOTERS: u32 = 1000; const MAXIMUM_TARGETS: u32 = 200; } impl MinerConfig for Runtime { type AccountId = AccountId; type MaxLength = MinerMaxLength; type MaxWeight = MinerMaxWeight; type MaxVotesPerVoter = ::MaxVotesPerVoter; type MaxWinners = MaxWinners; type MaxBackersPerWinner = MaxBackersPerWinner; type Solution = TestNposSolution; fn solution_weight(v: u32, t: u32, a: u32, d: u32) -> Weight { match MockWeightInfo::get() { MockedWeightInfo::Basic => Weight::from_parts( (10 as u64).saturating_add((5 as u64).saturating_mul(a as u64)), 0, ), MockedWeightInfo::Complex => Weight::from_parts((0 * v + 0 * t + 1000 * a + 0 * d) as u64, 0), MockedWeightInfo::Real => <() as multi_phase::weights::WeightInfo>::feasibility_check(v, t, a, d), } } } impl crate::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; type EstimateCallFee = pezframe_support::traits::ConstU64<8>; type SignedPhase = SignedPhase; type UnsignedPhase = UnsignedPhase; type BetterSignedThreshold = BetterSignedThreshold; type OffchainRepeat = OffchainRepeat; type MinerTxPriority = MinerTxPriority; type SignedRewardBase = SignedRewardBase; type SignedDepositBase = Self; type SignedDepositByte = (); type SignedDepositWeight = (); type SignedMaxWeight = SignedMaxWeight; type SignedMaxSubmissions = SignedMaxSubmissions; type SignedMaxRefunds = SignedMaxRefunds; type SlashHandler = (); type RewardHandler = (); type DataProvider = StakingMock; type WeightInfo = (); type BenchmarkingConfig = TestBenchmarkingConfig; type Fallback = MockFallback; type GovernanceFallback = pezframe_election_provider_support::onchain::OnChainExecution; type ForceOrigin = pezframe_system::EnsureRoot; type MaxWinners = MaxWinners; type MaxBackersPerWinner = MaxBackersPerWinner; type MinerConfig = Self; type Solver = SequentialPhragmen, Balancing>; type ElectionBounds = ElectionsBounds; } impl Convert> for Runtime { /// returns the geometric increase deposit fee if `EnableVariableDepositBase` is set, otherwise /// the fee is `SignedFixedDeposit`. fn convert(queue_len: usize) -> Balance { if !EnableVariableDepositBase::get() { SignedFixedDeposit::get() } else { GeometricDepositBase::::convert(queue_len) } } } impl pezframe_system::offchain::CreateTransactionBase for Runtime where RuntimeCall: From, { type RuntimeCall = RuntimeCall; type Extrinsic = Extrinsic; } impl pezframe_system::offchain::CreateBare for Runtime where RuntimeCall: From, { fn create_bare(call: Self::RuntimeCall) -> Self::Extrinsic { Extrinsic::new_bare(call) } } pub type Extrinsic = pezsp_runtime::testing::TestXt; parameter_types! { pub MaxNominations: u32 = ::LIMIT as u32; // only used in testing to manipulate mock behaviour pub static DataProviderAllowBadData: bool = false; } #[derive(Default)] pub struct ExtBuilder {} pub struct StakingMock; impl ElectionDataProvider for StakingMock { type BlockNumber = BlockNumber; type AccountId = AccountId; type MaxVotesPerVoter = MaxNominations; fn electable_targets( bounds: DataProviderBounds, remaining_pages: PageIndex, ) -> data_provider::Result> { assert!(remaining_pages.is_zero()); let targets = Targets::get(); if !DataProviderAllowBadData::get() && bounds.count.map_or(false, |max_len| targets.len() > max_len.0 as usize) { return Err("Targets too big"); } Ok(targets) } fn electing_voters( bounds: DataProviderBounds, remaining_pages: PageIndex, ) -> data_provider::Result>> { assert!(remaining_pages.is_zero()); let mut voters = Voters::get(); if !DataProviderAllowBadData::get() { if let Some(max_len) = bounds.count { voters.truncate(max_len.0 as usize) } } Ok(voters) } fn desired_targets() -> data_provider::Result { Ok(DesiredTargets::get()) } fn next_election_prediction(now: u64) -> u64 { now + EpochLength::get() - now % EpochLength::get() } #[cfg(feature = "runtime-benchmarks")] fn put_snapshot( voters: Vec>, targets: Vec, _target_stake: Option, ) { Targets::set(targets); Voters::set(voters); } #[cfg(feature = "runtime-benchmarks")] fn clear() { Targets::set(vec![]); Voters::set(vec![]); } #[cfg(feature = "runtime-benchmarks")] fn add_voter( voter: AccountId, weight: VoteWeight, targets: pezframe_support::BoundedVec, ) { let mut current = Voters::get(); current.push((voter, weight, targets)); Voters::set(current); } #[cfg(feature = "runtime-benchmarks")] fn add_target(target: AccountId) { let mut current = Targets::get(); current.push(target); Targets::set(current); } } impl ExtBuilder { pub fn miner_tx_priority(self, p: u64) -> Self { ::set(p); self } pub fn better_signed_threshold(self, p: Perbill) -> Self { ::set(p); self } pub fn phases(self, signed: BlockNumber, unsigned: BlockNumber) -> Self { ::set(signed); ::set(unsigned); self } pub fn onchain_fallback(self, onchain: bool) -> Self { ::set(onchain); self } pub fn miner_weight(self, weight: Weight) -> Self { ::set(weight); self } pub fn mock_weight_info(self, mock: MockedWeightInfo) -> Self { ::set(mock); self } pub fn desired_targets(self, t: u32) -> Self { ::set(t); self } pub fn add_voter( self, who: AccountId, stake: Balance, targets: BoundedVec, ) -> Self { VOTERS.with(|v| v.borrow_mut().push((who, stake, targets))); self } pub fn signed_max_submission(self, count: u32) -> Self { ::set(count); self } pub fn signed_base_deposit(self, base: u64, variable: bool, increase: Percent) -> Self { ::set(variable); ::set(base); ::set(increase); self } pub fn signed_deposit(self, base: u64, byte: u64, weight: u64) -> Self { ::set(base); ::set(byte); ::set(weight); self } pub fn signed_weight(self, weight: Weight) -> Self { ::set(weight); self } pub fn max_backers_per_winner(self, max: u32) -> Self { MaxBackersPerWinner::set(max); self } pub fn build(self) -> pezsp_io::TestExternalities { pezsp_tracing::try_init_simple(); let mut storage = pezframe_system::GenesisConfig::::default().build_storage().unwrap(); let _ = pezpallet_balances::GenesisConfig:: { balances: vec![ // bunch of account for submitting stuff only. (99, 100), (100, 100), (101, 100), (102, 100), (103, 100), (104, 100), (105, 100), (999, 100), (9999, 100), ], ..Default::default() } .assimilate_storage(&mut storage); pezsp_io::TestExternalities::from(storage) } pub fn build_offchainify( self, iters: u32, ) -> (pezsp_io::TestExternalities, Arc>) { let mut ext = self.build(); let (offchain, offchain_state) = TestOffchainExt::new(); let (pool, pool_state) = TestTransactionPoolExt::new(); let mut seed = [0_u8; 32]; seed[0..4].copy_from_slice(&iters.to_le_bytes()); offchain_state.write().seed = seed; ext.register_extension(OffchainDbExt::new(offchain.clone())); ext.register_extension(OffchainWorkerExt::new(offchain)); ext.register_extension(TransactionPoolExt::new(pool)); (ext, pool_state) } pub fn build_and_execute(self, test: impl FnOnce() -> ()) { pezsp_tracing::try_init_simple(); let mut ext = self.build(); ext.execute_with(test); #[cfg(feature = "try-runtime")] ext.execute_with(|| { pezframe_support::assert_ok!( >::try_state( System::block_number() ) ); }); } } pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) { (Balances::free_balance(who), Balances::reserved_balance(who)) }