diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index a570329dcfce3..92f3d43901a97 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -554,6 +554,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type CompactSolution = NposCompactSolution16; type Fallback = Fallback; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; + type ForceOrigin = EnsureRootOrHalfCouncil; type BenchmarkingConfig = (); } diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index d1de16f7f744f..9bec5cc4bd310 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -502,6 +502,8 @@ pub enum FeasibilityError { InvalidScore, /// The provided round is incorrect. InvalidRound, + /// Comparison against `MinimumUntrustedScore` failed. + UntrustedScoreTooLow, } impl From for FeasibilityError { @@ -579,6 +581,9 @@ pub mod pallet { /// Configuration for the fallback type Fallback: Get; + /// Origin that can set the minimum score. + type ForceOrigin: EnsureOrigin; + /// The configuration of benchmarking. type BenchmarkingConfig: BenchmarkingConfig; @@ -773,6 +778,21 @@ pub mod pallet { Ok(None.into()) } + + /// Set a new value for `MinimumUntrustedScore`. + /// + /// Dispatch origin must be aligned with `T::ForceOrigin`. + /// + /// This check can be turned off by setting the value to `None`. + #[pallet::weight(T::DbWeight::get().writes(1))] + fn set_minimum_untrusted_score( + origin: OriginFor, + maybe_next_score: Option, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + >::set(maybe_next_score); + Ok(()) + } } #[pallet::event] @@ -909,6 +929,14 @@ pub mod pallet { #[pallet::getter(fn snapshot_metadata)] pub type SnapshotMetadata = StorageValue<_, SolutionOrSnapshotSize>; + /// The minimum score that each 'untrusted' solution must attain in order to be considered + /// feasible. + /// + /// Can be set via `set_minimum_untrusted_score`. + #[pallet::storage] + #[pallet::getter(fn minimum_untrusted_score)] + pub type MinimumUntrustedScore = StorageValue<_, ElectionScore>; + #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] pub struct Pallet(PhantomData); @@ -959,7 +987,7 @@ impl Pallet { /// /// Returns `Ok(snapshot_weight)` if success, where `snapshot_weight` is the weight that /// needs to recorded for the creation of snapshot. - pub(crate) fn on_initialize_open_signed() -> Result { + pub fn on_initialize_open_signed() -> Result { let weight = Self::create_snapshot()?; >::put(Phase::Signed); Self::deposit_event(Event::SignedPhaseStarted(Self::round())); @@ -972,7 +1000,7 @@ impl Pallet { /// /// Returns `Ok(snapshot_weight)` if success, where `snapshot_weight` is the weight that /// needs to recorded for the creation of snapshot. - pub(crate) fn on_initialize_open_unsigned( + pub fn on_initialize_open_unsigned( need_snapshot: bool, enabled: bool, now: T::BlockNumber, @@ -997,7 +1025,7 @@ impl Pallet { /// 3. [`DesiredTargets`] /// /// Returns `Ok(consumed_weight)` if operation is okay. - pub(crate) fn create_snapshot() -> Result { + pub fn create_snapshot() -> Result { let target_limit = >::max_value().saturated_into::(); let voter_limit = >::max_value().saturated_into::(); @@ -1052,6 +1080,15 @@ impl Pallet { // upon arrival, thus we would then remove it here. Given overlay it is cheap anyhow ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount); + // ensure that the solution's score can pass absolute min-score. + let submitted_score = solution.score.clone(); + ensure!( + Self::minimum_untrusted_score().map_or(true, |min_score| + sp_npos_elections::is_score_better(submitted_score, min_score, Perbill::zero()) + ), + FeasibilityError::UntrustedScoreTooLow + ); + // read the entire snapshot. let RoundSnapshot { voters: snapshot_voters, targets: snapshot_targets } = Self::snapshot().ok_or(FeasibilityError::SnapshotUnavailable)?; @@ -1596,6 +1633,31 @@ mod tests { }) } + #[test] + fn untrusted_score_verification_is_respected() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert_eq!(MultiPhase::current_phase(), Phase::Signed); + + + let (solution, _) = MultiPhase::mine_solution(2).unwrap(); + // default solution has a score of [50, 100, 5000]. + assert_eq!(solution.score, [50, 100, 5000]); + + >::put([49, 0, 0]); + assert_ok!(MultiPhase::feasibility_check(solution.clone(), ElectionCompute::Signed)); + + >::put([51, 0, 0]); + assert_noop!( + MultiPhase::feasibility_check( + solution, + ElectionCompute::Signed + ), + FeasibilityError::UntrustedScoreTooLow, + ); + }) + } + #[test] fn number_of_voters_allowed_2sec_block() { // Just a rough estimate with the substrate weights. diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index f57836178d497..2fb7927d98f91 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -345,6 +345,7 @@ impl crate::Config for Runtime { type BenchmarkingConfig = (); type OnChainAccuracy = Perbill; type Fallback = Fallback; + type ForceOrigin = frame_system::EnsureRoot; type CompactSolution = TestCompact; } diff --git a/frame/election-provider-multi-phase/src/unsigned.rs b/frame/election-provider-multi-phase/src/unsigned.rs index ef1cdfd5a71c4..78726c542078c 100644 --- a/frame/election-provider-multi-phase/src/unsigned.rs +++ b/frame/election-provider-multi-phase/src/unsigned.rs @@ -199,7 +199,7 @@ impl Pallet { } /// Mine a new solution as a call. Performs all checks. - fn mine_checked_call() -> Result, MinerError> { + pub fn mine_checked_call() -> Result, MinerError> { let iters = Self::get_balancing_iters(); // get the solution, with a load of checks to ensure if submitted, IT IS ABSOLUTELY VALID. let (raw_solution, witness) = Self::mine_and_check(iters)?; @@ -227,7 +227,7 @@ impl Pallet { // perform basic checks of a solution's validity // // Performance: note that it internally clones the provided solution. - fn basic_checks( + pub fn basic_checks( raw_solution: &RawSolution>, solution_type: &str, ) -> Result<(), MinerError> { @@ -404,7 +404,7 @@ impl Pallet { /// /// Indeed, the score must be computed **after** this step. If this step reduces the score too /// much or remove a winner, then the solution must be discarded **after** this step. - fn trim_assignments_weight( + pub fn trim_assignments_weight( desired_targets: u32, size: SolutionOrSnapshotSize, max_weight: Weight, @@ -438,7 +438,7 @@ impl Pallet { /// /// The score must be computed **after** this step. If this step reduces the score too much, /// then the solution must be discarded. - pub(crate) fn trim_assignments_length( + pub fn trim_assignments_length( max_allowed_length: u32, assignments: &mut Vec>, encoded_size_of: impl Fn(&[IndexAssignmentOf]) -> Result, @@ -579,7 +579,7 @@ impl Pallet { /// /// Returns `Ok(())` if offchain worker limit is respected, `Err(reason)` otherwise. If `Ok()` /// is returned, `now` is written in storage and will be used in further calls as the baseline. - pub(crate) fn ensure_offchain_repeat_frequency(now: T::BlockNumber) -> Result<(), MinerError> { + pub fn ensure_offchain_repeat_frequency(now: T::BlockNumber) -> Result<(), MinerError> { let threshold = T::OffchainRepeat::get(); let last_block = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK); @@ -619,7 +619,7 @@ impl Pallet { /// /// NOTE: Ideally, these tests should move more and more outside of this and more to the miner's /// code, so that we do less and less storage reads here. - pub(crate) fn unsigned_pre_dispatch_checks( + pub fn unsigned_pre_dispatch_checks( solution: &RawSolution>, ) -> DispatchResult { // ensure solution is timely. Don't panic yet. This is a cheap check. diff --git a/frame/merkle-mountain-range/src/lib.rs b/frame/merkle-mountain-range/src/lib.rs index 6992341f6bbd1..a8e707c7ac4e0 100644 --- a/frame/merkle-mountain-range/src/lib.rs +++ b/frame/merkle-mountain-range/src/lib.rs @@ -154,7 +154,7 @@ decl_storage! { decl_module! { /// A public part of the pallet. pub struct Module, I: Instance = DefaultInstance> for enum Call where origin: T::Origin { - fn on_initialize(n: T::BlockNumber) -> Weight { + fn on_initialize(_n: T::BlockNumber) -> Weight { use primitives::LeafDataProvider; let leaves = Self::mmr_leaves(); let peaks_before = mmr::utils::NodesUtils::new(leaves).number_of_peaks();