diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 2ce19483e5539e972fe5a345cd51994e570688c4..fd7fd4213366f74b752fdd0ba5a3da348747519c 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -73,6 +73,7 @@ use pallet_session::{historical as pallet_session_historical}; use sp_inherents::{InherentData, CheckInherentsResult}; use static_assertions::const_assert; use pallet_contracts::weights::WeightInfo; +use pallet_election_provider_multi_phase::FallbackStrategy; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; @@ -516,9 +517,14 @@ parameter_types! { pub const SignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4; pub const UnsignedPhase: u32 = EPOCH_DURATION_IN_BLOCKS / 4; - // fallback: no need to do on-chain phragmen initially. - pub const Fallback: pallet_election_provider_multi_phase::FallbackStrategy = - pallet_election_provider_multi_phase::FallbackStrategy::Nothing; + // signed config + pub const SignedMaxSubmissions: u32 = 10; + pub const SignedRewardBase: Balance = 1 * DOLLARS; + pub const SignedDepositBase: Balance = 1 * DOLLARS; + pub const SignedDepositByte: Balance = 1 * CENTS; + + // fallback: no on-chain fallback. + pub const Fallback: FallbackStrategy = FallbackStrategy::Nothing; pub SolutionImprovementThreshold: Perbill = Perbill::from_rational(1u32, 10_000); @@ -559,6 +565,14 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type MinerMaxWeight = MinerMaxWeight; type MinerMaxLength = MinerMaxLength; type MinerTxPriority = MultiPhaseUnsignedPriority; + type SignedMaxSubmissions = SignedMaxSubmissions; + type SignedRewardBase = SignedRewardBase; + type SignedDepositBase = SignedDepositBase; + type SignedDepositByte = SignedDepositByte; + type SignedDepositWeight = (); + type SignedMaxWeight = MinerMaxWeight; + type SlashHandler = (); // burn slashes + type RewardHandler = (); // nothing to do upon rewards type DataProvider = Staking; type OnChainAccuracy = Perbill; type CompactSolution = NposCompactSolution16; @@ -1556,6 +1570,7 @@ impl_runtime_apis! { add_benchmark!(params, batches, pallet_uniques, Uniques); add_benchmark!(params, batches, pallet_utility, Utility); add_benchmark!(params, batches, pallet_vesting, Vesting); + add_benchmark!(params, batches, pallet_election_provider_multi_phase, ElectionProviderMultiPhase); if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } Ok(batches) diff --git a/substrate/frame/election-provider-multi-phase/src/benchmarking.rs b/substrate/frame/election-provider-multi-phase/src/benchmarking.rs index 4eade8e184e75e60a8726a8b0d360a34abbe8855..7988163e98f65bad603ad3c3c70dc82b48f43e47 100644 --- a/substrate/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/substrate/frame/election-provider-multi-phase/src/benchmarking.rs @@ -19,7 +19,7 @@ use super::*; use crate::{Pallet as MultiPhase, unsigned::IndexAssignmentOf}; -use frame_benchmarking::impl_benchmark_test_suite; +use frame_benchmarking::{account, impl_benchmark_test_suite}; use frame_support::{assert_ok, traits::OnInitialize}; use frame_system::RawOrigin; use rand::{prelude::SliceRandom, rngs::SmallRng, SeedableRng}; @@ -57,7 +57,7 @@ fn solution_with_size<T: Config>( let targets: Vec<T::AccountId> = (0..size.targets).map(|i| frame_benchmarking::account("Targets", i, SEED)).collect(); - let mut rng = SmallRng::seed_from_u64(SEED as u64); + let mut rng = SmallRng::seed_from_u64(SEED.into()); // decide who are the winners. let winners = targets @@ -176,6 +176,39 @@ frame_benchmarking::benchmarks! { assert!(<MultiPhase<T>>::current_phase().is_unsigned()); } + finalize_signed_phase_accept_solution { + let receiver = account("receiver", 0, SEED); + let initial_balance = T::Currency::minimum_balance() * 10u32.into(); + T::Currency::make_free_balance_be(&receiver, initial_balance); + let ready: ReadySolution<T::AccountId> = Default::default(); + let deposit: BalanceOf<T> = 10u32.into(); + let reward: BalanceOf<T> = 20u32.into(); + + assert_ok!(T::Currency::reserve(&receiver, deposit)); + assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + }: { + <MultiPhase<T>>::finalize_signed_phase_accept_solution(ready, &receiver, deposit, reward) + } verify { + assert_eq!(T::Currency::free_balance(&receiver), initial_balance + 20u32.into()); + assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into()); + } + + finalize_signed_phase_reject_solution { + let receiver = account("receiver", 0, SEED); + let initial_balance = T::Currency::minimum_balance().max(One::one()) * 10u32.into(); + let deposit: BalanceOf<T> = 10u32.into(); + T::Currency::make_free_balance_be(&receiver, initial_balance); + assert_ok!(T::Currency::reserve(&receiver, deposit)); + + assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + assert_eq!(T::Currency::reserved_balance(&receiver), 10u32.into()); + }: { + <MultiPhase<T>>::finalize_signed_phase_reject_solution(&receiver, deposit) + } verify { + assert_eq!(T::Currency::free_balance(&receiver), initial_balance - 10u32.into()); + assert_eq!(T::Currency::reserved_balance(&receiver), 0u32.into()); + } + on_initialize_open_unsigned_without_snapshot { // need to assume signed phase was open before <MultiPhase<T>>::on_initialize_open_signed().unwrap(); @@ -227,6 +260,38 @@ frame_benchmarking::benchmarks! { assert!(<MultiPhase<T>>::snapshot().is_some()); } + submit { + let c in 1 .. (T::SignedMaxSubmissions::get() - 1); + + // the solution will be worse than all of them meaning the score need to be checked against + // ~ log2(c) + let solution = RawSolution { + score: [(10_000_000u128 - 1).into(), 0, 0], + ..Default::default() + }; + + MultiPhase::<T>::on_initialize_open_signed().expect("should be ok to start signed phase"); + <Round<T>>::put(1); + + let mut signed_submissions = SignedSubmissions::<T>::get(); + for i in 0..c { + let solution = RawSolution { + score: [(10_000_000 + i).into(), 0, 0], + ..Default::default() + }; + let signed_submission = SignedSubmission { solution, ..Default::default() }; + signed_submissions.insert(signed_submission); + } + signed_submissions.put(); + + let caller = frame_benchmarking::whitelisted_caller(); + T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance() * 10u32.into()); + + }: _(RawOrigin::Signed(caller), solution, c) + verify { + assert!(<MultiPhase<T>>::signed_submissions().len() as u32 == c + 1); + } + submit_unsigned { // number of votes in snapshot. let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; @@ -234,9 +299,12 @@ frame_benchmarking::benchmarks! { let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. - let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; + let a in + (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. - let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; + let d in + (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. + T::BenchmarkingConfig::DESIRED_TARGETS[1]; let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::<T>(witness, a, d); @@ -249,7 +317,8 @@ frame_benchmarking::benchmarks! { let encoded_call = <Call<T>>::submit_unsigned(raw_solution.clone(), witness).encode(); }: { assert_ok!(<MultiPhase<T>>::submit_unsigned(RawOrigin::None.into(), raw_solution, witness)); - let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot).unwrap(); + let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot) + .unwrap(); let _decoded_call = <Call<T> as Decode>::decode(&mut &*encoded_call).unwrap(); } verify { assert!(<MultiPhase<T>>::queued_solution().is_some()); @@ -263,13 +332,17 @@ frame_benchmarking::benchmarks! { let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. - let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; + let a in + (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. - let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; + let d in + (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. + T::BenchmarkingConfig::DESIRED_TARGETS[1]; // Subtract this percentage from the actual encoded size let f in 0 .. 95; - // Compute a random solution, then work backwards to get the lists of voters, targets, and assignments + // Compute a random solution, then work backwards to get the lists of voters, targets, and + // assignments let witness = SolutionOrSnapshotSize { voters: v, targets: t }; let RawSolution { compact, .. } = solution_with_size::<T>(witness, a, d); let RoundSnapshot { voters, targets } = MultiPhase::<T>::snapshot().unwrap(); @@ -313,7 +386,11 @@ frame_benchmarking::benchmarks! { } verify { let compact = CompactOf::<T>::try_from(index_assignments.as_slice()).unwrap(); let encoding = compact.encode(); - log!(trace, "encoded size prediction = {}", encoded_size_of(index_assignments.as_slice()).unwrap()); + log!( + trace, + "encoded size prediction = {}", + encoded_size_of(index_assignments.as_slice()).unwrap(), + ); log!(trace, "actual encoded size = {}", encoding.len()); assert!(encoding.len() <= desired_size); } @@ -326,9 +403,12 @@ frame_benchmarking::benchmarks! { let t in (T::BenchmarkingConfig::TARGETS[0]) .. T::BenchmarkingConfig::TARGETS[1]; // number of assignments, i.e. compact.len(). This means the active nominators, thus must be // a subset of `v` component. - let a in (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; + let a in + (T::BenchmarkingConfig::ACTIVE_VOTERS[0]) .. T::BenchmarkingConfig::ACTIVE_VOTERS[1]; // number of desired targets. Must be a subset of `t` component. - let d in (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. T::BenchmarkingConfig::DESIRED_TARGETS[1]; + let d in + (T::BenchmarkingConfig::DESIRED_TARGETS[0]) .. + T::BenchmarkingConfig::DESIRED_TARGETS[1]; let size = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::<T>(size, a, d); @@ -340,7 +420,8 @@ frame_benchmarking::benchmarks! { let encoded_snapshot = <MultiPhase<T>>::snapshot().unwrap().encode(); }: { assert_ok!(<MultiPhase<T>>::feasibility_check(raw_solution, ElectionCompute::Unsigned)); - let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot).unwrap(); + let _decoded_snap = <RoundSnapshot<T::AccountId> as Decode>::decode(&mut &*encoded_snapshot) + .unwrap(); } } diff --git a/substrate/frame/election-provider-multi-phase/src/helpers.rs b/substrate/frame/election-provider-multi-phase/src/helpers.rs index bf5b360499cb404ab92c04fecfd9e6efafa641d7..46eeef0a6bf73800e6db878d48f295a59483d108 100644 --- a/substrate/frame/election-provider-multi-phase/src/helpers.rs +++ b/substrate/frame/election-provider-multi-phase/src/helpers.rs @@ -47,13 +47,13 @@ pub fn generate_voter_cache<T: Config>( cache } -/// Create a function the returns the index a voter in the snapshot. +/// Create a function that returns the index of a voter in the snapshot. /// /// The returning index type is the same as the one defined in `T::CompactSolution::Voter`. /// /// ## Warning /// -/// The snapshot must be the same is the one used to create `cache`. +/// Note that this will represent the snapshot data from which the `cache` is generated. pub fn voter_index_fn<T: Config>( cache: &BTreeMap<T::AccountId, usize>, ) -> impl Fn(&T::AccountId) -> Option<CompactVoterIndexOf<T>> + '_ { @@ -78,7 +78,7 @@ pub fn voter_index_fn_owned<T: Config>( /// /// ## Warning /// -/// The snapshot must be the same is the one used to create `cache`. +/// Note that this will represent the snapshot data from which the `cache` is generated. pub fn voter_index_fn_usize<T: Config>( cache: &BTreeMap<T::AccountId, usize>, ) -> impl Fn(&T::AccountId) -> Option<usize> + '_ { @@ -103,7 +103,7 @@ pub fn voter_index_fn_linear<T: Config>( } } -/// Create a function the returns the index to a target in the snapshot. +/// Create a function that returns the index of a target in the snapshot. /// /// The returned index type is the same as the one defined in `T::CompactSolution::Target`. /// diff --git a/substrate/frame/election-provider-multi-phase/src/lib.rs b/substrate/frame/election-provider-multi-phase/src/lib.rs index 2864ca518d0687e5a0c5e55ea050445210146d62..45e04a757f0b38bc11ff03c2663f860871027d71 100644 --- a/substrate/frame/election-provider-multi-phase/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/src/lib.rs @@ -231,7 +231,7 @@ use codec::{Decode, Encode}; use frame_support::{ dispatch::DispatchResultWithPostInfo, ensure, - traits::{Currency, Get, ReservableCurrency}, + traits::{Currency, Get, ReservableCurrency, OnUnbalanced}, weights::Weight, }; use frame_system::{ensure_none, offchain::SendTransactionTypes}; @@ -266,10 +266,14 @@ pub mod helpers; const LOG_TARGET: &'static str = "runtime::election-provider"; +pub mod signed; pub mod unsigned; pub mod weights; -/// The weight declaration of the pallet. +pub use signed::{ + BalanceOf, NegativeImbalanceOf, PositiveImbalanceOf, SignedSubmission, SignedSubmissionOf, + SignedSubmissions, SubmissionIndicesOf, +}; pub use weights::WeightInfo; /// The compact solution type used by this crate. @@ -411,7 +415,7 @@ impl Default for ElectionCompute { /// /// Such a solution should never become effective in anyway before being checked by the /// `Pallet::feasibility_check` -#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)] +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, PartialOrd, Ord)] pub struct RawSolution<C> { /// Compact election edges. pub compact: C, @@ -583,6 +587,44 @@ pub mod pallet { /// this value, based on [`WeightInfo::submit_unsigned`]. type MinerMaxWeight: Get<Weight>; + /// Maximum number of signed submissions that can be queued. + /// + /// It is best to avoid adjusting this during an election, as it impacts downstream data + /// structures. In particular, `SignedSubmissionIndices<T>` is bounded on this value. If you + /// update this value during an election, you _must_ ensure that + /// `SignedSubmissionIndices.len()` is less than or equal to the new value. Otherwise, + /// attempts to submit new solutions may cause a runtime panic. + #[pallet::constant] + type SignedMaxSubmissions: Get<u32>; + + /// Maximum weight of a signed solution. + /// + /// This should probably be similar to [`Config::MinerMaxWeight`]. + #[pallet::constant] + type SignedMaxWeight: Get<Weight>; + + /// Base reward for a signed solution + #[pallet::constant] + type SignedRewardBase: Get<BalanceOf<Self>>; + + /// Base deposit for a signed solution. + #[pallet::constant] + type SignedDepositBase: Get<BalanceOf<Self>>; + + /// Per-byte deposit for a signed solution. + #[pallet::constant] + type SignedDepositByte: Get<BalanceOf<Self>>; + + /// Per-weight deposit for a signed solution. + #[pallet::constant] + type SignedDepositWeight: Get<BalanceOf<Self>>; + + /// Handler for the slashed deposits. + type SlashHandler: OnUnbalanced<NegativeImbalanceOf<Self>>; + + /// Handler for the rewards. + type RewardHandler: OnUnbalanced<PositiveImbalanceOf<Self>>; + /// Maximum length (bytes) that the mined solution should consume. /// /// The miner will ensure that the total length of the unsigned solution will not exceed @@ -599,6 +641,7 @@ pub mod pallet { + Eq + Clone + sp_std::fmt::Debug + + Ord + CompactSolution; /// Accuracy used for fallback on-chain election. @@ -656,11 +699,20 @@ pub mod pallet { Phase::Signed | Phase::Off if remaining <= unsigned_deadline && remaining > Zero::zero() => { - // Determine if followed by signed or not. + // our needs vary according to whether or not the unsigned phase follows a signed phase let (need_snapshot, enabled, signed_weight) = if current_phase == Phase::Signed { - // Followed by a signed phase: close the signed phase, no need for snapshot. - // TODO: proper weight https://github.com/paritytech/substrate/pull/7910. - (false, true, Weight::zero()) + // there was previously a signed phase: close the signed phase, no need for snapshot. + // + // Notes: + // + // - `Self::finalize_signed_phase()` also appears in `fn do_elect`. This is + // a guard against the case that `elect` is called prematurely. This adds + // a small amount of overhead, but that is unfortunately unavoidable. + let (_success, weight) = Self::finalize_signed_phase(); + // In the future we can consider disabling the unsigned phase if the signed + // phase completes successfully, but for now we're enabling it unconditionally + // as a defensive measure. + (false, true, weight) } else { // No signed phase: create a new snapshot, definitely `enable` the unsigned // phase. @@ -807,8 +859,12 @@ pub mod pallet { // Store the newly received solution. log!(info, "queued unsigned solution with score {:?}", ready.score); + let ejected_a_solution = <QueuedSolution<T>>::exists(); <QueuedSolution<T>>::put(ready); - Self::deposit_event(Event::SolutionStored(ElectionCompute::Unsigned)); + Self::deposit_event(Event::SolutionStored( + ElectionCompute::Unsigned, + ejected_a_solution, + )); Ok(None.into()) } @@ -828,6 +884,79 @@ pub mod pallet { Ok(()) } + /// Submit a solution for the signed phase. + /// + /// The dispatch origin fo this call must be __signed__. + /// + /// The solution is potentially queued, based on the claimed score and processed at the end + /// of the signed phase. + /// + /// A deposit is reserved and recorded for the solution. Based on the outcome, the solution + /// might be rewarded, slashed, or get all or a part of the deposit back. + /// + /// # <weight> + /// Queue size must be provided as witness data. + /// # </weight> + #[pallet::weight(T::WeightInfo::submit(*num_signed_submissions))] + pub fn submit( + origin: OriginFor<T>, + solution: RawSolution<CompactOf<T>>, + num_signed_submissions: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + // ensure witness data is correct. + ensure!( + num_signed_submissions >= <SignedSubmissions<T>>::decode_len().unwrap_or_default() as u32, + Error::<T>::SignedInvalidWitness, + ); + + // ensure solution is timely. + ensure!(Self::current_phase().is_signed(), Error::<T>::PreDispatchEarlySubmission); + + // NOTE: this is the only case where having separate snapshot would have been better + // because could do just decode_len. But we can create abstractions to do this. + + // build size. Note: this is not needed for weight calc, thus not input. + // unlikely to ever return an error: if phase is signed, snapshot will exist. + let size = Self::snapshot_metadata().ok_or(Error::<T>::MissingSnapshotMetadata)?; + + ensure!( + Self::feasibility_weight_of(&solution, size) < T::SignedMaxWeight::get(), + Error::<T>::SignedTooMuchWeight, + ); + + // create the submission + let deposit = Self::deposit_for(&solution, size); + let submission = SignedSubmission { who: who.clone(), deposit, solution }; + + // insert the submission if the queue has space or it's better than the weakest + // eject the weakest if the queue was full + let mut signed_submissions = Self::signed_submissions(); + let maybe_removed = match signed_submissions.insert(submission) { + // it's an error if we failed to insert a submission: this indicates the queue was + // full but our solution had insufficient score to eject any solution + signed::InsertResult::NotInserted => return Err(Error::<T>::SignedQueueFull.into()), + signed::InsertResult::Inserted => None, + signed::InsertResult::InsertedEjecting(weakest) => Some(weakest), + }; + + // collect deposit. Thereafter, the function cannot fail. + T::Currency::reserve(&who, deposit) + .map_err(|_| Error::<T>::SignedCannotPayDeposit)?; + + let ejected_a_solution = maybe_removed.is_some(); + // if we had to remove the weakest solution, unreserve its deposit + if let Some(removed) = maybe_removed { + let _remainder = T::Currency::unreserve(&removed.who, removed.deposit); + debug_assert!(_remainder.is_zero()); + } + + signed_submissions.put(); + Self::deposit_event(Event::SolutionStored(ElectionCompute::Signed, ejected_a_solution)); + Ok(()) + } + /// Set a solution in the queue, to be handed out to the client of this pallet in the next /// call to `ElectionProvider::elect`. /// @@ -860,7 +989,9 @@ pub mod pallet { /// /// If the solution is signed, this means that it hasn't yet been processed. If the /// solution is unsigned, this means that it has also been processed. - SolutionStored(ElectionCompute), + /// + /// The `bool` is `true` when a previous solution was ejected to make room for this one. + SolutionStored(ElectionCompute, bool), /// The election has been finalized, with `Some` of the given computation, or else if the /// election failed, `None`. ElectionFinalized(Option<ElectionCompute>), @@ -883,8 +1014,20 @@ pub mod pallet { PreDispatchWrongWinnerCount, /// Submission was too weak, score-wise. PreDispatchWeakSubmission, + /// The queue was full, and the solution was not better than any of the existing ones. + SignedQueueFull, + /// The origin failed to pay the deposit. + SignedCannotPayDeposit, + /// Witness data to dispatchable is invalid. + SignedInvalidWitness, + /// The signed submission consumes too much weight + SignedTooMuchWeight, /// OCW submitted solution for wrong round OcwCallWrongEra, + /// Snapshot metadata should exist but didn't. + MissingSnapshotMetadata, + /// `Self::insert_submission` returned an invalid index. + InvalidSubmissionIndex, /// The call is not allowed at this point. CallNotAllowed, } @@ -988,6 +1131,45 @@ pub mod pallet { #[pallet::getter(fn snapshot_metadata)] pub type SnapshotMetadata<T: Config> = StorageValue<_, SolutionOrSnapshotSize>; + // The following storage items collectively comprise `SignedSubmissions<T>`, and should never be + // accessed independently. Instead, get `Self::signed_submissions()`, modify it as desired, and + // then do `signed_submissions.put()` when you're done with it. + + /// The next index to be assigned to an incoming signed submission. + /// + /// Every accepted submission is assigned a unique index; that index is bound to that particular + /// submission for the duration of the election. On election finalization, the next index is + /// reset to 0. + /// + /// We can't just use `SignedSubmissionIndices.len()`, because that's a bounded set; past its + /// capacity, it will simply saturate. We can't just iterate over `SignedSubmissionsMap`, + /// because iteration is slow. Instead, we store the value here. + #[pallet::storage] + pub(crate) type SignedSubmissionNextIndex<T: Config> = StorageValue<_, u32, ValueQuery>; + + /// A sorted, bounded set of `(score, index)`, where each `index` points to a value in + /// `SignedSubmissions`. + /// + /// We never need to process more than a single signed submission at a time. Signed submissions + /// can be quite large, so we're willing to pay the cost of multiple database accesses to access + /// them one at a time instead of reading and decoding all of them at once. + #[pallet::storage] + pub(crate) type SignedSubmissionIndices<T: Config> = + StorageValue<_, SubmissionIndicesOf<T>, ValueQuery>; + + /// Unchecked, signed solutions. + /// + /// Together with `SubmissionIndices`, this stores a bounded set of `SignedSubmissions` while + /// allowing us to keep only a single one in memory at a time. + /// + /// Twox note: the key of the map is an auto-incrementing index which users cannot inspect or + /// affect; we shouldn't need a cryptographically secure hasher. + #[pallet::storage] + pub(crate) type SignedSubmissionsMap<T: Config> = + StorageMap<_, Twox64Concat, u32, SignedSubmissionOf<T>, ValueQuery>; + + // `SignedSubmissions` items end here. + /// The minimum score that each 'untrusted' solution must attain in order to be considered /// feasible. /// @@ -1223,7 +1405,7 @@ impl<T: Config> Pallet<T> { /// 3. Clear all snapshot data. fn rotate_round() { // Inc round. - <Round<T>>::mutate(|r| *r = *r + 1); + <Round<T>>::mutate(|r| *r += 1); // Phase is off now. <CurrentPhase<T>>::put(Phase::Off); @@ -1242,6 +1424,13 @@ impl<T: Config> Pallet<T> { } fn do_elect() -> Result<(Supports<T::AccountId>, Weight), ElectionError> { + // We have to unconditionally try finalizing the signed phase here. There are only two + // possibilities: + // + // - signed phase was open, in which case this is essential for correct functioning of the system + // - signed phase was complete or not started, in which case finalization is idempotent and + // inexpensive (1 read of an empty vector). + let (_, signed_finalize_weight) = Self::finalize_signed_phase(); <QueuedSolution<T>>::take() .map_or_else( || match T::Fallback::get() { @@ -1261,7 +1450,7 @@ impl<T: Config> Pallet<T> { if Self::round() != 1 { log!(info, "Finalized election round with compute {:?}.", compute); } - (supports, weight) + (supports, weight.saturating_add(signed_finalize_weight)) }) .map_err(|err| { Self::deposit_event(Event::ElectionFinalized(None)); @@ -1309,7 +1498,14 @@ mod feasibility_check { //! more. The best way to audit and review these tests is to try and come up with a solution //! that is invalid, but gets through the system as valid. - use super::{mock::*, *}; + use super::*; + use crate::{ + mock::{ + MultiPhase, Runtime, roll_to, TargetIndex, raw_solution, EpochLength, UnsignedPhase, + SignedPhase, VoterIndex, ExtBuilder, + }, + }; + use frame_support::assert_noop; const COMPUTE: ElectionCompute = ElectionCompute::OnChain; @@ -1476,16 +1672,24 @@ mod feasibility_check { #[cfg(test)] mod tests { - use super::{mock::*, Event, *}; + use super::*; + use crate::{ + Phase, + mock::{ + ExtBuilder, MultiPhase, Runtime, roll_to, MockWeightInfo, AccountId, TargetIndex, + Targets, multi_phase_events, System, SignedMaxSubmissions, + }, + }; use frame_election_provider_support::ElectionProvider; + use frame_support::{assert_noop, assert_ok}; use sp_npos_elections::Support; #[test] fn phase_rotation_works() { ExtBuilder::default().build_and_execute(|| { // 0 ------- 15 ------- 25 ------- 30 ------- ------- 45 ------- 55 ------- 60 - // | | | | - // Signed Unsigned Signed Unsigned + // | | | | | | + // Signed Unsigned Elect Signed Unsigned Elect assert_eq!(System::block_number(), 0); assert_eq!(MultiPhase::current_phase(), Phase::Off); @@ -1644,6 +1848,44 @@ mod tests { assert!(MultiPhase::snapshot_metadata().is_none()); assert!(MultiPhase::desired_targets().is_none()); assert!(MultiPhase::queued_solution().is_none()); + assert!(MultiPhase::signed_submissions().is_empty()); + }) + } + + #[test] + fn early_termination_with_submissions() { + // an early termination in the signed phase, with no queued solution. + ExtBuilder::default().build_and_execute(|| { + // signed phase started at block 15 and will end at 25. + roll_to(14); + assert_eq!(MultiPhase::current_phase(), Phase::Off); + + roll_to(15); + assert_eq!(multi_phase_events(), vec![Event::SignedPhaseStarted(1)]); + assert_eq!(MultiPhase::current_phase(), Phase::Signed); + assert_eq!(MultiPhase::round(), 1); + + // fill the queue with signed submissions + for s in 0..SignedMaxSubmissions::get() { + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(MultiPhase::submit( + crate::mock::Origin::signed(99), + solution, + MultiPhase::signed_submissions().len() as u32 + )); + } + + // an unexpected call to elect. + roll_to(20); + assert!(MultiPhase::elect().is_ok()); + + // all storage items must be cleared. + assert_eq!(MultiPhase::round(), 2); + assert!(MultiPhase::snapshot().is_none()); + assert!(MultiPhase::snapshot_metadata().is_none()); + assert!(MultiPhase::desired_targets().is_none()); + assert!(MultiPhase::queued_solution().is_none()); + assert!(MultiPhase::signed_submissions().is_empty()); }) } diff --git a/substrate/frame/election-provider-multi-phase/src/mock.rs b/substrate/frame/election-provider-multi-phase/src/mock.rs index bd035aaf829698a6680748d3884ef433938e9344..8840e2b935d35f89b6dd5652c046969b0337e090 100644 --- a/substrate/frame/election-provider-multi-phase/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/src/mock.rs @@ -260,8 +260,13 @@ parameter_types! { pub static DesiredTargets: u32 = 2; pub static SignedPhase: u64 = 10; pub static UnsignedPhase: u64 = 5; - pub static MaxSignedSubmissions: u32 = 5; - + pub static SignedMaxSubmissions: u32 = 5; + pub static SignedDepositBase: Balance = 5; + pub static SignedDepositByte: Balance = 0; + pub static SignedDepositWeight: Balance = 0; + pub static SignedRewardBase: Balance = 7; + pub static SignedRewardMax: Balance = 10; + pub static SignedMaxWeight: Weight = BlockWeights::get().max_block; pub static MinerMaxIterations: u32 = 5; pub static MinerTxPriority: u64 = 100; pub static SolutionImprovementThreshold: Perbill = Perbill::zero(); @@ -304,6 +309,27 @@ impl multi_phase::weights::WeightInfo for DualMockWeightInfo { <() as multi_phase::weights::WeightInfo>::on_initialize_open_unsigned_without_snapshot() } } + fn finalize_signed_phase_accept_solution() -> Weight { + if MockWeightInfo::get() { + Zero::zero() + } else { + <() as multi_phase::weights::WeightInfo>::finalize_signed_phase_accept_solution() + } + } + fn finalize_signed_phase_reject_solution() -> Weight { + if MockWeightInfo::get() { + Zero::zero() + } else { + <() as multi_phase::weights::WeightInfo>::finalize_signed_phase_reject_solution() + } + } + fn submit(c: u32) -> Weight { + if MockWeightInfo::get() { + Zero::zero() + } else { + <() as multi_phase::weights::WeightInfo>::submit(c) + } + } fn elect_queued() -> Weight { if MockWeightInfo::get() { Zero::zero() @@ -342,6 +368,14 @@ impl crate::Config for Runtime { type MinerMaxWeight = MinerMaxWeight; type MinerMaxLength = MinerMaxLength; type MinerTxPriority = MinerTxPriority; + type SignedRewardBase = SignedRewardBase; + type SignedDepositBase = SignedDepositBase; + type SignedDepositByte = (); + type SignedDepositWeight = (); + type SignedMaxWeight = SignedMaxWeight; + type SignedMaxSubmissions = SignedMaxSubmissions; + type SlashHandler = (); + type RewardHandler = (); type DataProvider = StakingMock; type WeightInfo = DualMockWeightInfo; type BenchmarkingConfig = (); @@ -440,6 +474,20 @@ impl ExtBuilder { 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_deposit(self, base: u64, byte: u64, weight: u64) -> Self { + <SignedDepositBase>::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 = @@ -481,3 +529,7 @@ impl ExtBuilder { self.build().execute_with(test) } } + +pub(crate) fn balances(who: &u64) -> (u64, u64) { + (Balances::free_balance(who), Balances::reserved_balance(who)) +} diff --git a/substrate/frame/election-provider-multi-phase/src/signed.rs b/substrate/frame/election-provider-multi-phase/src/signed.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba1123c1331ad586b663801c0f87e8f5674ca4db --- /dev/null +++ b/substrate/frame/election-provider-multi-phase/src/signed.rs @@ -0,0 +1,920 @@ +// This file is part of Substrate. + +// Copyright (C) 2020 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 signed phase implementation. + +use crate::{ + CompactOf, Config, ElectionCompute, Pallet, RawSolution, ReadySolution, SolutionOrSnapshotSize, + Weight, WeightInfo, QueuedSolution, SignedSubmissionsMap, SignedSubmissionIndices, + SignedSubmissionNextIndex, +}; +use codec::{Encode, Decode, HasCompact}; +use frame_support::{ + storage::bounded_btree_map::BoundedBTreeMap, + traits::{Currency, Get, OnUnbalanced, ReservableCurrency}, + DebugNoBound, +}; +use sp_arithmetic::traits::SaturatedConversion; +use sp_npos_elections::{is_score_better, CompactSolution, ElectionScore}; +use sp_runtime::{ + RuntimeDebug, + traits::{Saturating, Zero}, +}; +use sp_std::{ + cmp::Ordering, + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + ops::Deref, +}; + +/// A raw, unchecked signed submission. +/// +/// This is just a wrapper around [`RawSolution`] and some additional info. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default)] +pub struct SignedSubmission<AccountId, Balance: HasCompact, CompactSolution> { + /// Who submitted this solution. + pub who: AccountId, + /// The deposit reserved for storing this solution. + pub deposit: Balance, + /// The raw solution itself. + pub solution: RawSolution<CompactSolution>, +} + +impl<AccountId, Balance, CompactSolution> Ord + for SignedSubmission<AccountId, Balance, CompactSolution> +where + AccountId: Ord, + Balance: Ord + HasCompact, + CompactSolution: Ord, + RawSolution<CompactSolution>: Ord, +{ + fn cmp(&self, other: &Self) -> Ordering { + self.solution + .score + .cmp(&other.solution.score) + .then_with(|| self.solution.cmp(&other.solution)) + .then_with(|| self.deposit.cmp(&other.deposit)) + .then_with(|| self.who.cmp(&other.who)) + } +} + +impl<AccountId, Balance, CompactSolution> PartialOrd + for SignedSubmission<AccountId, Balance, CompactSolution> +where + AccountId: Ord, + Balance: Ord + HasCompact, + CompactSolution: Ord, + RawSolution<CompactSolution>: Ord, +{ + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +pub type BalanceOf<T> = + <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; +pub type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency< + <T as frame_system::Config>::AccountId, +>>::PositiveImbalance; +pub type NegativeImbalanceOf<T> = <<T as Config>::Currency as Currency< + <T as frame_system::Config>::AccountId, +>>::NegativeImbalance; +pub type SignedSubmissionOf<T> = + SignedSubmission<<T as frame_system::Config>::AccountId, BalanceOf<T>, CompactOf<T>>; + +pub type SubmissionIndicesOf<T> = + BoundedBTreeMap<ElectionScore, u32, <T as Config>::SignedMaxSubmissions>; + +/// Outcome of [`SignedSubmissions::insert`]. +pub enum InsertResult<T: Config> { + /// The submission was not inserted because the queue was full and the submission had + /// insufficient score to eject a prior solution from the queue. + NotInserted, + /// The submission was inserted successfully without ejecting a solution. + Inserted, + /// The submission was inserted successfully. As the queue was full, this operation ejected a + /// prior solution, contained in this variant. + InsertedEjecting(SignedSubmissionOf<T>), +} + +/// Mask type which pretends to be a set of `SignedSubmissionOf<T>`, while in fact delegating to the +/// actual implementations in `SignedSubmissionIndices<T>`, `SignedSubmissionsMap<T>`, and +/// `SignedSubmissionNextIndex<T>`. +#[cfg_attr(feature = "std", derive(DebugNoBound))] +pub struct SignedSubmissions<T: Config> { + indices: SubmissionIndicesOf<T>, + next_idx: u32, + insertion_overlay: BTreeMap<u32, SignedSubmissionOf<T>>, + deletion_overlay: BTreeSet<u32>, +} + +impl<T: Config> SignedSubmissions<T> { + /// Get the signed submissions from storage. + pub fn get() -> Self { + let submissions = SignedSubmissions { + indices: SignedSubmissionIndices::<T>::get(), + next_idx: SignedSubmissionNextIndex::<T>::get(), + insertion_overlay: BTreeMap::new(), + deletion_overlay: BTreeSet::new(), + }; + // validate that the stored state is sane + debug_assert!(submissions.indices.values().copied().max().map_or( + true, + |max_idx| submissions.next_idx > max_idx, + )); + submissions + } + + /// Put the signed submissions back into storage. + pub fn put(mut self) { + // validate that we're going to write only sane things to storage + debug_assert!(self.insertion_overlay.keys().copied().max().map_or( + true, + |max_idx| self.next_idx > max_idx, + )); + debug_assert!(self.indices.values().copied().max().map_or( + true, + |max_idx| self.next_idx > max_idx, + )); + + SignedSubmissionIndices::<T>::put(self.indices); + SignedSubmissionNextIndex::<T>::put(self.next_idx); + for key in self.deletion_overlay { + self.insertion_overlay.remove(&key); + SignedSubmissionsMap::<T>::remove(key); + } + for (key, value) in self.insertion_overlay { + SignedSubmissionsMap::<T>::insert(key, value); + } + } + + /// Get the submission at a particular index. + fn get_submission(&self, idx: u32) -> Option<SignedSubmissionOf<T>> { + if self.deletion_overlay.contains(&idx) { + // Note: can't actually remove the item from the insertion overlay (if present) + // because we don't want to use `&mut self` here. There may be some kind of + // `RefCell` optimization possible here in the future. + None + } else { + self.insertion_overlay + .get(&idx) + .cloned() + .or_else(|| SignedSubmissionsMap::<T>::try_get(idx).ok()) + } + } + + /// Perform three operations: + /// + /// - Remove a submission (identified by score) + /// - Insert a new submission (identified by score and insertion index) + /// - Return the submission which was removed. + /// + /// Note: in the case that `weakest_score` is not present in `self.indices`, this will return + /// `None` without inserting the new submission and without further notice. + /// + /// Note: this does not enforce any ordering relation between the submission removed and that + /// inserted. + /// + /// Note: this doesn't insert into `insertion_overlay`, the optional new insertion must be + /// inserted into `insertion_overlay` to keep the variable `self` in a valid state. + fn swap_out_submission( + &mut self, + remove_score: ElectionScore, + insert: Option<(ElectionScore, u32)>, + ) -> Option<SignedSubmissionOf<T>> { + let remove_idx = self.indices.remove(&remove_score)?; + if let Some((insert_score, insert_idx)) = insert { + self.indices + .try_insert(insert_score, insert_idx) + .expect("just removed an item, we must be under capacity; qed"); + } + + self.insertion_overlay.remove(&remove_idx).or_else(|| { + (!self.deletion_overlay.contains(&remove_idx)).then(|| { + self.deletion_overlay.insert(remove_idx); + SignedSubmissionsMap::<T>::try_get(remove_idx).ok() + }).flatten() + }) + } + + /// Iterate through the set of signed submissions in order of increasing score. + pub fn iter(&self) -> impl '_ + Iterator<Item = SignedSubmissionOf<T>> { + self.indices.iter().filter_map(move |(_score, &idx)| { + let maybe_submission = self.get_submission(idx); + if maybe_submission.is_none() { + log!( + error, + "SignedSubmissions internal state is invalid (idx {}); \ + there is a logic error in code handling signed solution submissions", + idx, + ) + } + maybe_submission + }) + } + + /// Empty the set of signed submissions, returning an iterator of signed submissions in + /// arbitrary order. + /// + /// Note that if the iterator is dropped without consuming all elements, not all may be removed + /// from the underlying `SignedSubmissionsMap`, putting the storages into an invalid state. + /// + /// Note that, like `put`, this function consumes `Self` and modifies storage. + fn drain(mut self) -> impl Iterator<Item = SignedSubmissionOf<T>> { + SignedSubmissionIndices::<T>::kill(); + SignedSubmissionNextIndex::<T>::kill(); + let insertion_overlay = sp_std::mem::take(&mut self.insertion_overlay); + SignedSubmissionsMap::<T>::drain() + .filter(move |(k, _v)| !self.deletion_overlay.contains(k)) + .map(|(_k, v)| v) + .chain(insertion_overlay.into_iter().map(|(_k, v)| v)) + } + + /// Decode the length of the signed submissions without actually reading the entire struct into + /// memory. + /// + /// Note that if you hold an instance of `SignedSubmissions`, this function does _not_ + /// track its current length. This only decodes what is currently stored in memory. + pub fn decode_len() -> Option<usize> { + SignedSubmissionIndices::<T>::decode_len() + } + + /// Insert a new signed submission into the set. + /// + /// In the event that the new submission is not better than the current weakest according + /// to `is_score_better`, we do not change anything. + pub fn insert( + &mut self, + submission: SignedSubmissionOf<T>, + ) -> InsertResult<T> { + // verify the expectation that we never reuse an index + debug_assert!(!self.indices.values().any(|&idx| idx == self.next_idx)); + + let weakest = match self.indices.try_insert(submission.solution.score, self.next_idx) { + Ok(Some(prev_idx)) => { + // a submission of equal score was already present in the set; + // no point editing the actual backing map as we know that the newer solution can't + // be better than the old. However, we do need to put the old value back. + self.indices + .try_insert(submission.solution.score, prev_idx) + .expect("didn't change the map size; qed"); + return InsertResult::NotInserted; + } + Ok(None) => { + // successfully inserted into the set; no need to take out weakest member + None + } + Err((insert_score, insert_idx)) => { + // could not insert into the set because it is full. + // note that we short-circuit return here in case the iteration produces `None`. + // If there wasn't a weakest entry to remove, then there must be a capacity of 0, + // which means that we can't meaningfully proceed. + let weakest_score = match self.indices.iter().next() { + None => return InsertResult::NotInserted, + Some((score, _)) => *score, + }; + let threshold = T::SolutionImprovementThreshold::get(); + + // if we haven't improved on the weakest score, don't change anything. + if !is_score_better(insert_score, weakest_score, threshold) { + return InsertResult::NotInserted; + } + + self.swap_out_submission(weakest_score, Some((insert_score, insert_idx))) + } + }; + + // we've taken out the weakest, so update the storage map and the next index + debug_assert!(!self.insertion_overlay.contains_key(&self.next_idx)); + self.insertion_overlay.insert(self.next_idx, submission); + debug_assert!(!self.deletion_overlay.contains(&self.next_idx)); + self.next_idx += 1; + match weakest { + Some(weakest) => InsertResult::InsertedEjecting(weakest), + None => InsertResult::Inserted, + } + } + + /// Remove the signed submission with the highest score from the set. + pub fn pop_last(&mut self) -> Option<SignedSubmissionOf<T>> { + let (score, _) = self.indices.iter().rev().next()?; + // deref in advance to prevent mutable-immutable borrow conflict + let score = *score; + self.swap_out_submission(score, None) + } +} + +impl<T: Config> Deref for SignedSubmissions<T> { + type Target = SubmissionIndicesOf<T>; + + fn deref(&self) -> &Self::Target { + &self.indices + } +} + +impl<T: Config> Pallet<T> { + /// `Self` accessor for `SignedSubmission<T>`. + pub fn signed_submissions() -> SignedSubmissions<T> { + SignedSubmissions::<T>::get() + } + + /// Finish the signed phase. Process the signed submissions from best to worse until a valid one + /// is found, rewarding the best one and slashing the invalid ones along the way. + /// + /// Returns true if we have a good solution in the signed phase. + /// + /// This drains the [`SignedSubmissions`], potentially storing the best valid one in + /// [`QueuedSolution`]. + pub fn finalize_signed_phase() -> (bool, Weight) { + let mut all_submissions = Self::signed_submissions(); + let mut found_solution = false; + let mut weight = T::DbWeight::get().reads(1); + + let SolutionOrSnapshotSize { voters, targets } = + Self::snapshot_metadata().unwrap_or_default(); + + let reward = T::SignedRewardBase::get(); + + while let Some(best) = all_submissions.pop_last() { + let SignedSubmission { solution, who, deposit} = best; + let active_voters = solution.compact.voter_count() as u32; + let feasibility_weight = { + // defensive only: at the end of signed phase, snapshot will exits. + let desired_targets = Self::desired_targets().unwrap_or_default(); + T::WeightInfo::feasibility_check( + voters, + targets, + active_voters, + desired_targets, + ) + }; + // the feasibility check itself has some weight + weight = weight.saturating_add(feasibility_weight); + match Self::feasibility_check(solution, ElectionCompute::Signed) { + Ok(ready_solution) => { + Self::finalize_signed_phase_accept_solution( + ready_solution, + &who, + deposit, + reward, + ); + found_solution = true; + + weight = weight + .saturating_add(T::WeightInfo::finalize_signed_phase_accept_solution()); + break; + } + Err(_) => { + Self::finalize_signed_phase_reject_solution(&who, deposit); + weight = weight + .saturating_add(T::WeightInfo::finalize_signed_phase_reject_solution()); + } + } + } + + // Any unprocessed solution is pointless to even consider. Feasible or malicious, + // they didn't end up being used. Unreserve the bonds. + let discarded = all_submissions.len(); + for SignedSubmission { who, deposit, .. } in all_submissions.drain() { + let _remaining = T::Currency::unreserve(&who, deposit); + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + debug_assert!(_remaining.is_zero()); + } + + debug_assert!(!SignedSubmissionIndices::<T>::exists()); + debug_assert!(!SignedSubmissionNextIndex::<T>::exists()); + debug_assert!(SignedSubmissionsMap::<T>::iter().next().is_none()); + + log!(debug, "closed signed phase, found solution? {}, discarded {}", found_solution, discarded); + (found_solution, weight) + } + + /// Helper function for the case where a solution is accepted in the signed phase. + /// + /// Extracted to facilitate with weight calculation. + /// + /// Infallible + pub fn finalize_signed_phase_accept_solution( + ready_solution: ReadySolution<T::AccountId>, + who: &T::AccountId, + deposit: BalanceOf<T>, + reward: BalanceOf<T>, + ) { + // write this ready solution. + <QueuedSolution<T>>::put(ready_solution); + + // emit reward event + Self::deposit_event(crate::Event::Rewarded(who.clone())); + + // unreserve deposit. + let _remaining = T::Currency::unreserve(who, deposit); + debug_assert!(_remaining.is_zero()); + + // Reward. + let positive_imbalance = T::Currency::deposit_creating(who, reward); + T::RewardHandler::on_unbalanced(positive_imbalance); + } + + /// Helper function for the case where a solution is accepted in the rejected phase. + /// + /// Extracted to facilitate with weight calculation. + /// + /// Infallible + pub fn finalize_signed_phase_reject_solution(who: &T::AccountId, deposit: BalanceOf<T>) { + Self::deposit_event(crate::Event::Slashed(who.clone())); + let (negative_imbalance, _remaining) = T::Currency::slash_reserved(who, deposit); + debug_assert!(_remaining.is_zero()); + T::SlashHandler::on_unbalanced(negative_imbalance); + } + + /// The feasibility weight of the given raw solution. + pub fn feasibility_weight_of( + solution: &RawSolution<CompactOf<T>>, + size: SolutionOrSnapshotSize, + ) -> Weight { + T::WeightInfo::feasibility_check( + size.voters, + size.targets, + solution.compact.voter_count() as u32, + solution.compact.unique_targets().len() as u32, + ) + } + + /// Collect a sufficient deposit to store this solution. + /// + /// The deposit is composed of 3 main elements: + /// + /// 1. base deposit, fixed for all submissions. + /// 2. a per-byte deposit, for renting the state usage. + /// 3. a per-weight deposit, for the potential weight usage in an upcoming on_initialize + pub fn deposit_for( + solution: &RawSolution<CompactOf<T>>, + size: SolutionOrSnapshotSize, + ) -> BalanceOf<T> { + let encoded_len: u32 = solution.encoded_size().saturated_into(); + let encoded_len: BalanceOf<T> = encoded_len.into(); + let feasibility_weight = Self::feasibility_weight_of(solution, size); + + let len_deposit = T::SignedDepositByte::get().saturating_mul(encoded_len); + let weight_deposit = T::SignedDepositWeight::get().saturating_mul(feasibility_weight.saturated_into()); + + T::SignedDepositBase::get().saturating_add(len_deposit).saturating_add(weight_deposit) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + Phase, Error, + mock::{ + balances, ExtBuilder, MultiPhase, Origin, raw_solution, roll_to, Runtime, + SignedMaxSubmissions, SignedMaxWeight, + }, + }; + use frame_support::{dispatch::DispatchResult, assert_noop, assert_storage_noop, assert_ok}; + + fn submit_with_witness( + origin: Origin, + solution: RawSolution<CompactOf<Runtime>>, + ) -> DispatchResult { + MultiPhase::submit(origin, solution, MultiPhase::signed_submissions().len() as u32) + } + + #[test] + fn cannot_submit_too_early() { + ExtBuilder::default().build_and_execute(|| { + roll_to(2); + assert_eq!(MultiPhase::current_phase(), Phase::Off); + + // create a temp snapshot only for this test. + MultiPhase::create_snapshot().unwrap(); + let solution = raw_solution(); + + assert_noop!( + submit_with_witness(Origin::signed(10), solution), + Error::<Runtime>::PreDispatchEarlySubmission, + ); + }) + } + + #[test] + fn wrong_witness_fails() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let solution = raw_solution(); + // submit this once correctly + assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + assert_eq!(MultiPhase::signed_submissions().len(), 1); + + // now try and cheat by passing a lower queue length + assert_noop!( + MultiPhase::submit(Origin::signed(99), solution, 0), + Error::<Runtime>::SignedInvalidWitness, + ); + }) + } + + #[test] + fn should_pay_deposit() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let solution = raw_solution(); + assert_eq!(balances(&99), (100, 0)); + + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + + assert_eq!(balances(&99), (95, 5)); + assert_eq!(MultiPhase::signed_submissions().iter().next().unwrap().deposit, 5); + }) + } + + #[test] + fn good_solution_is_rewarded() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let solution = raw_solution(); + assert_eq!(balances(&99), (100, 0)); + + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + assert_eq!(balances(&99), (95, 5)); + + assert!(MultiPhase::finalize_signed_phase().0); + assert_eq!(balances(&99), (100 + 7, 0)); + }) + } + + #[test] + fn bad_solution_is_slashed() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let mut solution = raw_solution(); + assert_eq!(balances(&99), (100, 0)); + + // make the solution invalid. + solution.score[0] += 1; + + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + assert_eq!(balances(&99), (95, 5)); + + // no good solution was stored. + assert!(!MultiPhase::finalize_signed_phase().0); + // and the bond is gone. + assert_eq!(balances(&99), (95, 0)); + }) + } + + #[test] + fn suppressed_solution_gets_bond_back() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let mut solution = raw_solution(); + assert_eq!(balances(&99), (100, 0)); + assert_eq!(balances(&999), (100, 0)); + + // submit as correct. + assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + + // make the solution invalid and weaker. + solution.score[0] -= 1; + assert_ok!(submit_with_witness(Origin::signed(999), solution)); + assert_eq!(balances(&99), (95, 5)); + assert_eq!(balances(&999), (95, 5)); + + // _some_ good solution was stored. + assert!(MultiPhase::finalize_signed_phase().0); + + // 99 is rewarded. + assert_eq!(balances(&99), (100 + 7, 0)); + // 999 gets everything back. + assert_eq!(balances(&999), (100, 0)); + }) + } + + #[test] + fn cannot_submit_worse_with_full_queue() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for s in 0..SignedMaxSubmissions::get() { + // score is always getting better + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + + + // weaker. + let solution = RawSolution { score: [4, 0, 0], ..Default::default() }; + + assert_noop!( + submit_with_witness(Origin::signed(99), solution), + Error::<Runtime>::SignedQueueFull, + ); + }) + } + + #[test] + fn weakest_is_removed_if_better_provided() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for s in 0..SignedMaxSubmissions::get() { + // score is always getting better + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + + assert_eq!( + MultiPhase::signed_submissions() + .iter() + .map(|s| s.solution.score[0]) + .collect::<Vec<_>>(), + vec![5, 6, 7, 8, 9] + ); + + // better. + let solution = RawSolution { score: [20, 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + + // the one with score 5 was rejected, the new one inserted. + assert_eq!( + MultiPhase::signed_submissions() + .iter() + .map(|s| s.solution.score[0]) + .collect::<Vec<_>>(), + vec![6, 7, 8, 9, 20] + ); + }) + } + + #[test] + fn replace_weakest_works() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for s in 1..SignedMaxSubmissions::get() { + // score is always getting better + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + + let solution = RawSolution { score: [4, 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + + assert_eq!( + MultiPhase::signed_submissions() + .iter() + .map(|s| s.solution.score[0]) + .collect::<Vec<_>>(), + vec![4, 6, 7, 8, 9], + ); + + // better. + let solution = RawSolution { score: [5, 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + + // the one with score 5 was rejected, the new one inserted. + assert_eq!( + MultiPhase::signed_submissions() + .iter() + .map(|s| s.solution.score[0]) + .collect::<Vec<_>>(), + vec![5, 6, 7, 8, 9], + ); + }) + } + + #[test] + fn early_ejected_solution_gets_bond_back() { + ExtBuilder::default().signed_deposit(2, 0, 0).build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for s in 0..SignedMaxSubmissions::get() { + // score is always getting better + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + + assert_eq!(balances(&99).1, 2 * 5); + assert_eq!(balances(&999).1, 0); + + // better. + let solution = RawSolution { score: [20, 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(999), solution)); + + // got one bond back. + assert_eq!(balances(&99).1, 2 * 4); + assert_eq!(balances(&999).1, 2); + }) + } + + #[test] + fn equally_good_solution_is_not_accepted() { + ExtBuilder::default().signed_max_submission(3).build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for i in 0..SignedMaxSubmissions::get() { + let solution = RawSolution { score: [(5 + i).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + assert_eq!( + MultiPhase::signed_submissions() + .iter() + .map(|s| s.solution.score[0]) + .collect::<Vec<_>>(), + vec![5, 6, 7] + ); + + // 5 is not accepted. This will only cause processing with no benefit. + let solution = RawSolution { score: [5, 0, 0], ..Default::default() }; + assert_noop!( + submit_with_witness(Origin::signed(99), solution), + Error::<Runtime>::SignedQueueFull, + ); + }) + } + + #[test] + fn all_in_one_signed_submission_scenario() { + // a combination of: + // - good_solution_is_rewarded + // - bad_solution_is_slashed + // - suppressed_solution_gets_bond_back + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + assert_eq!(balances(&99), (100, 0)); + assert_eq!(balances(&999), (100, 0)); + assert_eq!(balances(&9999), (100, 0)); + let solution = raw_solution(); + + // submit a correct one. + assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + + // make the solution invalidly better and submit. This ought to be slashed. + let mut solution_999 = solution.clone(); + solution_999.score[0] += 1; + assert_ok!(submit_with_witness(Origin::signed(999), solution_999)); + + // make the solution invalidly worse and submit. This ought to be suppressed and + // returned. + let mut solution_9999 = solution.clone(); + solution_9999.score[0] -= 1; + assert_ok!(submit_with_witness(Origin::signed(9999), solution_9999)); + + assert_eq!( + MultiPhase::signed_submissions().iter().map(|x| x.who).collect::<Vec<_>>(), + vec![9999, 99, 999] + ); + + // _some_ good solution was stored. + assert!(MultiPhase::finalize_signed_phase().0); + + // 99 is rewarded. + assert_eq!(balances(&99), (100 + 7, 0)); + // 999 is slashed. + assert_eq!(balances(&999), (95, 0)); + // 9999 gets everything back. + assert_eq!(balances(&9999), (100, 0)); + }) + } + + #[test] + fn cannot_consume_too_much_future_weight() { + ExtBuilder::default().signed_weight(40).mock_weight_info(true).build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let (solution, witness) = MultiPhase::mine_solution(2).unwrap(); + let solution_weight = <Runtime as Config>::WeightInfo::feasibility_check( + witness.voters, + witness.targets, + solution.compact.voter_count() as u32, + solution.compact.unique_targets().len() as u32, + ); + // default solution will have 5 edges (5 * 5 + 10) + assert_eq!(solution_weight, 35); + assert_eq!(solution.compact.voter_count(), 5); + assert_eq!(<Runtime as Config>::SignedMaxWeight::get(), 40); + + assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + + <SignedMaxWeight>::set(30); + + // note: resubmitting the same solution is technically okay as long as the queue has + // space. + assert_noop!( + submit_with_witness(Origin::signed(99), solution), + Error::<Runtime>::SignedTooMuchWeight, + ); + }) + } + + #[test] + fn insufficient_deposit_doesnt_store_submission() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let solution = raw_solution(); + + assert_eq!(balances(&123), (0, 0)); + assert_noop!( + submit_with_witness(Origin::signed(123), solution), + Error::<Runtime>::SignedCannotPayDeposit, + ); + + assert_eq!(balances(&123), (0, 0)); + }) + } + + // given a full queue, and a solution which _should_ be allowed in, but the proposer of this + // new solution has insufficient deposit, we should not modify storage at all + #[test] + fn insufficient_deposit_with_full_queue_works_properly() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + for s in 0..SignedMaxSubmissions::get() { + // score is always getting better + let solution = RawSolution { score: [(5 + s).into(), 0, 0], ..Default::default() }; + assert_ok!(submit_with_witness(Origin::signed(99), solution)); + } + + // this solution has a higher score than any in the queue + let solution = RawSolution { + score: [(5 + SignedMaxSubmissions::get()).into(), 0, 0], + ..Default::default() + }; + + assert_eq!(balances(&123), (0, 0)); + assert_noop!( + submit_with_witness(Origin::signed(123), solution), + Error::<Runtime>::SignedCannotPayDeposit, + ); + + assert_eq!(balances(&123), (0, 0)); + }) + } + + #[test] + fn finalize_signed_phase_is_idempotent_given_no_submissions() { + ExtBuilder::default().build_and_execute(|| { + for block_number in 0..25 { + roll_to(block_number); + + assert_eq!(SignedSubmissions::<Runtime>::decode_len().unwrap_or_default(), 0); + assert_storage_noop!(MultiPhase::finalize_signed_phase()); + } + }) + } + + #[test] + fn finalize_signed_phase_is_idempotent_given_submissions() { + ExtBuilder::default().build_and_execute(|| { + roll_to(15); + assert!(MultiPhase::current_phase().is_signed()); + + let solution = raw_solution(); + + // submit a correct one. + assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + + // _some_ good solution was stored. + assert!(MultiPhase::finalize_signed_phase().0); + + // calling it again doesn't change anything + assert_storage_noop!(MultiPhase::finalize_signed_phase()); + }) + } +} diff --git a/substrate/frame/election-provider-multi-phase/src/unsigned.rs b/substrate/frame/election-provider-multi-phase/src/unsigned.rs index 543883fc035c500daedd7412a3924d7f3b970b1d..52ecae7afa5fc6431e5081fa64777c82f5675fea 100644 --- a/substrate/frame/election-provider-multi-phase/src/unsigned.rs +++ b/substrate/frame/election-provider-multi-phase/src/unsigned.rs @@ -30,8 +30,10 @@ use sp_npos_elections::{ assignment_staked_to_ratio_normalized, is_score_better, seq_phragmen, }; use sp_runtime::{ + DispatchError, + SaturatedConversion, offchain::storage::{MutateStorageError, StorageValueRef}, - traits::TrailingZeroInput, SaturatedConversion + traits::TrailingZeroInput, }; use sp_std::{cmp::Ordering, convert::TryFrom, vec::Vec}; @@ -57,7 +59,8 @@ pub type Assignment<T> = sp_npos_elections::Assignment< CompactAccuracyOf<T>, >; -/// The [`IndexAssignment`][sp_npos_elections::IndexAssignment] type specialized for a particular runtime `T`. +/// The [`IndexAssignment`][sp_npos_elections::IndexAssignment] type specialized for a particular +/// runtime `T`. pub type IndexAssignmentOf<T> = sp_npos_elections::IndexAssignmentOf<CompactOf<T>>; #[derive(Debug, Eq, PartialEq)] @@ -69,7 +72,7 @@ pub enum MinerError { /// Submitting a transaction to the pool failed. PoolSubmissionFailed, /// The pre-dispatch checks failed for the mined solution. - PreDispatchChecksFailed, + PreDispatchChecksFailed(DispatchError), /// The solution generated from the miner is not feasible. Feasibility(FeasibilityError), /// Something went wrong fetching the lock. @@ -234,7 +237,7 @@ impl<T: Config> Pallet<T> { ) -> Result<(), MinerError> { Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| { log!(debug, "pre-dispatch checks failed for {} solution: {:?}", solution_type, err); - MinerError::PreDispatchChecksFailed + MinerError::PreDispatchChecksFailed(err) })?; Self::feasibility_check(raw_solution.clone(), ElectionCompute::Unsigned).map_err(|err| { @@ -344,7 +347,11 @@ impl<T: Config> Pallet<T> { // converting to `Compact`. let mut index_assignments = sorted_assignments .into_iter() - .map(|assignment| IndexAssignmentOf::<T>::new(&assignment, &voter_index, &target_index)) + .map(|assignment| IndexAssignmentOf::<T>::new( + &assignment, + &voter_index, + &target_index, + )) .collect::<Result<Vec<_>, _>>()?; // trim assignments list for weight and length. @@ -416,7 +423,9 @@ impl<T: Config> Pallet<T> { size, max_weight, ); - let removing: usize = assignments.len().saturating_sub(maximum_allowed_voters.saturated_into()); + let removing: usize = assignments.len().saturating_sub( + maximum_allowed_voters.saturated_into(), + ); log!( debug, "from {} assignments, truncating to {} for weight, removing {}", @@ -464,7 +473,9 @@ impl<T: Config> Pallet<T> { } } let maximum_allowed_voters = - if low < assignments.len() && encoded_size_of(&assignments[..low + 1])? <= max_allowed_length { + if low < assignments.len() && + encoded_size_of(&assignments[..low + 1])? <= max_allowed_length + { low + 1 } else { low @@ -674,6 +685,15 @@ mod max_weight { fn on_initialize_open_unsigned_without_snapshot() -> Weight { unreachable!() } + fn finalize_signed_phase_accept_solution() -> Weight { + unreachable!() + } + fn finalize_signed_phase_reject_solution() -> Weight { + unreachable!() + } + fn submit(c: u32) -> Weight { + unreachable!() + } fn submit_unsigned(v: u32, t: u32, a: u32, d: u32) -> Weight { (0 * v + 0 * t + 1000 * a + 0 * d) as Weight } @@ -994,7 +1014,11 @@ mod tests { assert_eq!( MultiPhase::mine_check_save_submit().unwrap_err(), - MinerError::PreDispatchChecksFailed, + MinerError::PreDispatchChecksFailed(DispatchError::Module{ + index: 2, + error: 1, + message: Some("PreDispatchWrongWinnerCount"), + }), ); }) } @@ -1199,11 +1223,17 @@ mod tests { let mut storage = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK); storage.clear(); - assert!(!ocw_solution_exists::<Runtime>(), "no solution should be present before we mine one"); + assert!( + !ocw_solution_exists::<Runtime>(), + "no solution should be present before we mine one", + ); // creates and cache a solution MultiPhase::offchain_worker(25); - assert!(ocw_solution_exists::<Runtime>(), "a solution must be cached after running the worker"); + assert!( + ocw_solution_exists::<Runtime>(), + "a solution must be cached after running the worker", + ); // after an election, the solution must be cleared // we don't actually care about the result of the election @@ -1329,10 +1359,15 @@ mod tests { _ => panic!("bad call: unexpected submission"), }; - // Custom(3) maps to PreDispatchChecksFailed - let pre_dispatch_check_error = TransactionValidityError::Invalid(InvalidTransaction::Custom(3)); + // Custom(7) maps to PreDispatchChecksFailed + let pre_dispatch_check_error = TransactionValidityError::Invalid( + InvalidTransaction::Custom(7), + ); assert_eq!( - <MultiPhase as ValidateUnsigned>::validate_unsigned(TransactionSource::Local, &call) + <MultiPhase as ValidateUnsigned>::validate_unsigned( + TransactionSource::Local, + &call, + ) .unwrap_err(), pre_dispatch_check_error, ); @@ -1359,7 +1394,11 @@ mod tests { let compact_clone = compact.clone(); // when - MultiPhase::trim_assignments_length(encoded_len, &mut assignments, encoded_size_of).unwrap(); + MultiPhase::trim_assignments_length( + encoded_len, + &mut assignments, + encoded_size_of, + ).unwrap(); // then let compact = CompactOf::<Runtime>::try_from(assignments.as_slice()).unwrap(); @@ -1383,7 +1422,11 @@ mod tests { let compact_clone = compact.clone(); // when - MultiPhase::trim_assignments_length(encoded_len as u32 - 1, &mut assignments, encoded_size_of).unwrap(); + MultiPhase::trim_assignments_length( + encoded_len as u32 - 1, + &mut assignments, + encoded_size_of, + ).unwrap(); // then let compact = CompactOf::<Runtime>::try_from(assignments.as_slice()).unwrap(); @@ -1414,7 +1457,11 @@ mod tests { .unwrap(); // when - MultiPhase::trim_assignments_length(encoded_len - 1, &mut assignments, encoded_size_of).unwrap(); + MultiPhase::trim_assignments_length( + encoded_len - 1, + &mut assignments, + encoded_size_of, + ).unwrap(); // then assert_eq!(assignments.len(), count - 1, "we must have removed exactly one assignment"); diff --git a/substrate/frame/election-provider-multi-phase/src/weights.rs b/substrate/frame/election-provider-multi-phase/src/weights.rs index 51b99bc962d43d3c711e92b8427281a861595a4c..6a245ebb512545b4194ed39c6d106c36c47a3aa4 100644 --- a/substrate/frame/election-provider-multi-phase/src/weights.rs +++ b/substrate/frame/election-provider-multi-phase/src/weights.rs @@ -18,7 +18,7 @@ //! Autogenerated weights for pallet_election_provider_multi_phase //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 -//! DATE: 2021-06-19, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2021-06-20, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 // Executed Command: @@ -47,8 +47,11 @@ pub trait WeightInfo { fn on_initialize_nothing() -> Weight; fn on_initialize_open_signed() -> Weight; fn on_initialize_open_unsigned_with_snapshot() -> Weight; + fn finalize_signed_phase_accept_solution() -> Weight; + fn finalize_signed_phase_reject_solution() -> Weight; fn on_initialize_open_unsigned_without_snapshot() -> Weight; fn elect_queued() -> Weight; + fn submit(c: u32, ) -> Weight; fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight; fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight; } @@ -57,52 +60,69 @@ pub trait WeightInfo { pub struct SubstrateWeight<T>(PhantomData<T>); impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { fn on_initialize_nothing() -> Weight { - (24_579_000 as Weight) + (33_392_000 as Weight) .saturating_add(T::DbWeight::get().reads(8 as Weight)) } fn on_initialize_open_signed() -> Weight { - (87_463_000 as Weight) + (115_659_000 as Weight) .saturating_add(T::DbWeight::get().reads(10 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } fn on_initialize_open_unsigned_with_snapshot() -> Weight { - (87_381_000 as Weight) + (114_970_000 as Weight) .saturating_add(T::DbWeight::get().reads(10 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } + fn finalize_signed_phase_accept_solution() -> Weight { + (51_442_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn finalize_signed_phase_reject_solution() -> Weight { + (23_160_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } fn on_initialize_open_unsigned_without_snapshot() -> Weight { - (18_489_000 as Weight) + (24_101_000 as Weight) .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } fn elect_queued() -> Weight { - (6_038_989_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - .saturating_add(T::DbWeight::get().writes(6 as Weight)) + (6_153_604_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(8 as Weight)) + } + fn submit(c: u32, ) -> Weight { + (78_972_000 as Weight) + // Standard Error: 16_000 + .saturating_add((308_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) } fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight { (0 as Weight) // Standard Error: 12_000 - .saturating_add((3_480_000 as Weight).saturating_mul(v as Weight)) + .saturating_add((3_572_000 as Weight).saturating_mul(v as Weight)) // Standard Error: 42_000 - .saturating_add((194_000 as Weight).saturating_mul(t as Weight)) + .saturating_add((23_000 as Weight).saturating_mul(t as Weight)) // Standard Error: 12_000 - .saturating_add((10_498_000 as Weight).saturating_mul(a as Weight)) + .saturating_add((11_529_000 as Weight).saturating_mul(a as Weight)) // Standard Error: 63_000 - .saturating_add((3_074_000 as Weight).saturating_mul(d as Weight)) + .saturating_add((3_333_000 as Weight).saturating_mul(d as Weight)) .saturating_add(T::DbWeight::get().reads(7 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight { (0 as Weight) // Standard Error: 7_000 - .saturating_add((3_481_000 as Weight).saturating_mul(v as Weight)) - // Standard Error: 24_000 - .saturating_add((385_000 as Weight).saturating_mul(t as Weight)) + .saturating_add((3_647_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 23_000 + .saturating_add((390_000 as Weight).saturating_mul(t as Weight)) // Standard Error: 7_000 - .saturating_add((8_538_000 as Weight).saturating_mul(a as Weight)) - // Standard Error: 36_000 - .saturating_add((3_322_000 as Weight).saturating_mul(d as Weight)) + .saturating_add((9_614_000 as Weight).saturating_mul(a as Weight)) + // Standard Error: 35_000 + .saturating_add((3_405_000 as Weight).saturating_mul(d as Weight)) .saturating_add(T::DbWeight::get().reads(4 as Weight)) } } @@ -110,52 +130,69 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { // For backwards compatibility and tests impl WeightInfo for () { fn on_initialize_nothing() -> Weight { - (24_579_000 as Weight) + (33_392_000 as Weight) .saturating_add(RocksDbWeight::get().reads(8 as Weight)) } fn on_initialize_open_signed() -> Weight { - (87_463_000 as Weight) + (115_659_000 as Weight) .saturating_add(RocksDbWeight::get().reads(10 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } fn on_initialize_open_unsigned_with_snapshot() -> Weight { - (87_381_000 as Weight) + (114_970_000 as Weight) .saturating_add(RocksDbWeight::get().reads(10 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } + fn finalize_signed_phase_accept_solution() -> Weight { + (51_442_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + } + fn finalize_signed_phase_reject_solution() -> Weight { + (23_160_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } fn on_initialize_open_unsigned_without_snapshot() -> Weight { - (18_489_000 as Weight) + (24_101_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } fn elect_queued() -> Weight { - (6_038_989_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - .saturating_add(RocksDbWeight::get().writes(6 as Weight)) + (6_153_604_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(8 as Weight)) + } + fn submit(c: u32, ) -> Weight { + (78_972_000 as Weight) + // Standard Error: 16_000 + .saturating_add((308_000 as Weight).saturating_mul(c as Weight)) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } fn submit_unsigned(v: u32, t: u32, a: u32, d: u32, ) -> Weight { (0 as Weight) // Standard Error: 12_000 - .saturating_add((3_480_000 as Weight).saturating_mul(v as Weight)) + .saturating_add((3_572_000 as Weight).saturating_mul(v as Weight)) // Standard Error: 42_000 - .saturating_add((194_000 as Weight).saturating_mul(t as Weight)) + .saturating_add((23_000 as Weight).saturating_mul(t as Weight)) // Standard Error: 12_000 - .saturating_add((10_498_000 as Weight).saturating_mul(a as Weight)) + .saturating_add((11_529_000 as Weight).saturating_mul(a as Weight)) // Standard Error: 63_000 - .saturating_add((3_074_000 as Weight).saturating_mul(d as Weight)) + .saturating_add((3_333_000 as Weight).saturating_mul(d as Weight)) .saturating_add(RocksDbWeight::get().reads(7 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } fn feasibility_check(v: u32, t: u32, a: u32, d: u32, ) -> Weight { (0 as Weight) // Standard Error: 7_000 - .saturating_add((3_481_000 as Weight).saturating_mul(v as Weight)) - // Standard Error: 24_000 - .saturating_add((385_000 as Weight).saturating_mul(t as Weight)) + .saturating_add((3_647_000 as Weight).saturating_mul(v as Weight)) + // Standard Error: 23_000 + .saturating_add((390_000 as Weight).saturating_mul(t as Weight)) // Standard Error: 7_000 - .saturating_add((8_538_000 as Weight).saturating_mul(a as Weight)) - // Standard Error: 36_000 - .saturating_add((3_322_000 as Weight).saturating_mul(d as Weight)) + .saturating_add((9_614_000 as Weight).saturating_mul(a as Weight)) + // Standard Error: 35_000 + .saturating_add((3_405_000 as Weight).saturating_mul(d as Weight)) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) } } diff --git a/substrate/frame/support/src/storage/bounded_btree_map.rs b/substrate/frame/support/src/storage/bounded_btree_map.rs index 8c50557618eec9551075a2f4e30eee04aa441e00..0c1994d63a35da0f630346f92fe687891456cba1 100644 --- a/substrate/frame/support/src/storage/bounded_btree_map.rs +++ b/substrate/frame/support/src/storage/bounded_btree_map.rs @@ -39,7 +39,8 @@ pub struct BoundedBTreeMap<K, V, S>(BTreeMap<K, V>, PhantomData<S>); impl<K, V, S> Decode for BoundedBTreeMap<K, V, S> where - BTreeMap<K, V>: Decode, + K: Decode + Ord, + V: Decode, S: Get<u32>, { fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> { @@ -115,14 +116,15 @@ where self.0.get_mut(key) } - /// Exactly the same semantics as [`BTreeMap::insert`], but returns an `Err` (and is a noop) if the - /// new length of the map exceeds `S`. - pub fn try_insert(&mut self, key: K, value: V) -> Result<(), ()> { - if self.len() < Self::bound() { - self.0.insert(key, value); - Ok(()) + /// Exactly the same semantics as [`BTreeMap::insert`], but returns an `Err` (and is a noop) if + /// the new length of the map exceeds `S`. + /// + /// In the `Err` case, returns the inserted pair so it can be further used without cloning. + pub fn try_insert(&mut self, key: K, value: V) -> Result<Option<V>, (K, V)> { + if self.len() < Self::bound() || self.0.contains_key(&key) { + Ok(self.0.insert(key, value)) } else { - Err(()) + Err((key, value)) } } @@ -407,4 +409,50 @@ pub mod test { Err("BoundedBTreeMap exceeds its limit".into()), ); } + + #[test] + fn unequal_eq_impl_insert_works() { + // given a struct with a strange notion of equality + #[derive(Debug)] + struct Unequal(u32, bool); + + impl PartialEq for Unequal { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + impl Eq for Unequal {} + + impl Ord for Unequal { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } + } + + impl PartialOrd for Unequal { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } + } + + let mut map = BoundedBTreeMap::<Unequal, u32, Four>::new(); + + // when the set is full + + for i in 0..4 { + map.try_insert(Unequal(i, false), i).unwrap(); + } + + // can't insert a new distinct member + map.try_insert(Unequal(5, false), 5).unwrap_err(); + + // but _can_ insert a distinct member which compares equal, though per the documentation, + // neither the set length nor the actual member are changed, but the value is + map.try_insert(Unequal(0, true), 6).unwrap(); + assert_eq!(map.len(), 4); + let (zero_key, zero_value) = map.get_key_value(&Unequal(0, true)).unwrap(); + assert_eq!(zero_key.0, 0); + assert_eq!(zero_key.1, false); + assert_eq!(*zero_value, 6); + } } diff --git a/substrate/frame/support/src/storage/bounded_btree_set.rs b/substrate/frame/support/src/storage/bounded_btree_set.rs index f551a3cbfa38e2c823794b240f315478272af763..10c2300a08a09fa343da909a1b23652e2fd8118a 100644 --- a/substrate/frame/support/src/storage/bounded_btree_set.rs +++ b/substrate/frame/support/src/storage/bounded_btree_set.rs @@ -39,7 +39,7 @@ pub struct BoundedBTreeSet<T, S>(BTreeSet<T>, PhantomData<S>); impl<T, S> Decode for BoundedBTreeSet<T, S> where - BTreeSet<T>: Decode, + T: Decode + Ord, S: Get<u32>, { fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> { @@ -103,14 +103,15 @@ where self.0.clear() } - /// Exactly the same semantics as [`BTreeSet::insert`], but returns an `Err` (and is a noop) if the - /// new length of the set exceeds `S`. - pub fn try_insert(&mut self, item: T) -> Result<(), ()> { - if self.len() < Self::bound() { - self.0.insert(item); - Ok(()) + /// Exactly the same semantics as [`BTreeSet::insert`], but returns an `Err` (and is a noop) if + /// the new length of the set exceeds `S`. + /// + /// In the `Err` case, returns the inserted item so it can be further used without cloning. + pub fn try_insert(&mut self, item: T) -> Result<bool, T> { + if self.len() < Self::bound() || self.0.contains(&item) { + Ok(self.0.insert(item)) } else { - Err(()) + Err(item) } } @@ -393,4 +394,49 @@ pub mod test { Err("BoundedBTreeSet exceeds its limit".into()), ); } + + #[test] + fn unequal_eq_impl_insert_works() { + // given a struct with a strange notion of equality + #[derive(Debug)] + struct Unequal(u32, bool); + + impl PartialEq for Unequal { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + impl Eq for Unequal {} + + impl Ord for Unequal { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } + } + + impl PartialOrd for Unequal { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } + } + + let mut set = BoundedBTreeSet::<Unequal, Four>::new(); + + // when the set is full + + for i in 0..4 { + set.try_insert(Unequal(i, false)).unwrap(); + } + + // can't insert a new distinct member + set.try_insert(Unequal(5, false)).unwrap_err(); + + // but _can_ insert a distinct member which compares equal, though per the documentation, + // neither the set length nor the actual member are changed + set.try_insert(Unequal(0, true)).unwrap(); + assert_eq!(set.len(), 4); + let zero_item = set.get(&Unequal(0, true)).unwrap(); + assert_eq!(zero_item.0, 0); + assert_eq!(zero_item.1, false); + } } diff --git a/substrate/primitives/npos-elections/compact/src/lib.rs b/substrate/primitives/npos-elections/compact/src/lib.rs index e8cde87744539b77c696385fad91c0d231c592a2..0e9fbb34eea17d0518758e7571563862449989de 100644 --- a/substrate/primitives/npos-elections/compact/src/lib.rs +++ b/substrate/primitives/npos-elections/compact/src/lib.rs @@ -169,7 +169,7 @@ fn struct_def( ); quote!{ #compact_impl - #[derive(Default, PartialEq, Eq, Clone, Debug)] + #[derive(Default, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)] } } else { // automatically derived.