diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b2efcb196787d..310de20bb8f8b 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -713,6 +713,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type MaxElectingVoters = MaxElectingVoters; type BenchmarkingConfig = ElectionProviderBenchmarkConfig; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; + type EmergencyDepositMultiplier = ConstU32<2>; } parameter_types! { diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index e1d3cb8ed5dee..19c4d15b08f86 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -69,8 +69,8 @@ //! Upon the end of the signed phase, the solutions are examined from best to worse (i.e. `pop()`ed //! until drained). Each solution undergoes an expensive `Pallet::feasibility_check`, which ensures //! the score claimed by this score was correct, and it is valid based on the election data (i.e. -//! votes and targets). At each step, if the current best solution passes the feasibility check, -//! it is considered to be the best one. The sender of the origin is rewarded, and the rest of the +//! votes and targets). At each step, if the current best solution passes the feasibility check, it +//! is considered to be the best one. The sender of the origin is rewarded, and the rest of the //! queued solutions get their deposit back and are discarded, without being checked. //! //! The following example covers all of the cases at the end of the signed phase: @@ -126,12 +126,12 @@ //! 2. Any other unforeseen internal error //! //! A call to `T::ElectionProvider::elect` is made, and `Ok(_)` cannot be returned, then the pallet -//! proceeds to the [`Phase::Emergency`]. During this phase, any solution can be submitted from -//! [`Config::ForceOrigin`], without any checking, via [`Pallet::set_emergency_election_result`] -//! transaction. Hence, `[`Config::ForceOrigin`]` should only be set to a trusted origin, such as -//! the council or root. Once submitted, the forced solution is kept in [`QueuedSolution`] until the -//! next call to `T::ElectionProvider::elect`, where it is returned and [`Phase`] goes back to -//! `Off`. +//! proceeds to the [`Phase::Emergency`]. During this phase, there are two possibilities. One of +//! them is to use the [`Pallet::set_emergency_election_result`] function that submits a solution +//! without any checking. It only accepts solutions from `[`Config::ForceOrigin`]`, hence it should +//! only be set to a trusted origin, such as the council or root. Once submitted, the forced +//! solution is kept in [`QueuedSolution`] until the next call to `T::ElectionProvider::elect`, +//! where it is returned and [`Phase`] goes back to `Off`. //! //! This implies that the user of this pallet (i.e. a staking pallet) should re-try calling //! `T::ElectionProvider::elect` in case of error, until `OK(_)` is returned. @@ -151,6 +151,13 @@ //! //! See the `staking-miner` documentation in the Polkadot repository for more information. //! +//! The other option is to use the `Pallet::submit_emergency_solution` function which allows you to +//! submit signed solutions during the emergency phase. The only difference between this function +//! and the [`Pallet::submit`] function is that in [`Pallet::submit_emergency_solution`] the +//! submissions are checked on the fly. Good solutions will get rewarded, but bad submissions will +//! be highly punished. The deposit needed for submitting during the emergency phase is higher +//! compared to the one in the regular submission. +//! //! ## Feasible Solution (correct solution) //! //! All submissions must undergo a feasibility check. Signed solutions are checked one by one at the @@ -172,8 +179,8 @@ //! //! ## Error types //! -//! This pallet provides a verbose error system to ease future debugging and debugging. The overall -//! hierarchy of errors is as follows: +//! This pallet provides a verbose error system to ease future debugging. The overall hierarchy of +//! errors is as follows: //! //! 1. [`pallet::Error`]: These are the errors that can be returned in the dispatchables of the //! pallet, either signed or unsigned. Since decomposition with nested enums is not possible @@ -705,6 +712,10 @@ pub mod pallet { /// The weight of the pallet. type WeightInfo: WeightInfo; + + /// The multiple of regular deposit needed for signed phase during emergency phase. + #[pallet::constant] + type EmergencyDepositMultiplier: Get; } #[pallet::hooks] @@ -959,7 +970,7 @@ pub mod pallet { /// Submit a solution for the signed phase. /// - /// The dispatch origin fo this call must be __signed__. + /// The dispatch origin for this call must be __signed__. /// /// The solution is potentially queued, based on the claimed score and processed at the end /// of the signed phase. @@ -1070,6 +1081,59 @@ pub mod pallet { >::put(solution); Ok(()) } + + /// Submit a signed solution during the emergency phase. It will go through the + /// `feasibility_check` right away. + /// + /// The dispatch origin for this call must be __signed__. + /// + /// The deposit that is reserved might be rewarded or slashed based on the outcome. + #[pallet::weight( + T::MinerConfig::solution_weight( + T::BenchmarkingConfig::SNAPSHOT_MAXIMUM_VOTERS, + T::BenchmarkingConfig::MAXIMUM_TARGETS, + T::BenchmarkingConfig::ACTIVE_VOTERS[1], + T::BenchmarkingConfig::DESIRED_TARGETS[1] + ) + .saturating_add(T::DbWeight::get().reads_writes(1, 1)) + )] + pub fn submit_emergency_solution( + origin: OriginFor, + raw_solution: Box>>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(Self::current_phase().is_emergency(), >::CallNotAllowed); + let size = Self::snapshot_metadata().ok_or(Error::::MissingSnapshotMetadata)?; + + ensure!( + Self::solution_weight_of(&raw_solution, size) < T::SignedMaxWeight::get(), + Error::::SignedTooMuchWeight + ); + + let deposit = Self::deposit_for_emergency(&raw_solution, size); + T::Currency::reserve(&who, deposit).map_err(|_| Error::::SignedCannotPayDeposit)?; + + let call_fee = { + let call = Call::submit { raw_solution: raw_solution.clone() }; + T::EstimateCallFee::estimate_call_fee(&call, None.into()) + }; + + match Self::feasibility_check(*raw_solution, ElectionCompute::Signed) { + Ok(ready_solution) => { + Self::finalize_signed_phase_accept_solution( + ready_solution, + &who, + deposit, + call_fee, + ); + }, + Err(_) => { + Self::finalize_signed_phase_reject_solution(&who, deposit); + }, + } + + Ok(()) + } } #[pallet::event] @@ -2017,6 +2081,77 @@ mod tests { }) } + #[test] + fn accepts_good_solution_during_emergency() { + ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { + roll_to(25); + assert_eq!(MultiPhase::current_phase(), Phase::Unsigned((true, 25))); + + // No solutions are submitted so the queue should be empty + assert!(MultiPhase::queued_solution().is_none()); + assert_eq!(MultiPhase::elect().unwrap_err(), ElectionError::Fallback("NoFallback.")); + + assert!(MultiPhase::current_phase().is_emergency()); + + let solution = crate::mock::raw_solution(); + let origin = crate::mock::Origin::signed(99); + + assert_ok!(MultiPhase::submit_emergency_solution(origin, Box::new(solution))); + + // The queued solution should be some now because the submitted solution is correct. + assert!(MultiPhase::queued_solution().is_some()); + + let reward = crate::mock::SignedRewardBase::get(); + + assert_eq!( + multi_phase_events(), + vec![ + Event::SignedPhaseStarted { round: 1 }, + Event::UnsignedPhaseStarted { round: 1 }, + Event::ElectionFinalized { election_compute: None }, + Event::Rewarded { account: 99, value: reward } + ] + ); + }); + } + + #[test] + fn rejects_bad_solution_during_emergency() { + ExtBuilder::default().onchain_fallback(false).build_and_execute(|| { + roll_to(25); + assert_eq!(MultiPhase::current_phase(), Phase::Unsigned((true, 25))); + + // No solutions are submitted so the queue should be empty. + assert!(MultiPhase::queued_solution().is_none()); + assert_eq!(MultiPhase::elect().unwrap_err(), ElectionError::Fallback("NoFallback.")); + + assert!(MultiPhase::current_phase().is_emergency()); + + let mut solution = crate::mock::raw_solution(); + // modifying the solution, so that it becomes incorrect. + solution.round += 1; + let origin = crate::mock::Origin::signed(99); + + assert_ok!(MultiPhase::submit_emergency_solution(origin, Box::new(solution.clone()))); + + // The queued solution should be none now because the submitted solution is incorrect. + assert!(MultiPhase::queued_solution().is_none()); + + let size = MultiPhase::snapshot_metadata().unwrap(); + let deposit = MultiPhase::deposit_for_emergency(&solution, size); + + assert_eq!( + multi_phase_events(), + vec![ + Event::SignedPhaseStarted { round: 1 }, + Event::UnsignedPhaseStarted { round: 1 }, + Event::ElectionFinalized { election_compute: None }, + Event::Slashed { account: 99, value: deposit } + ] + ); + }); + } + #[test] fn fallback_strategy_works() { ExtBuilder::default().onchain_fallback(true).build_and_execute(|| { diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index 7eff70b47eba5..562c6607eb88d 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -281,6 +281,7 @@ parameter_types! { pub static MockWeightInfo: MockedWeightInfo = MockedWeightInfo::Real; pub static MaxElectingVoters: VoterIndex = u32::max_value(); pub static MaxElectableTargets: TargetIndex = TargetIndex::max_value(); + pub static EmergencyDepositMultiplier: u32 = 2; pub static EpochLength: u64 = 30; pub static OnChainFallback: bool = true; @@ -387,6 +388,7 @@ impl crate::Config for Runtime { type MaxElectableTargets = MaxElectableTargets; type MinerConfig = Self; type Solver = SequentialPhragmen, Balancing>; + type EmergencyDepositMultiplier = EmergencyDepositMultiplier; } impl frame_system::offchain::SendTransactionTypes for Runtime diff --git a/frame/election-provider-multi-phase/src/signed.rs b/frame/election-provider-multi-phase/src/signed.rs index eca75139f925a..21e74781fa678 100644 --- a/frame/election-provider-multi-phase/src/signed.rs +++ b/frame/election-provider-multi-phase/src/signed.rs @@ -521,6 +521,15 @@ impl Pallet { .saturating_add(len_deposit) .saturating_add(weight_deposit) } + + /// Similar as `deposit_for`, but returns the result for a emergency solution. + pub fn deposit_for_emergency( + raw_solution: &RawSolution>, + size: SolutionOrSnapshotSize, + ) -> BalanceOf { + BalanceOf::::from(T::EmergencyDepositMultiplier::get()) + .saturating_mul(Self::deposit_for(raw_solution, size)) + } } #[cfg(test)]