diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index d4b9975704a45..2b9accffc8c3a 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -39,6 +39,7 @@ sp-session = { version = "4.0.0-dev", default-features = false, path = "../../.. sp-transaction-pool = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/transaction-pool" } sp-version = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/version" } sp-npos-elections = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/npos-elections" } +sp-io = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/io" } # frame dependencies frame-executive = { version = "4.0.0-dev", default-features = false, path = "../../../frame/executive" } @@ -98,9 +99,6 @@ pallet-vesting = { version = "4.0.0-dev", default-features = false, path = "../. [build-dependencies] substrate-wasm-builder = { version = "5.0.0-dev", path = "../../../utils/wasm-builder" } -[dev-dependencies] -sp-io = { version = "4.0.0-dev", path = "../../../primitives/io" } - [features] default = ["std"] with-tracing = ["frame-executive/with-tracing"] @@ -169,6 +167,7 @@ std = [ "log/std", "frame-try-runtime/std", "sp-npos-elections/std", + "sp-io/std" ] runtime-benchmarks = [ "frame-benchmarking", diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index 936dc1c35c84c..7dc87c531ab57 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -534,7 +534,6 @@ parameter_types! { // miner configs pub const MultiPhaseUnsignedPriority: TransactionPriority = StakingUnsignedPriority::get() - 1u64; - pub const MinerMaxIterations: u32 = 10; pub MinerMaxWeight: Weight = RuntimeBlockWeights::get() .get(DispatchClass::Normal) .max_extrinsic.expect("Normal extrinsics have a weight limit configured; qed") @@ -570,6 +569,32 @@ impl pallet_election_provider_multi_phase::BenchmarkingConfig for BenchmarkConfi const MAXIMUM_TARGETS: u32 = 2000; } +/// Maximum number of iterations for balancing that will be executed in the embedded OCW +/// miner of election provider multi phase. +pub const MINER_MAX_ITERATIONS: u32 = 10; + +/// A source of random balance for NposSolver, which is meant to be run by the OCW election miner. +pub struct OffchainRandomBalancing; +impl frame_support::pallet_prelude::Get> + for OffchainRandomBalancing +{ + fn get() -> Option<(usize, sp_npos_elections::ExtendedBalance)> { + use sp_runtime::traits::TrailingZeroInput; + let iters = match MINER_MAX_ITERATIONS { + 0 => 0, + max @ _ => { + let seed = sp_io::offchain::random_seed(); + let random = ::decode(&mut TrailingZeroInput::new(&seed)) + .expect("input is padded with zeroes; qed") % + max.saturating_add(1); + random as usize + }, + }; + + Some((iters, 0)) + } +} + impl pallet_election_provider_multi_phase::Config for Runtime { type Event = Event; type Currency = Balances; @@ -578,7 +603,6 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type UnsignedPhase = UnsignedPhase; type SolutionImprovementThreshold = SolutionImprovementThreshold; type OffchainRepeat = OffchainRepeat; - type MinerMaxIterations = MinerMaxIterations; type MinerMaxWeight = MinerMaxWeight; type MinerMaxLength = MinerMaxLength; type MinerTxPriority = MultiPhaseUnsignedPriority; @@ -594,6 +618,11 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type OnChainAccuracy = Perbill; type Solution = NposSolution16; type Fallback = Fallback; + type Solver = frame_election_provider_support::SequentialPhragmen< + AccountId, + pallet_election_provider_multi_phase::SolutionAccuracyOf, + OffchainRandomBalancing, + >; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; type ForceOrigin = EnsureRootOrHalfCouncil; type BenchmarkingConfig = BenchmarkConfig; diff --git a/frame/election-provider-multi-phase/src/lib.rs b/frame/election-provider-multi-phase/src/lib.rs index b2e0d3898428a..1a130371f3b4d 100644 --- a/frame/election-provider-multi-phase/src/lib.rs +++ b/frame/election-provider-multi-phase/src/lib.rs @@ -485,13 +485,13 @@ pub struct SolutionOrSnapshotSize { /// Internal errors of the pallet. /// /// Note that this is different from [`pallet::Error`]. -#[derive(Debug, Eq, PartialEq)] +#[derive(frame_support::DebugNoBound, frame_support::PartialEqNoBound)] #[cfg_attr(feature = "runtime-benchmarks", derive(strum_macros::IntoStaticStr))] -pub enum ElectionError { +pub enum ElectionError { /// An error happened in the feasibility check sub-system. Feasibility(FeasibilityError), /// An error in the miner (offchain) sub-system. - Miner(unsigned::MinerError), + Miner(unsigned::MinerError), /// An error in the on-chain fallback. OnChainFallback(onchain::Error), /// An error happened in the data provider. @@ -500,20 +500,20 @@ pub enum ElectionError { NoFallbackConfigured, } -impl From for ElectionError { +impl From for ElectionError { fn from(e: onchain::Error) -> Self { ElectionError::OnChainFallback(e) } } -impl From for ElectionError { +impl From for ElectionError { fn from(e: FeasibilityError) -> Self { ElectionError::Feasibility(e) } } -impl From for ElectionError { - fn from(e: unsigned::MinerError) -> Self { +impl From> for ElectionError { + fn from(e: unsigned::MinerError) -> Self { ElectionError::Miner(e) } } @@ -555,6 +555,7 @@ pub use pallet::*; #[frame_support::pallet] pub mod pallet { use super::*; + use frame_election_provider_support::NposSolver; use frame_support::{pallet_prelude::*, traits::EstimateCallFee}; use frame_system::pallet_prelude::*; @@ -592,10 +593,6 @@ pub mod pallet { /// The priority of the unsigned transaction submitted in the unsigned-phase #[pallet::constant] type MinerTxPriority: Get; - /// Maximum number of iteration of balancing that will be executed in the embedded miner of - /// the pallet. - #[pallet::constant] - type MinerMaxIterations: Get; /// Maximum weight that the miner should consume. /// @@ -668,6 +665,9 @@ pub mod pallet { /// Configuration for the fallback type Fallback: Get; + /// OCW election solution miner algorithm implementation. + type Solver: NposSolver; + /// Origin that can control this pallet. Note that any action taken by this origin (such) /// as providing an emergency solution is not checked. Thus, it must be a trusted origin. type ForceOrigin: EnsureOrigin; @@ -1298,7 +1298,7 @@ impl Pallet { /// /// Extracted for easier weight calculation. fn create_snapshot_external( - ) -> Result<(Vec, Vec>, u32), ElectionError> { + ) -> Result<(Vec, Vec>, u32), ElectionError> { let target_limit = >::max_value().saturated_into::(); let voter_limit = >::max_value().saturated_into::(); @@ -1328,7 +1328,7 @@ impl Pallet { /// /// This is a *self-weighing* function, it will register its own extra weight as /// [`DispatchClass::Mandatory`] with the system pallet. - pub fn create_snapshot() -> Result<(), ElectionError> { + pub fn create_snapshot() -> Result<(), ElectionError> { // this is self-weighing itself.. let (targets, voters, desired_targets) = Self::create_snapshot_external()?; @@ -1471,7 +1471,7 @@ impl Pallet { } /// On-chain fallback of election. - fn onchain_fallback() -> Result, ElectionError> { + fn onchain_fallback() -> Result, ElectionError> { > as ElectionProvider< T::AccountId, T::BlockNumber, @@ -1479,7 +1479,7 @@ impl Pallet { .map_err(Into::into) } - fn do_elect() -> Result, ElectionError> { + fn do_elect() -> Result, ElectionError> { // We have to unconditionally try finalizing the signed phase here. There are only two // possibilities: // @@ -1530,7 +1530,7 @@ impl Pallet { } impl ElectionProvider for Pallet { - type Error = ElectionError; + type Error = ElectionError; type DataProvider = T::DataProvider; fn elect() -> Result, Self::Error> { @@ -2013,7 +2013,10 @@ mod tests { roll_to(15); assert_eq!(MultiPhase::current_phase(), Phase::Signed); - let (solution, _) = MultiPhase::mine_solution(2).unwrap(); + // set the solution balancing to get the desired score. + crate::mock::Balancing::set(Some((2, 0))); + + let (solution, _) = MultiPhase::mine_solution::<::Solver>().unwrap(); // Default solution has a score of [50, 100, 5000]. assert_eq!(solution.score, [50, 100, 5000]); diff --git a/frame/election-provider-multi-phase/src/mock.rs b/frame/election-provider-multi-phase/src/mock.rs index 03dc6985f313c..e63c171f4dcc0 100644 --- a/frame/election-provider-multi-phase/src/mock.rs +++ b/frame/election-provider-multi-phase/src/mock.rs @@ -17,7 +17,7 @@ use super::*; use crate as multi_phase; -use frame_election_provider_support::{data_provider, ElectionDataProvider}; +use frame_election_provider_support::{data_provider, ElectionDataProvider, SequentialPhragmen}; pub use frame_support::{assert_noop, assert_ok}; use frame_support::{parameter_types, traits::Hooks, weights::Weight}; use multi_phase::unsigned::{IndexAssignmentOf, Voter}; @@ -31,7 +31,7 @@ use sp_core::{ }; use sp_npos_elections::{ assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, to_without_backing, - ElectionResult, EvaluateSupport, NposSolution, + ElectionResult, EvaluateSupport, ExtendedBalance, NposSolution, }; use sp_runtime::{ testing::Header, @@ -262,7 +262,6 @@ parameter_types! { pub static SignedDepositWeight: Balance = 0; pub static SignedRewardBase: Balance = 7; pub static SignedMaxWeight: Weight = BlockWeights::get().max_block; - pub static MinerMaxIterations: u32 = 5; pub static MinerTxPriority: u64 = 100; pub static SolutionImprovementThreshold: Perbill = Perbill::zero(); pub static OffchainRepeat: BlockNumber = 5; @@ -352,6 +351,10 @@ impl multi_phase::weights::WeightInfo for DualMockWeightInfo { } } +parameter_types! { + pub static Balancing: Option<(usize, ExtendedBalance)> = Some((0, 0)); +} + impl crate::Config for Runtime { type Event = Event; type Currency = Balances; @@ -360,7 +363,6 @@ impl crate::Config for Runtime { type UnsignedPhase = UnsignedPhase; type SolutionImprovementThreshold = SolutionImprovementThreshold; type OffchainRepeat = OffchainRepeat; - type MinerMaxIterations = MinerMaxIterations; type MinerMaxWeight = MinerMaxWeight; type MinerMaxLength = MinerMaxLength; type MinerTxPriority = MinerTxPriority; @@ -379,6 +381,7 @@ impl crate::Config for Runtime { type Fallback = Fallback; type ForceOrigin = frame_system::EnsureRoot; type Solution = TestNposSolution; + type Solver = SequentialPhragmen, Balancing>; } 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 8e140fa857b85..f83d72827852a 100644 --- a/frame/election-provider-multi-phase/src/signed.rs +++ b/frame/election-provider-multi-phase/src/signed.rs @@ -836,7 +836,8 @@ mod tests { roll_to(15); assert!(MultiPhase::current_phase().is_signed()); - let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = + MultiPhase::mine_solution::<::Solver>().unwrap(); let solution_weight = ::WeightInfo::feasibility_check( witness.voters, witness.targets, diff --git a/frame/election-provider-multi-phase/src/unsigned.rs b/frame/election-provider-multi-phase/src/unsigned.rs index aa01920fe490f..86d3a471bb7de 100644 --- a/frame/election-provider-multi-phase/src/unsigned.rs +++ b/frame/election-provider-multi-phase/src/unsigned.rs @@ -22,17 +22,17 @@ use crate::{ ReadySolution, RoundSnapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight, WeightInfo, }; -use codec::{Decode, Encode}; +use codec::Encode; +use frame_election_provider_support::{NposSolver, PerThing128}; use frame_support::{dispatch::DispatchResult, ensure, traits::Get}; use frame_system::offchain::SubmitTransaction; use sp_arithmetic::Perbill; use sp_npos_elections::{ assignment_ratio_to_staked_normalized, assignment_staked_to_ratio_normalized, is_score_better, - seq_phragmen, ElectionResult, NposSolution, + ElectionResult, NposSolution, }; use sp_runtime::{ offchain::storage::{MutateStorageError, StorageValueRef}, - traits::TrailingZeroInput, DispatchError, SaturatedConversion, }; use sp_std::{boxed::Box, cmp::Ordering, convert::TryFrom, vec::Vec}; @@ -61,8 +61,11 @@ pub type Assignment = /// runtime `T`. pub type IndexAssignmentOf = sp_npos_elections::IndexAssignmentOf>; -#[derive(Debug, Eq, PartialEq)] -pub enum MinerError { +/// Error type of the pallet's [`crate::Config::Solver`]. +pub type SolverErrorOf = <::Solver as NposSolver>::Error; +/// Error type for operations related to the OCW npos solution miner. +#[derive(frame_support::DebugNoBound, frame_support::PartialEqNoBound)] +pub enum MinerError { /// An internal error in the NPoS elections crate. NposElections(sp_npos_elections::Error), /// Snapshot data was unavailable unexpectedly. @@ -83,22 +86,24 @@ pub enum MinerError { FailedToStoreSolution, /// There are no more voters to remove to trim the solution. NoMoreVoters, + /// An error from the solver. + Solver(SolverErrorOf), } -impl From for MinerError { +impl From for MinerError { fn from(e: sp_npos_elections::Error) -> Self { MinerError::NposElections(e) } } -impl From for MinerError { +impl From for MinerError { fn from(e: FeasibilityError) -> Self { MinerError::Feasibility(e) } } /// Save a given call into OCW storage. -fn save_solution(call: &Call) -> Result<(), MinerError> { +fn save_solution(call: &Call) -> Result<(), MinerError> { log!(debug, "saving a call to the offchain storage."); let storage = StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL); match storage.mutate::<_, (), _>(|_| Ok(call.clone())) { @@ -116,7 +121,7 @@ fn save_solution(call: &Call) -> Result<(), MinerError> { } /// Get a saved solution from OCW storage if it exists. -fn restore_solution() -> Result, MinerError> { +fn restore_solution() -> Result, MinerError> { StorageValueRef::persistent(&OFFCHAIN_CACHED_CALL) .get() .ok() @@ -149,7 +154,7 @@ fn ocw_solution_exists() -> bool { impl Pallet { /// Attempt to restore a solution from cache. Otherwise, compute it fresh. Either way, submit /// if our call's score is greater than that of the cached solution. - pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> { + pub fn restore_or_compute_then_maybe_submit() -> Result<(), MinerError> { log!(debug, "miner attempting to restore or compute an unsigned solution."); let call = restore_solution::() @@ -163,7 +168,7 @@ impl Pallet { Err(MinerError::SolutionCallInvalid) } }) - .or_else::(|error| { + .or_else::, _>(|error| { log!(debug, "restoring solution failed due to {:?}", error); match error { MinerError::NoStoredSolution => { @@ -194,7 +199,7 @@ impl Pallet { } /// Mine a new solution, cache it, and submit it back to the chain as an unsigned transaction. - pub fn mine_check_save_submit() -> Result<(), MinerError> { + pub fn mine_check_save_submit() -> Result<(), MinerError> { log!(debug, "miner attempting to compute an unsigned solution."); let call = Self::mine_checked_call()?; @@ -203,10 +208,9 @@ impl Pallet { } /// Mine a new solution as a call. Performs all checks. - pub fn mine_checked_call() -> Result, MinerError> { - let iters = Self::get_balancing_iters(); + pub fn mine_checked_call() -> Result, MinerError> { // get the solution, with a load of checks to ensure if submitted, IT IS ABSOLUTELY VALID. - let (raw_solution, witness) = Self::mine_and_check(iters)?; + let (raw_solution, witness) = Self::mine_and_check()?; let score = raw_solution.score.clone(); let call: Call = Call::submit_unsigned(Box::new(raw_solution), witness).into(); @@ -221,7 +225,7 @@ impl Pallet { Ok(call) } - fn submit_call(call: Call) -> Result<(), MinerError> { + fn submit_call(call: Call) -> Result<(), MinerError> { log!(debug, "miner submitting a solution as an unsigned transaction"); SubmitTransaction::>::submit_unsigned_transaction(call.into()) @@ -234,7 +238,7 @@ impl Pallet { pub fn basic_checks( raw_solution: &RawSolution>, solution_type: &str, - ) -> Result<(), MinerError> { + ) -> 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(err) @@ -257,38 +261,37 @@ impl Pallet { /// If you want a checked solution and submit it at the same time, use /// [`Pallet::mine_check_save_submit`]. pub fn mine_and_check( - iters: usize, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { - let (raw_solution, witness) = Self::mine_solution(iters)?; + ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + let (raw_solution, witness) = Self::mine_solution::()?; Self::basic_checks(&raw_solution, "mined")?; Ok((raw_solution, witness)) } /// Mine a new npos solution. - pub fn mine_solution( - iters: usize, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + /// + /// The Npos Solver type, `S`, must have the same AccountId and Error type as the + /// [`crate::Config::Solver`] in order to create a unified return type. + pub fn mine_solution( + ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> + where + S: NposSolver>, + { let RoundSnapshot { voters, targets } = Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?; let desired_targets = Self::desired_targets().ok_or(MinerError::SnapshotUnAvailable)?; - seq_phragmen::<_, SolutionAccuracyOf>( - desired_targets as usize, - targets, - voters, - Some((iters, 0)), - ) - .map_err(Into::into) - .and_then(Self::prepare_election_result) + S::solve(desired_targets as usize, targets, voters) + .map_err(|e| MinerError::Solver::(e)) + .and_then(|e| Self::prepare_election_result::(e)) } /// Convert a raw solution from [`sp_npos_elections::ElectionResult`] to [`RawSolution`], which /// is ready to be submitted to the chain. /// /// Will always reduce the solution as well. - pub fn prepare_election_result( - election_result: ElectionResult>, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + pub fn prepare_election_result( + election_result: ElectionResult, + ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { // NOTE: This code path is generally not optimized as it is run offchain. Could use some at // some point though. @@ -378,23 +381,6 @@ impl Pallet { Ok((RawSolution { solution, score, round }, size)) } - /// Get a random number of iterations to run the balancing in the OCW. - /// - /// Uses the offchain seed to generate a random number, maxed with - /// [`Config::MinerMaxIterations`]. - pub fn get_balancing_iters() -> usize { - match T::MinerMaxIterations::get() { - 0 => 0, - max @ _ => { - let seed = sp_io::offchain::random_seed(); - let random = ::decode(&mut TrailingZeroInput::new(seed.as_ref())) - .expect("input is padded with zeroes; qed") % - max.saturating_add(1); - random as usize - }, - } - } - /// Greedily reduce the size of the solution to fit into the block w.r.t. weight. /// /// The weight of the solution is foremost a function of the number of voters (i.e. @@ -448,7 +434,7 @@ impl Pallet { max_allowed_length: u32, assignments: &mut Vec>, encoded_size_of: impl Fn(&[IndexAssignmentOf]) -> Result, - ) -> Result<(), MinerError> { + ) -> Result<(), MinerError> { // Perform a binary search for the max subset of which can fit into the allowed // length. Having discovered that, we can truncate efficiently. let max_allowed_length: usize = max_allowed_length.saturated_into(); @@ -584,7 +570,7 @@ impl Pallet { /// /// Returns `Ok(())` if offchain worker limit is respected, `Err(reason)` otherwise. If `Ok()` /// is returned, `now` is written in storage and will be used in further calls as the baseline. - pub fn ensure_offchain_repeat_frequency(now: T::BlockNumber) -> Result<(), MinerError> { + pub fn ensure_offchain_repeat_frequency(now: T::BlockNumber) -> Result<(), MinerError> { let threshold = T::OffchainRepeat::get(); let last_block = StorageValueRef::persistent(&OFFCHAIN_LAST_BLOCK); @@ -761,6 +747,7 @@ mod tests { CurrentPhase, InvalidTransaction, Phase, QueuedSolution, TransactionSource, TransactionValidityError, }; + use codec::Decode; use frame_benchmarking::Zero; use frame_support::{assert_noop, assert_ok, dispatch::Dispatchable, traits::OffchainWorker}; use sp_npos_elections::IndexAssignment; @@ -975,7 +962,8 @@ mod tests { assert_eq!(MultiPhase::desired_targets().unwrap(), 2); // mine seq_phragmen solution with 2 iters. - let (solution, witness) = MultiPhase::mine_solution(2).unwrap(); + let (solution, witness) = + MultiPhase::mine_solution::<::Solver>().unwrap(); // ensure this solution is valid. assert!(MultiPhase::queued_solution().is_none()); @@ -993,7 +981,8 @@ mod tests { roll_to(25); assert!(MultiPhase::current_phase().is_unsigned()); - let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = + MultiPhase::mine_solution::<::Solver>().unwrap(); let solution_weight = ::WeightInfo::submit_unsigned( witness.voters, witness.targets, @@ -1007,7 +996,8 @@ mod tests { // now reduce the max weight ::set(25); - let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = + MultiPhase::mine_solution::<::Solver>().unwrap(); let solution_weight = ::WeightInfo::submit_unsigned( witness.voters, witness.targets, @@ -1359,7 +1349,7 @@ mod tests { // OCW must have submitted now let encoded = pool.read().transactions[0].clone(); - let extrinsic: Extrinsic = Decode::decode(&mut &*encoded).unwrap(); + let extrinsic: Extrinsic = codec::Decode::decode(&mut &*encoded).unwrap(); let call = extrinsic.call; assert!(matches!(call, OuterCall::MultiPhase(Call::submit_unsigned(..)))); }) @@ -1534,14 +1524,14 @@ mod tests { roll_to(25); // how long would the default solution be? - let solution = MultiPhase::mine_solution(0).unwrap(); + let solution = MultiPhase::mine_solution::<::Solver>().unwrap(); let max_length = ::MinerMaxLength::get(); let solution_size = solution.0.solution.encoded_size(); assert!(solution_size <= max_length as usize); // now set the max size to less than the actual size and regenerate ::MinerMaxLength::set(solution_size as u32 - 1); - let solution = MultiPhase::mine_solution(0).unwrap(); + let solution = MultiPhase::mine_solution::<::Solver>().unwrap(); let max_length = ::MinerMaxLength::get(); let solution_size = solution.0.solution.encoded_size(); assert!(solution_size <= max_length as usize); diff --git a/frame/election-provider-support/src/lib.rs b/frame/election-provider-support/src/lib.rs index f2d11911c9b3e..d2c4b1053cc6d 100644 --- a/frame/election-provider-support/src/lib.rs +++ b/frame/election-provider-support/src/lib.rs @@ -161,12 +161,14 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod onchain; +use frame_support::traits::Get; use sp_std::{fmt::Debug, prelude::*}; /// Re-export some type as they are used in the interface. pub use sp_arithmetic::PerThing; pub use sp_npos_elections::{ - Assignment, ExtendedBalance, PerThing128, Support, Supports, VoteWeight, + Assignment, ElectionResult, ExtendedBalance, IdentifierT, PerThing128, Support, Supports, + VoteWeight, }; /// Types that are used by the data provider trait. @@ -294,3 +296,69 @@ impl ElectionProvider for () { Err("<() as ElectionProvider> cannot do anything.") } } + +/// Something that can compute the result to an NPoS solution. +pub trait NposSolver { + /// The account identifier type of this solver. + type AccountId: sp_npos_elections::IdentifierT; + /// The accuracy of this solver. This will affect the accuracy of the output. + type Accuracy: PerThing128; + /// The error type of this implementation. + type Error: sp_std::fmt::Debug + sp_std::cmp::PartialEq; + + /// Solve an NPoS solution with the given `voters`, `targets`, and select `to_elect` count + /// of `targets`. + fn solve( + to_elect: usize, + targets: Vec, + voters: Vec<(Self::AccountId, VoteWeight, Vec)>, + ) -> Result, Self::Error>; +} + +/// A wrapper for [`sp_npos_elections::seq_phragmen`] that implements [`super::NposSolver`]. See the +/// documentation of [`sp_npos_elections::seq_phragmen`] for more info. +pub struct SequentialPhragmen( + sp_std::marker::PhantomData<(AccountId, Accuracy, Balancing)>, +); + +impl< + AccountId: IdentifierT, + Accuracy: PerThing128, + Balancing: Get>, + > NposSolver for SequentialPhragmen +{ + type AccountId = AccountId; + type Accuracy = Accuracy; + type Error = sp_npos_elections::Error; + fn solve( + winners: usize, + targets: Vec, + voters: Vec<(Self::AccountId, VoteWeight, Vec)>, + ) -> Result, Self::Error> { + sp_npos_elections::seq_phragmen(winners, targets, voters, Balancing::get()) + } +} + +/// A wrapper for [`sp_npos_elections::phragmms`] that implements [`NposSolver`]. See the +/// documentation of [`sp_npos_elections::phragmms`] for more info. +pub struct PhragMMS( + sp_std::marker::PhantomData<(AccountId, Accuracy, Balancing)>, +); + +impl< + AccountId: IdentifierT, + Accuracy: PerThing128, + Balancing: Get>, + > NposSolver for PhragMMS +{ + type AccountId = AccountId; + type Accuracy = Accuracy; + type Error = sp_npos_elections::Error; + fn solve( + winners: usize, + targets: Vec, + voters: Vec<(Self::AccountId, VoteWeight, Vec)>, + ) -> Result, Self::Error> { + sp_npos_elections::phragmms(winners, targets, voters, Balancing::get()) + } +} diff --git a/primitives/npos-elections/Cargo.toml b/primitives/npos-elections/Cargo.toml index 0d1834a94ad93..5c6e5c1b13d53 100644 --- a/primitives/npos-elections/Cargo.toml +++ b/primitives/npos-elections/Cargo.toml @@ -19,11 +19,11 @@ sp-std = { version = "4.0.0-dev", default-features = false, path = "../std" } sp-npos-elections-solution-type = { version = "4.0.0-dev", path = "./solution-type" } sp-arithmetic = { version = "4.0.0-dev", default-features = false, path = "../arithmetic" } sp-core = { version = "4.0.0-dev", default-features = false, path = "../core" } +sp-runtime = { version = "4.0.0-dev", path = "../runtime", default-features = false } [dev-dependencies] substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } rand = "0.7.3" -sp-runtime = { version = "4.0.0-dev", path = "../runtime" } [features] default = ["std"] @@ -34,4 +34,5 @@ std = [ "sp-std/std", "sp-arithmetic/std", "sp-core/std", + "sp-runtime/std", ] diff --git a/primitives/npos-elections/src/phragmen.rs b/primitives/npos-elections/src/phragmen.rs index 0f9b144919761..5ed472284351a 100644 --- a/primitives/npos-elections/src/phragmen.rs +++ b/primitives/npos-elections/src/phragmen.rs @@ -68,16 +68,16 @@ const DEN: ExtendedBalance = ExtendedBalance::max_value(); /// check where t is the standard threshold. The underlying algorithm is sound, but the conversions /// between numeric types can be lossy. pub fn seq_phragmen( - rounds: usize, - initial_candidates: Vec, - initial_voters: Vec<(AccountId, VoteWeight, Vec)>, - balance: Option<(usize, ExtendedBalance)>, + to_elect: usize, + candidates: Vec, + voters: Vec<(AccountId, VoteWeight, Vec)>, + balancing: Option<(usize, ExtendedBalance)>, ) -> Result, crate::Error> { - let (candidates, voters) = setup_inputs(initial_candidates, initial_voters); + let (candidates, voters) = setup_inputs(candidates, voters); - let (candidates, mut voters) = seq_phragmen_core::(rounds, candidates, voters)?; + let (candidates, mut voters) = seq_phragmen_core::(to_elect, candidates, voters)?; - if let Some((iterations, tolerance)) = balance { + if let Some((iterations, tolerance)) = balancing { // NOTE: might create zero-edges, but we will strip them again when we convert voter into // assignment. let _iters = balancing::balance::(&mut voters, iterations, tolerance); @@ -87,7 +87,7 @@ pub fn seq_phragmen( .into_iter() .filter(|c_ptr| c_ptr.borrow().elected) // defensive only: seq-phragmen-core returns only up to rounds. - .take(rounds) + .take(to_elect) .collect::>(); // sort winners based on desirability. @@ -116,12 +116,12 @@ pub fn seq_phragmen( /// This can only fail if the normalization fails. // To create the inputs needed for this function, see [`crate::setup_inputs`]. pub fn seq_phragmen_core( - rounds: usize, + to_elect: usize, candidates: Vec>, mut voters: Vec>, ) -> Result<(Vec>, Vec>), crate::Error> { // we have already checked that we have more candidates than minimum_candidate_count. - let to_elect = rounds.min(candidates.len()); + let to_elect = to_elect.min(candidates.len()); // main election loop for round in 0..to_elect { diff --git a/primitives/npos-elections/src/phragmms.rs b/primitives/npos-elections/src/phragmms.rs index 4e7316d5778b3..e9135a13190c6 100644 --- a/primitives/npos-elections/src/phragmms.rs +++ b/primitives/npos-elections/src/phragmms.rs @@ -43,11 +43,11 @@ use sp_std::{prelude::*, rc::Rc}; /// `expect` this to return `Ok`. pub fn phragmms( to_elect: usize, - initial_candidates: Vec, - initial_voters: Vec<(AccountId, VoteWeight, Vec)>, - balancing_config: Option<(usize, ExtendedBalance)>, -) -> Result, &'static str> { - let (candidates, mut voters) = setup_inputs(initial_candidates, initial_voters); + candidates: Vec, + voters: Vec<(AccountId, VoteWeight, Vec)>, + balancing: Option<(usize, ExtendedBalance)>, +) -> Result, crate::Error> { + let (candidates, mut voters) = setup_inputs(candidates, voters); let mut winners = vec![]; for round in 0..to_elect { @@ -58,7 +58,7 @@ pub fn phragmms( round_winner.borrow_mut().elected = true; winners.push(round_winner); - if let Some((iterations, tolerance)) = balancing_config { + if let Some((iterations, tolerance)) = balancing { balance(&mut voters, iterations, tolerance); } } else { @@ -68,7 +68,11 @@ pub fn phragmms( let mut assignments = voters.into_iter().filter_map(|v| v.into_assignment()).collect::>(); - let _ = assignments.iter_mut().map(|a| a.try_normalize()).collect::>()?; + let _ = assignments + .iter_mut() + .map(|a| a.try_normalize()) + .collect::>() + .map_err(|e| crate::Error::ArithmeticError(e))?; let winners = winners .into_iter() .map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake))