Files
pezkuwi-subxt/substrate/frame/election-provider-multi-phase/src/mock.rs
T
Ankan ffb2125f4a [NPoS] Remove better solution threshold for unsigned submissions (#2694)
closes https://github.com/paritytech-secops/srlabs_findings/issues/78.

Removes `BetterUnsignedThreshold` from pallet EPM. This will essentially
mean any solution submitted by the validator that is strictly better
than the current queued solution would be accepted.

The reason for having these thresholds is to limit number of solutions
submitted on-chain. However for unsigned submissions, the number of
solutions that could be submitted on average is limited even without
thresholding (calculation shown in the corresponding issue).
2023-12-15 20:59:39 +01:00

651 lines
19 KiB
Rust

// This file is part of Substrate.
// 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 frame_election_provider_support::{
bounds::{DataProviderBounds, ElectionBounds},
data_provider, onchain, ElectionDataProvider, NposSolution, SequentialPhragmen,
};
pub use frame_support::{assert_noop, assert_ok, derive_impl, pallet_prelude::GetDefault};
use frame_support::{
parameter_types,
traits::{ConstU32, Hooks},
weights::{constants, Weight},
BoundedVec,
};
use multi_phase::unsigned::{IndexAssignmentOf, VoterOf};
use parking_lot::RwLock;
use sp_core::{
offchain::{
testing::{PoolState, TestOffchainExt, TestTransactionPoolExt},
OffchainDbExt, OffchainWorkerExt, TransactionPoolExt,
},
H256,
};
use sp_npos_elections::{
assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, BalancingConfig,
ElectionResult, EvaluateSupport,
};
use sp_runtime::{
bounded_vec,
testing::Header,
traits::{BlakeTwo256, Convert, IdentityLookup},
BuildStorage, PerU16, Percent,
};
use std::sync::Arc;
pub type Block = sp_runtime::generic::Block<Header, UncheckedExtrinsic>;
pub type UncheckedExtrinsic =
sp_runtime::generic::UncheckedExtrinsic<AccountId, RuntimeCall, (), ()>;
frame_support::construct_runtime!(
pub struct Runtime
{
System: frame_system::{Pallet, Call, Event<T>, Config<T>},
Balances: pallet_balances::{Pallet, Call, Event<T>, Config<T>},
MultiPhase: multi_phase::{Pallet, Call, Event<T>},
}
);
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;
frame_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 pallet.
pub(crate) fn multi_phase_events() -> Vec<super::Event<Runtime>> {
System::read_events_for_pallet::<super::Event<Runtime>>()
}
/// 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!(MultiPhase::current_phase(), Phase::Unsigned(_)) {
roll_to(System::block_number() + 1);
}
}
pub fn roll_to_signed() {
while !matches!(MultiPhase::current_phase(), 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!(MultiPhase::round() <= n);
while MultiPhase::round() != n {
roll_to_signed();
assert_ok!(MultiPhase::elect());
}
}
pub struct TrimHelpers {
pub voters: Vec<VoterOf<Runtime>>,
pub assignments: Vec<IndexAssignmentOf<Runtime>>,
pub encoded_size_of:
Box<dyn Fn(&[IndexAssignmentOf<Runtime>]) -> Result<usize, sp_npos_elections::Error>>,
pub voter_index: Box<
dyn Fn(
&<Runtime as frame_system::Config>::AccountId,
) -> Option<SolutionVoterIndexOf<Runtime>>,
>,
}
/// Helpers for setting up trimming tests.
///
/// Assignments are pre-sorted in reverse order of stake.
pub fn trim_helpers() -> TrimHelpers {
let RoundSnapshot { voters, targets } = MultiPhase::snapshot().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<Runtime>]| {
SolutionOf::<Runtime>::try_from(assignments).map(|s| s.encoded_size())
});
let cache = helpers::generate_voter_cache::<Runtime>(&voters);
let voter_index = helpers::voter_index_fn_owned::<Runtime>(cache);
let target_index = helpers::target_index_fn::<Runtime>(&targets);
let desired_targets = MultiPhase::desired_targets().unwrap();
let ElectionResult::<_, SolutionAccuracyOf<Runtime>> { 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::<Runtime>::new(assignment, &voter_index, &target_index)
})
.collect::<Result<Vec<_>, _>>()
.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<SolutionOf<Runtime>> {
let RoundSnapshot { voters, targets } = MultiPhase::snapshot().unwrap();
let desired_targets = MultiPhase::desired_targets().unwrap();
let ElectionResult::<_, SolutionAccuracyOf<Runtime>> { winners: _, assignments } =
seq_phragmen(desired_targets as usize, targets.clone(), voters.clone(), None).unwrap();
// closures
let cache = helpers::generate_voter_cache::<Runtime>(&voters);
let voter_index = helpers::voter_index_fn_linear::<Runtime>(&voters);
let target_index = helpers::target_index_fn_linear::<Runtime>(&targets);
let stake_of = helpers::stake_of_fn::<Runtime>(&voters, &cache);
let score = {
let staked = assignment_ratio_to_staked_normalized(assignments.clone(), &stake_of).unwrap();
to_supports(&staked).evaluate()
};
let solution =
<SolutionOf<Runtime>>::from_assignment(&assignments, &voter_index, &target_index).unwrap();
let round = MultiPhase::round();
RawSolution { solution, score, round }
}
pub fn witness() -> SolutionOrSnapshotSize {
MultiPhase::snapshot()
.map(|snap| SolutionOrSnapshotSize {
voters: snap.voters.len() as u32,
targets: snap.targets.len() as u32,
})
.unwrap_or_default()
}
#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)]
impl frame_system::Config for Runtime {
type SS58Prefix = ();
type BaseCallFilter = frame_support::traits::Everything;
type RuntimeOrigin = RuntimeOrigin;
type Nonce = u64;
type RuntimeCall = RuntimeCall;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = AccountId;
type Lookup = IdentityLookup<Self::AccountId>;
type Block = Block;
type RuntimeEvent = RuntimeEvent;
type BlockHashCount = ();
type DbWeight = ();
type BlockLength = ();
type BlockWeights = BlockWeights;
type Version = ();
type PalletInfo = PalletInfo;
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type OnSetCode = ();
type MaxConsumers = ConstU32<16>;
}
const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75);
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
pub BlockWeights: frame_system::limits::BlockWeights = frame_system::limits::BlockWeights
::with_sensible_defaults(
Weight::from_parts(2u64 * constants::WEIGHT_REF_TIME_PER_SECOND, u64::MAX),
NORMAL_DISPATCH_RATIO,
);
}
impl pallet_balances::Config for Runtime {
type Balance = Balance;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type MaxLocks = ();
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type WeightInfo = ();
type FreezeIdentifier = ();
type MaxFreezes = ();
type RuntimeHoldReason = ();
type RuntimeFreezeReason = ();
type MaxHolds = ();
}
#[derive(Default, Eq, PartialEq, Debug, Clone, Copy)]
pub enum MockedWeightInfo {
#[default]
Basic,
Complex,
Real,
}
parameter_types! {
pub static Targets: Vec<AccountId> = vec![10, 20, 30, 40];
pub static Voters: Vec<VoterOf<Runtime>> = 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;
// `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<AccountId, SolutionAccuracyOf<Runtime>, Balancing>;
type DataProvider = StakingMock;
type WeightInfo = ();
type MaxWinners = MaxWinners;
type Bounds = OnChainElectionsBounds;
}
pub struct MockFallback;
impl ElectionProviderBase for MockFallback {
type BlockNumber = BlockNumber;
type AccountId = AccountId;
type Error = &'static str;
type DataProvider = StakingMock;
type MaxWinners = MaxWinners;
}
impl InstantElectionProvider for MockFallback {
fn instant_elect(
voters_bounds: DataProviderBounds,
targets_bounds: DataProviderBounds,
) -> Result<BoundedSupportsOf<Self>, Self::Error> {
if OnChainFallback::get() {
onchain::OnChainExecution::<OnChainSeqPhragmen>::instant_elect(
voters_bounds,
targets_bounds,
)
.map_err(|_| "onchain::OnChainExecution failed.")
} else {
Err("NoFallback.")
}
}
}
parameter_types! {
pub static Balancing: Option<BalancingConfig> = 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 = <StakingMock as ElectionDataProvider>::MaxVotesPerVoter;
type MaxWinners = MaxWinners;
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 = frame_support::traits::ConstU32<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 =
frame_election_provider_support::onchain::OnChainExecution<OnChainSeqPhragmen>;
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
type MaxWinners = MaxWinners;
type MinerConfig = Self;
type Solver = SequentialPhragmen<AccountId, SolutionAccuracyOf<Runtime>, Balancing>;
type ElectionBounds = ElectionsBounds;
}
impl Convert<usize, BalanceOf<Runtime>> 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::<Balance, SignedFixedDeposit, SignedDepositIncreaseFactor>::convert(queue_len)
}
}
}
impl<LocalCall> frame_system::offchain::SendTransactionTypes<LocalCall> for Runtime
where
RuntimeCall: From<LocalCall>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = Extrinsic;
}
pub type Extrinsic = sp_runtime::testing::TestXt<RuntimeCall, ()>;
parameter_types! {
pub MaxNominations: u32 = <TestNposSolution as NposSolution>::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) -> data_provider::Result<Vec<AccountId>> {
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) -> data_provider::Result<Vec<VoterOf<Runtime>>> {
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<u32> {
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<VoterOf<Runtime>>,
targets: Vec<AccountId>,
_target_stake: Option<VoteWeight>,
) {
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: frame_support::BoundedVec<AccountId, Self::MaxVotesPerVoter>,
) {
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 {
<MinerTxPriority>::set(p);
self
}
pub fn better_signed_threshold(self, p: Perbill) -> Self {
<BetterSignedThreshold>::set(p);
self
}
pub fn phases(self, signed: BlockNumber, unsigned: BlockNumber) -> Self {
<SignedPhase>::set(signed);
<UnsignedPhase>::set(unsigned);
self
}
pub fn onchain_fallback(self, onchain: bool) -> Self {
<OnChainFallback>::set(onchain);
self
}
pub fn miner_weight(self, weight: Weight) -> Self {
<MinerMaxWeight>::set(weight);
self
}
pub fn mock_weight_info(self, mock: MockedWeightInfo) -> Self {
<MockWeightInfo>::set(mock);
self
}
pub fn desired_targets(self, t: u32) -> Self {
<DesiredTargets>::set(t);
self
}
pub fn add_voter(
self,
who: AccountId,
stake: Balance,
targets: BoundedVec<AccountId, MaxNominations>,
) -> Self {
VOTERS.with(|v| v.borrow_mut().push((who, stake, targets)));
self
}
pub fn signed_max_submission(self, count: u32) -> Self {
<SignedMaxSubmissions>::set(count);
self
}
pub fn signed_base_deposit(self, base: u64, variable: bool, increase: Percent) -> Self {
<EnableVariableDepositBase>::set(variable);
<SignedFixedDeposit>::set(base);
<SignedDepositIncreaseFactor>::set(increase);
self
}
pub fn signed_deposit(self, base: u64, byte: u64, weight: u64) -> Self {
<SignedFixedDeposit>::set(base);
<SignedDepositByte>::set(byte);
<SignedDepositWeight>::set(weight);
self
}
pub fn signed_weight(self, weight: Weight) -> Self {
<SignedMaxWeight>::set(weight);
self
}
pub fn build(self) -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
let mut storage =
frame_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
let _ = pallet_balances::GenesisConfig::<Runtime> {
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),
],
}
.assimilate_storage(&mut storage);
sp_io::TestExternalities::from(storage)
}
pub fn build_offchainify(
self,
iters: u32,
) -> (sp_io::TestExternalities, Arc<RwLock<PoolState>>) {
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() -> ()) {
sp_tracing::try_init_simple();
let mut ext = self.build();
ext.execute_with(test);
#[cfg(feature = "try-runtime")]
ext.execute_with(|| {
assert_ok!(<MultiPhase as frame_support::traits::Hooks<u64>>::try_state(
System::block_number()
));
});
}
}
pub(crate) fn balances(who: &AccountId) -> (Balance, Balance) {
(Balances::free_balance(who), Balances::reserved_balance(who))
}