// 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. //! The overarching mock crate for all EPMB pallets. mod signed; mod staking; use super::*; use crate::{ self as multi_block, signed::{self as signed_pallet, HoldReason}, unsigned::{ self as unsigned_pallet, miner::{MinerConfig, OffchainMinerError, OffchainWorkerMiner}, }, verifier::{self as verifier_pallet, AsynchronousVerifier, Status}, }; use codec::{Decode, Encode, MaxEncodedLen}; use pezframe_election_provider_support::{ bounds::{ElectionBounds, ElectionBoundsBuilder}, InstantElectionProvider, NposSolution, SequentialPhragmen, }; pub use pezframe_support::{assert_noop, assert_ok}; use pezframe_support::{ derive_impl, ord_parameter_types, parameter_types, traits::{fungible::InspectHold, Hooks}, weights::{constants, Weight}, }; use pezframe_system::EnsureRoot; use parking_lot::RwLock; pub use signed::*; use pezsp_core::{ offchain::{ testing::{PoolState, TestOffchainExt, TestTransactionPoolExt}, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, }, ConstBool, }; use pezsp_npos_elections::EvaluateSupport; use pezsp_runtime::{ bounded_vec, traits::{BlakeTwo256, IdentityLookup}, BuildStorage, PerU16, Perbill, }; pub use staking::*; use std::{sync::Arc, vec}; pub type Extrinsic = pezsp_runtime::testing::TestXt; pub type Balance = u64; pub type AccountId = u64; pub type BlockNumber = u64; pub type VoterIndex = u32; pub type TargetIndex = u16; pezframe_support::construct_runtime!( pub enum Runtime { System: pezframe_system, Balances: pezpallet_balances, MultiBlock: multi_block, SignedPallet: signed_pallet, VerifierPallet: verifier_pallet, UnsignedPallet: unsigned_pallet, } ); pezframe_election_provider_support::generate_solution_type!( pub struct TestNposSolution::< VoterIndex = VoterIndex, TargetIndex = TargetIndex, Accuracy = PerU16, MaxVoters = ConstU32::<2_000> >(16) ); #[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)] impl pezframe_system::Config for Runtime { type Hashing = BlakeTwo256; type AccountId = AccountId; type Lookup = IdentityLookup; type BlockLength = (); type BlockWeights = BlockWeights; type AccountData = pezpallet_balances::AccountData; type Block = pezframe_system::mocking::MockBlock; } const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); parameter_types! { pub const ExistentialDeposit: Balance = 1; 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 Balance = Balance; type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); type MaxReserves = (); type ReserveIdentifier = [u8; 8]; type WeightInfo = (); } #[allow(unused)] #[derive(Clone, Debug)] pub enum FallbackModes { Continue, Emergency, Onchain, } #[derive(Clone, Debug)] pub enum AreWeDoneModes { Proceed, BackToSigned, } parameter_types! { // The block at which we emit the start signal. This is used in `roll_next`, which is used all // across tests. The number comes across as a bit weird, but this is mainly due to backwards // compatibility with olds tests, when we used to have pull based election prediction. pub static ElectionStart: BlockNumber = 11; pub static Pages: PageIndex = 3; pub static UnsignedPhase: BlockNumber = 5; pub static SignedPhase: BlockNumber = 5; pub static SignedValidationPhase: BlockNumber = 6; pub static FallbackMode: FallbackModes = FallbackModes::Emergency; pub static MinerTxPriority: u64 = 100; pub static OffchainRepeat: BlockNumber = 5; pub static OffchainStorage: bool = true; pub static MinerMaxLength: u32 = 256; pub static MinerPages: u32 = 1; pub static MaxVotesPerVoter: u32 = ::LIMIT as u32; // by default we stick to 3 pages to host our 12 voters. pub static VoterSnapshotPerBlock: VoterIndex = 4; // and 4 targets, whom we fetch all. pub static TargetSnapshotPerBlock: TargetIndex = 4; // we have 12 voters in the default setting, this should be enough to make sure they are not // trimmed accidentally in any test. #[derive(Encode, Decode, PartialEq, Eq, Debug, scale_info::TypeInfo, MaxEncodedLen)] pub static MaxBackersPerWinner: u32 = 12; pub static MaxBackersPerWinnerFinal: u32 = 12; // we have 4 targets in total and we desire `Desired` thereof, no single page can represent more // than the min of these two. #[derive(Encode, Decode, PartialEq, Eq, Debug, scale_info::TypeInfo, MaxEncodedLen)] pub static MaxWinnersPerPage: u32 = (staking::Targets::get().len() as u32).min(staking::DesiredTargets::get()); pub static AreWeDone: AreWeDoneModes = AreWeDoneModes::Proceed; } ord_parameter_types! { pub const Manager: AccountId = 7; } impl Get> for AreWeDone { fn get() -> Phase { match >::get() { AreWeDoneModes::BackToSigned => RevertToSignedIfNotQueuedOf::::get(), AreWeDoneModes::Proceed => ProceedRegardlessOf::::get(), } } } impl crate::verifier::Config for Runtime { type MaxBackersPerWinnerFinal = MaxBackersPerWinnerFinal; type MaxBackersPerWinner = MaxBackersPerWinner; type MaxWinnersPerPage = MaxWinnersPerPage; type SolutionDataProvider = signed::DualSignedPhase; type WeightInfo = (); } impl crate::unsigned::Config for Runtime { type MinerPages = MinerPages; type OffchainRepeat = OffchainRepeat; type OffchainStorage = OffchainStorage; type MinerTxPriority = MinerTxPriority; type OffchainSolver = SequentialPhragmen; type WeightInfo = (); } impl MinerConfig for Runtime { type AccountId = AccountId; type Hash = ::Hash; type MaxLength = MinerMaxLength; type Pages = Pages; type MaxVotesPerVoter = MaxVotesPerVoter; type Solution = TestNposSolution; type Solver = SequentialPhragmen; type TargetSnapshotPerBlock = TargetSnapshotPerBlock; type VoterSnapshotPerBlock = VoterSnapshotPerBlock; type MaxBackersPerWinner = MaxBackersPerWinner; type MaxBackersPerWinnerFinal = MaxBackersPerWinnerFinal; type MaxWinnersPerPage = MaxWinnersPerPage; } impl crate::Config for Runtime { type SignedPhase = SignedPhase; type SignedValidationPhase = SignedValidationPhase; type UnsignedPhase = UnsignedPhase; type DataProvider = staking::MockStaking; type Fallback = MockFallback; type TargetSnapshotPerBlock = TargetSnapshotPerBlock; type VoterSnapshotPerBlock = VoterSnapshotPerBlock; type MinerConfig = Self; type WeightInfo = (); type Verifier = VerifierPallet; type AdminOrigin = EnsureRoot; type ManagerOrigin = pezframe_system::EnsureSignedBy; type Pages = Pages; type AreWeDone = AreWeDone; type OnRoundRotation = CleanRound; } parameter_types! { pub static OnChainElectionBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); } impl onchain::Config for Runtime { type DataProvider = staking::MockStaking; type MaxBackersPerWinner = MaxBackersPerWinner; type MaxWinnersPerPage = MaxWinnersPerPage; type Sort = ConstBool; type Solver = SequentialPhragmen; type System = Runtime; type WeightInfo = (); type Bounds = OnChainElectionBounds; } pub struct MockFallback; impl ElectionProvider for MockFallback { type AccountId = AccountId; type BlockNumber = u64; type Error = String; type DataProvider = staking::MockStaking; type Pages = ConstU32<1>; type MaxWinnersPerPage = MaxWinnersPerPage; type MaxBackersPerWinner = MaxBackersPerWinner; type MaxBackersPerWinnerFinal = MaxBackersPerWinnerFinal; fn elect(_remaining: PageIndex) -> Result, Self::Error> { unreachable!() } 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> { match FallbackMode::get() { FallbackModes::Continue => crate::Continue::::instant_elect(voters, targets, desired_targets) .map_err(|x| x.to_string()), FallbackModes::Emergency => crate::InitiateEmergencyPhase::::instant_elect( voters, targets, desired_targets, ) .map_err(|x| x.to_string()), FallbackModes::Onchain => onchain::OnChainExecution::::instant_elect( voters, targets, desired_targets, ) .map_err(|e| format!("onchain fallback failed: {:?}", e)), } } fn bother() -> bool { matches!(FallbackMode::get(), FallbackModes::Onchain) } } 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 struct ExtBuilder {} impl ExtBuilder { pub fn full() -> Self { Self {} } pub fn verifier() -> Self { SignedPhase::set(0); SignedValidationPhase::set(0); signed::SignedPhaseSwitch::set(signed::SignedSwitch::Mock); Self {} } pub fn unsigned() -> Self { SignedPhase::set(0); SignedValidationPhase::set(0); signed::SignedPhaseSwitch::set(signed::SignedSwitch::Mock); Self {} } pub fn signed() -> Self { UnsignedPhase::set(0); Self {} } } impl ExtBuilder { pub(crate) fn max_backers_per_winner(self, c: u32) -> Self { MaxBackersPerWinner::set(c); self } pub(crate) fn max_backers_per_winner_final(self, c: u32) -> Self { MaxBackersPerWinnerFinal::set(c); self } pub(crate) fn offchain_storage(self, s: bool) -> Self { OffchainStorage::set(s); self } pub(crate) fn miner_tx_priority(self, p: u64) -> Self { MinerTxPriority::set(p); self } pub(crate) fn election_start(self, at: BlockNumber) -> Self { ElectionStart::set(at); self } pub(crate) fn pages(self, pages: PageIndex) -> Self { Pages::set(pages); self } pub(crate) fn voter_per_page(self, count: u32) -> Self { VoterSnapshotPerBlock::set(count); self } pub(crate) fn miner_max_length(self, len: u32) -> Self { MinerMaxLength::set(len); self } pub(crate) fn desired_targets(self, t: u32) -> Self { staking::DesiredTargets::set(t); self } pub(crate) fn signed_phase(self, d: BlockNumber, v: BlockNumber) -> Self { SignedPhase::set(d); SignedValidationPhase::set(v); self } pub(crate) fn unsigned_phase(self, d: BlockNumber) -> Self { UnsignedPhase::set(d); self } pub(crate) fn signed_validation_phase(self, d: BlockNumber) -> Self { SignedValidationPhase::set(d); self } pub(crate) fn miner_pages(self, p: u32) -> Self { MinerPages::set(p); self } pub(crate) fn max_signed_submissions(self, s: u32) -> Self { SignedMaxSubmissions::set(s); self } pub(crate) fn max_winners_per_page(self, w: u32) -> Self { MaxWinnersPerPage::set(w); self } #[allow(unused)] pub(crate) fn add_voter(self, who: AccountId, stake: Balance, targets: Vec) -> Self { staking::VOTERS.with(|v| v.borrow_mut().push((who, stake, targets.try_into().unwrap()))); self } pub(crate) fn fallback_mode(self, mode: FallbackModes) -> Self { FallbackMode::set(mode); self } pub(crate) fn are_we_done(self, mode: AreWeDoneModes) -> Self { AreWeDone::set(mode); self } pub(crate) fn build_unchecked(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. (91, 100), (92, 100), (93, 100), (94, 100), (95, 100), (96, 100), (97, 100), (98, 100), (99, 100), (999, 100), (9999, 100), ], ..Default::default() } .assimilate_storage(&mut storage); pezsp_io::TestExternalities::from(storage) } /// Warning: this does not execute the post-sanity-checks. pub(crate) fn build_offchainify(self) -> (pezsp_io::TestExternalities, Arc>) { let mut ext = self.build_unchecked(); let (offchain, _offchain_state) = TestOffchainExt::new(); let (pool, pool_state) = TestTransactionPoolExt::new(); ext.register_extension(OffchainDbExt::new(offchain.clone())); ext.register_extension(OffchainWorkerExt::new(offchain)); ext.register_extension(TransactionPoolExt::new(pool)); (ext, pool_state) } /// Build the externalities, and execute the given s`test` closure with it. pub(crate) fn build_and_execute(self, test: impl FnOnce() -> ()) { let mut ext = self.build_unchecked(); ext.execute_with_sanity_checks(test); } } pub trait ExecuteWithSanityChecks { fn execute_with_sanity_checks(&mut self, test: impl FnOnce() -> ()); } impl ExecuteWithSanityChecks for pezsp_io::TestExternalities { fn execute_with_sanity_checks(&mut self, test: impl FnOnce() -> ()) { self.execute_with(all_pallets_integrity_test); self.execute_with(test); self.execute_with(all_pallets_sanity_checks); } } fn all_pallets_integrity_test() { // ensure that all pallets are sane. VerifierPallet::integrity_test(); UnsignedPallet::integrity_test(); MultiBlock::integrity_test(); SignedPallet::integrity_test(); } fn all_pallets_sanity_checks() { let now = System::block_number(); let _ = VerifierPallet::do_try_state(now).unwrap(); let _ = UnsignedPallet::do_try_state(now).unwrap(); let _ = MultiBlock::do_try_state(now).unwrap(); let _ = SignedPallet::do_try_state(now).unwrap(); } /// Fully verify a solution. /// /// This will progress the blocks until the verifier pallet is done verifying it. /// /// The solution must have already been loaded via `load_and_start_verification`. /// /// Return the final supports, which is the outcome. If this succeeds, then the valid variant of the /// `QueuedSolution` form `verifier` is ready to be read. pub fn roll_to_full_verification() -> Vec> { // we must be ready to verify. assert_eq!(VerifierPallet::status(), Status::Ongoing(Pages::get() - 1)); while matches!(VerifierPallet::status(), Status::Ongoing(_)) { roll_to(System::block_number() + 1); } (MultiBlock::lsp()..=MultiBlock::msp()) .map(|p| VerifierPallet::get_queued_solution_page(p).unwrap_or_default()) .collect::>() } /// Generate a single page of `TestNposSolution` from the give supports. /// /// All of the voters in this support must live in a single page of the snapshot, noted by /// `snapshot_page`. pub fn solution_from_supports( supports: pezsp_npos_elections::Supports, snapshot_page: PageIndex, ) -> TestNposSolution { let staked = pezsp_npos_elections::supports_to_staked_assignment(supports); let assignments = pezsp_npos_elections::assignment_staked_to_ratio_normalized(staked).unwrap(); let voters = crate::Snapshot::::voters(snapshot_page).unwrap(); let targets = crate::Snapshot::::targets().unwrap(); let voter_index = helpers::voter_index_fn_linear::(&voters); let target_index = helpers::target_index_fn_linear::(&targets); TestNposSolution::from_assignment(&assignments, &voter_index, &target_index).unwrap() } /// Generate a raw paged solution from the given vector of supports. /// /// Given vector must be aligned with the snapshot, at most need to be 'pagified' which we do /// internally. pub fn raw_paged_from_supports( paged_supports: Vec>, round: u32, ) -> PagedRawSolution { let score = { let flattened = paged_supports.iter().cloned().flatten().collect::>(); flattened.evaluate() }; let solution_pages = paged_supports .pagify(Pages::get()) .map(|(page_index, page_support)| solution_from_supports(page_support.to_vec(), page_index)) .collect::>(); let solution_pages = solution_pages.try_into().unwrap(); PagedRawSolution { solution_pages, score, round } } /// ensure that the snapshot fully exists. /// /// NOTE: this should not be used that often, because we check snapshot in sanity checks, which are /// called ALL THE TIME. pub fn assert_full_snapshot() { assert_ok!(Snapshot::::ensure_snapshot(true, Pages::get())); } /// ensure that the no snapshot exists. /// /// NOTE: this should not be used that often, because we check snapshot in sanity checks, which are /// called ALL THE TIME. pub fn assert_none_snapshot() { assert_ok!(Snapshot::::ensure_snapshot(false, Pages::get())); } /// Simple wrapper for mining a new solution. Just more handy in case the interface of mine solution /// changes. /// /// For testing, we never want to do reduce. pub fn mine_full_solution() -> Result, OffchainMinerError> { OffchainWorkerMiner::::mine_solution(Pages::get(), false) } /// Same as [`mine_full_solution`] but with custom pages. pub fn mine_solution( pages: PageIndex, ) -> Result, OffchainMinerError> { OffchainWorkerMiner::::mine_solution(pages, false) } /// Assert that `count` voters exist across `pages` number of pages. pub fn ensure_voters(pages: PageIndex, count: usize) { assert_eq!(crate::Snapshot::::voter_pages(), pages); assert_eq!(crate::Snapshot::::voters_iter_flattened().count(), count); } /// Assert that `count` targets exist across `pages` number of pages. pub fn ensure_targets(pages: PageIndex, count: usize) { assert_eq!(crate::Snapshot::::target_pages(), pages); assert_eq!(crate::Snapshot::::targets().unwrap().len(), count); } /// get the events of the multi-block pallet. pub fn multi_block_events() -> Vec> { System::events() .into_iter() .map(|r| r.event) .filter_map(|e| if let RuntimeEvent::MultiBlock(inner) = e { Some(inner) } else { None }) .collect::>() } parameter_types! { static MultiBlockEvents: u32 = 0; static VerifierEvents: u32 = 0; } pub fn multi_block_events_since_last_call() -> Vec> { let events = multi_block_events(); let already_seen = MultiBlockEvents::get(); MultiBlockEvents::set(events.len() as u32); events.into_iter().skip(already_seen as usize).collect() } /// get the events of the verifier pallet. pub fn verifier_events() -> Vec> { System::events() .into_iter() .map(|r| r.event) .filter_map( |e| if let RuntimeEvent::VerifierPallet(inner) = e { Some(inner) } else { None }, ) .collect::>() } /// get the events of the verifier pallet since last call. pub fn verifier_events_since_last_call() -> Vec> { let events = verifier_events(); let already_seen = VerifierEvents::get(); VerifierEvents::set(events.len() as u32); events.into_iter().skip(already_seen as usize).collect() } /// proceed block number to `n`. pub fn roll_to(n: BlockNumber) { crate::Pallet::::roll_to( n, matches!(SignedPhaseSwitch::get(), SignedSwitch::Real), true, ); } /// proceed block number to whenever the snapshot is fully created (`Phase::Snapshot(0)`). pub fn roll_to_snapshot_created() { while !matches!(MultiBlock::current_phase(), Phase::Snapshot(0)) { roll_next() } roll_next(); assert_full_snapshot(); } /// proceed block number to whenever the unsigned phase is open (`Phase::Unsigned(_)`). pub fn roll_to_unsigned_open() { while !matches!(MultiBlock::current_phase(), Phase::Unsigned(_)) { roll_next() } } /// proceed block number to whenever the unsigned phase is about to close (`Phase::Unsigned(_)`). pub fn roll_to_last_unsigned() { while !matches!(MultiBlock::current_phase(), Phase::Unsigned(0)) { roll_next() } } /// proceed block number to whenever the signed phase is open (`Phase::Signed(_)`). pub fn roll_to_signed_open() { while !matches!(MultiBlock::current_phase(), Phase::Signed(_)) { roll_next(); } } /// proceed block number to whenever the signed validation phase is open /// (`Phase::SignedValidation(_)`). pub fn roll_to_signed_validation_open() { while !matches!(MultiBlock::current_phase(), Phase::SignedValidation(_)) { roll_next() } } /// proceed block number until we reach the done phase (`Phase::Done`). pub fn roll_to_done() { while !MultiBlock::current_phase().is_done() { roll_next() } } /// Proceed one block. pub fn roll_next() { let now = System::block_number(); roll_to(now + 1); } /// Proceed one block, and execute offchain workers as well. pub fn roll_next_with_ocw(maybe_pool: Option>>) { roll_to_with_ocw(System::block_number() + 1, maybe_pool) } pub fn roll_to_unsigned_open_with_ocw(maybe_pool: Option>>) { while !matches!(MultiBlock::current_phase(), Phase::Unsigned(_)) { roll_next_with_ocw(maybe_pool.clone()); } } /// proceed block number to `n`, while running all offchain workers as well. pub fn roll_to_with_ocw(n: BlockNumber, maybe_pool: Option>>) { use pezsp_runtime::traits::Dispatchable; let now = System::block_number(); for i in now + 1..=n { // check the offchain transaction pool, and if anything's there, submit it. if let Some(ref pool) = maybe_pool { pool.read() .transactions .clone() .into_iter() .map(|uxt| ::decode(&mut &*uxt).unwrap()) .for_each(|xt| { xt.function.dispatch(pezframe_system::RawOrigin::None.into()).unwrap(); }); pool.try_write().unwrap().transactions.clear(); } System::set_block_number(i); MultiBlock::on_initialize(i); VerifierPallet::on_initialize(i); UnsignedPallet::on_initialize(i); if matches!(SignedPhaseSwitch::get(), SignedSwitch::Real) { SignedPallet::on_initialize(i); } MultiBlock::offchain_worker(i); VerifierPallet::offchain_worker(i); UnsignedPallet::offchain_worker(i); if matches!(SignedPhaseSwitch::get(), SignedSwitch::Real) { SignedPallet::offchain_worker(i); } // invariants must hold at the end of each block. all_pallets_sanity_checks() } } /// An invalid solution with any score. pub fn fake_solution(score: ElectionScore) -> PagedRawSolution { PagedRawSolution { score, solution_pages: bounded_vec![Default::default()], ..Default::default() } } /// A real solution that's valid, but has a really bad score. /// /// This is different from `solution_from_supports` in that it does not require the snapshot to /// exist. pub fn raw_paged_solution_low_score() -> PagedRawSolution { PagedRawSolution { solution_pages: vec![TestNposSolution { // 2 targets, both voting for themselves votes1: vec![(0, 0), (1, 2)], ..Default::default() }] .try_into() .unwrap(), round: 0, score: ElectionScore { minimal_stake: 10, sum_stake: 20, sum_stake_squared: 200 }, } } /// Get the free and held balance of `who`. pub fn balances(who: AccountId) -> (Balance, Balance) { ( Balances::free_balance(who), Balances::balance_on_hold(&HoldReason::SignedSubmission.into(), &who), ) } /// Election bounds based on just the given count. pub fn bound_by_count(count: Option) -> DataProviderBounds { DataProviderBounds { count: count.map(|x| x.into()), size: None } } pub fn emergency_solution() -> (BoundedSupportsOf, ElectionScore) { let supports = onchain::OnChainExecution::::elect(0).unwrap(); let score = supports.evaluate(); (supports, score) }