diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 24c0bfc3fa..bc33d4a5fe 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -9141,26 +9141,12 @@ dependencies = [ "serde", "sp-arithmetic", "sp-core", - "sp-npos-elections-compact", + "sp-npos-elections-solution-type", "sp-runtime", "sp-std", "substrate-test-utils", ] -[[package]] -name = "sp-npos-elections-compact" -version = "4.0.0-dev" -dependencies = [ - "parity-scale-codec", - "proc-macro-crate 1.0.0", - "proc-macro2", - "quote", - "sp-arithmetic", - "sp-npos-elections", - "syn", - "trybuild", -] - [[package]] name = "sp-npos-elections-fuzzer" version = "2.0.0-alpha.5" @@ -9175,6 +9161,20 @@ dependencies = [ "structopt", ] +[[package]] +name = "sp-npos-elections-solution-type" +version = "4.0.0-dev" +dependencies = [ + "parity-scale-codec", + "proc-macro-crate 1.0.0", + "proc-macro2", + "quote", + "sp-arithmetic", + "sp-npos-elections", + "syn", + "trybuild", +] + [[package]] name = "sp-offchain" version = "4.0.0-dev" diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml index 2834344153..ec5620e8c3 100644 --- a/substrate/Cargo.toml +++ b/substrate/Cargo.toml @@ -156,7 +156,7 @@ members = [ "primitives/keystore", "primitives/maybe-compressed-blob", "primitives/npos-elections", - "primitives/npos-elections/compact", + "primitives/npos-elections/solution-type", "primitives/npos-elections/fuzzer", "primitives/offchain", "primitives/panic-handler", diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 7466c940d6..af16ea0f8a 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -548,15 +548,14 @@ parameter_types! { sp_npos_elections::generate_solution_type!( #[compact] - pub struct NposCompactSolution16::< + pub struct NposSolution16::< VoterIndex = u32, TargetIndex = u16, Accuracy = sp_runtime::PerU16, >(16) ); -pub const MAX_NOMINATIONS: u32 = - ::LIMIT as u32; +pub const MAX_NOMINATIONS: u32 = ::LIMIT as u32; /// The numbers configured here should always be more than the the maximum limits of staking pallet /// to ensure election snapshot will not run out of memory. @@ -593,7 +592,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime { type RewardHandler = (); // nothing to do upon rewards type DataProvider = Staking; type OnChainAccuracy = Perbill; - type CompactSolution = NposCompactSolution16; + type Solution = NposSolution16; type Fallback = Fallback; type WeightInfo = pallet_election_provider_multi_phase::weights::SubstrateWeight; type ForceOrigin = EnsureRootOrHalfCouncil; diff --git a/substrate/frame/election-provider-multi-phase/Cargo.toml b/substrate/frame/election-provider-multi-phase/Cargo.toml index c78fba0a56..74c1a01084 100644 --- a/substrate/frame/election-provider-multi-phase/Cargo.toml +++ b/substrate/frame/election-provider-multi-phase/Cargo.toml @@ -40,7 +40,7 @@ hex-literal = "0.3.1" substrate-test-utils = { version = "4.0.0-dev", path = "../../test-utils" } sp-core = { version = "4.0.0-dev", default-features = false, path = "../../primitives/core" } sp-io = { version = "4.0.0-dev", path = "../../primitives/io" } -sp-npos-elections = { version = "4.0.0-dev", default-features = false, features = [ "mocks" ], path = "../../primitives/npos-elections" } +sp-npos-elections = { version = "4.0.0-dev", default-features = false, path = "../../primitives/npos-elections" } sp-tracing = { version = "4.0.0-dev", path = "../../primitives/tracing" } frame-election-provider-support = { version = "4.0.0-dev", features = ["runtime-benchmarks"], path = "../election-provider-support" } pallet-balances = { version = "4.0.0-dev", path = "../balances" } @@ -67,6 +67,5 @@ std = [ runtime-benchmarks = [ "frame-benchmarking", "rand", - "sp-npos-elections/mocks", ] try-runtime = ["frame-support/try-runtime"] diff --git a/substrate/frame/election-provider-multi-phase/src/benchmarking.rs b/substrate/frame/election-provider-multi-phase/src/benchmarking.rs index cc7d99a854..da08722a6a 100644 --- a/substrate/frame/election-provider-multi-phase/src/benchmarking.rs +++ b/substrate/frame/election-provider-multi-phase/src/benchmarking.rs @@ -40,15 +40,15 @@ fn solution_with_size( size: SolutionOrSnapshotSize, active_voters_count: u32, desired_targets: u32, -) -> Result>, &'static str> { +) -> Result>, &'static str> { ensure!(size.targets >= desired_targets, "must have enough targets"); ensure!( - size.targets >= (>::LIMIT * 2) as u32, + size.targets >= (>::LIMIT * 2) as u32, "must have enough targets for unique votes." ); ensure!(size.voters >= active_voters_count, "must have enough voters"); ensure!( - (>::LIMIT as u32) < desired_targets, + (>::LIMIT as u32) < desired_targets, "must have enough winners to give them votes." ); @@ -75,7 +75,7 @@ fn solution_with_size( // chose a random subset of winners. let winner_votes = winners .as_slice() - .choose_multiple(&mut rng, >::LIMIT) + .choose_multiple(&mut rng, >::LIMIT) .cloned() .collect::>(); let voter = frame_benchmarking::account::("Voter", i, SEED); @@ -92,7 +92,7 @@ fn solution_with_size( let rest_voters = (active_voters_count..size.voters) .map(|i| { let votes = (&non_winners) - .choose_multiple(&mut rng, >::LIMIT) + .choose_multiple(&mut rng, >::LIMIT) .cloned() .collect::>(); let voter = frame_benchmarking::account::("Voter", i, SEED); @@ -129,25 +129,25 @@ fn solution_with_size( let assignments = active_voters .iter() .map(|(voter, _stake, votes)| { - let percent_per_edge: InnerOf> = + let percent_per_edge: InnerOf> = (100 / votes.len()).try_into().unwrap_or_else(|_| panic!("failed to convert")); crate::unsigned::Assignment:: { who: voter.clone(), distribution: votes .iter() - .map(|t| (t.clone(), >::from_percent(percent_per_edge))) + .map(|t| (t.clone(), >::from_percent(percent_per_edge))) .collect::>(), } }) .collect::>(); - let compact = - >::from_assignment(&assignments, &voter_index, &target_index).unwrap(); - let score = compact.clone().score(&winners, stake_of, voter_at, target_at).unwrap(); + let solution = + >::from_assignment(&assignments, &voter_index, &target_index).unwrap(); + let score = solution.clone().score(&winners, stake_of, voter_at, target_at).unwrap(); let round = >::round(); assert!(score[0] > 0, "score is zero, this probably means that the stakes are not set."); - Ok(RawSolution { compact, score, round }) + Ok(RawSolution { solution, score, round }) } fn set_up_data_provider(v: u32, t: u32) { @@ -265,7 +265,7 @@ frame_benchmarking::benchmarks! { let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. 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 + // number of assignments, i.e. solution.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]; // number of desired targets. Must be a subset of `t` component. @@ -308,11 +308,11 @@ frame_benchmarking::benchmarks! { let mut signed_submissions = SignedSubmissions::::get(); for i in 0..c { - let solution = RawSolution { + let raw_solution = RawSolution { score: [(10_000_000 + i).into(), 0, 0], ..Default::default() }; - let signed_submission = SignedSubmission { solution, ..Default::default() }; + let signed_submission = SignedSubmission { raw_solution, ..Default::default() }; signed_submissions.insert(signed_submission); } signed_submissions.put(); @@ -330,7 +330,7 @@ frame_benchmarking::benchmarks! { let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. 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 + // number of assignments, i.e. solution.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]; @@ -369,7 +369,7 @@ frame_benchmarking::benchmarks! { let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. 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 + // number of assignments, i.e. solution.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]; // number of desired targets. Must be a subset of `t` component. @@ -378,8 +378,8 @@ frame_benchmarking::benchmarks! { let size = SolutionOrSnapshotSize { voters: v, targets: t }; let raw_solution = solution_with_size::(size, a, d)?; - assert_eq!(raw_solution.compact.voter_count() as u32, a); - assert_eq!(raw_solution.compact.unique_targets().len() as u32, d); + assert_eq!(raw_solution.solution.voter_count() as u32, a); + assert_eq!(raw_solution.solution.unique_targets().len() as u32, d); // encode the most significant storage item that needs to be decoded in the dispatch. let encoded_snapshot = >::snapshot().unwrap().encode(); @@ -447,7 +447,7 @@ frame_benchmarking::benchmarks! { let v in (T::BenchmarkingConfig::VOTERS[0]) .. T::BenchmarkingConfig::VOTERS[1]; // number of targets in snapshot. 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 + // number of assignments, i.e. solution.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]; @@ -461,11 +461,11 @@ frame_benchmarking::benchmarks! { // 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::(witness, a, d)?; + let RawSolution { solution, .. } = solution_with_size::(witness, a, d)?; let RoundSnapshot { voters, targets } = MultiPhase::::snapshot().unwrap(); let voter_at = helpers::voter_at_fn::(&voters); let target_at = helpers::target_at_fn::(&targets); - let mut assignments = compact.into_assignment(voter_at, target_at).unwrap(); + let mut assignments = solution.into_assignment(voter_at, target_at).unwrap(); // make a voter cache and some helper functions for access let cache = helpers::generate_voter_cache::(&voters); @@ -488,7 +488,7 @@ frame_benchmarking::benchmarks! { .unwrap(); let encoded_size_of = |assignments: &[IndexAssignmentOf]| { - CompactOf::::try_from(assignments).map(|compact| compact.encoded_size()) + SolutionOf::::try_from(assignments).map(|solution| solution.encoded_size()) }; let desired_size = Percent::from_percent(100 - f.saturated_into::()) @@ -501,8 +501,8 @@ frame_benchmarking::benchmarks! { &encoded_size_of, ).unwrap(); } verify { - let compact = CompactOf::::try_from(index_assignments.as_slice()).unwrap(); - let encoding = compact.encode(); + let solution = SolutionOf::::try_from(index_assignments.as_slice()).unwrap(); + let encoding = solution.encode(); log!( trace, "encoded size prediction = {}", diff --git a/substrate/frame/election-provider-multi-phase/src/helpers.rs b/substrate/frame/election-provider-multi-phase/src/helpers.rs index 0abf448a45..72b1b23f27 100644 --- a/substrate/frame/election-provider-multi-phase/src/helpers.rs +++ b/substrate/frame/election-provider-multi-phase/src/helpers.rs @@ -17,7 +17,7 @@ //! Some helper functions/macros for this crate. -use super::{CompactTargetIndexOf, CompactVoterIndexOf, Config, VoteWeight}; +use super::{Config, SolutionTargetIndexOf, SolutionVoterIndexOf, VoteWeight}; use sp_std::{collections::btree_map::BTreeMap, convert::TryInto, prelude::*}; #[macro_export] @@ -49,18 +49,18 @@ pub fn generate_voter_cache( /// 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`. +/// The returning index type is the same as the one defined in `T::Solution::Voter`. /// /// ## Warning /// /// Note that this will represent the snapshot data from which the `cache` is generated. pub fn voter_index_fn( cache: &BTreeMap, -) -> impl Fn(&T::AccountId) -> Option> + '_ { +) -> impl Fn(&T::AccountId) -> Option> + '_ { move |who| { cache .get(who) - .and_then(|i| >>::try_into(*i).ok()) + .and_then(|i| >>::try_into(*i).ok()) } } @@ -70,11 +70,11 @@ pub fn voter_index_fn( /// borrowed. pub fn voter_index_fn_owned( cache: BTreeMap, -) -> impl Fn(&T::AccountId) -> Option> { +) -> impl Fn(&T::AccountId) -> Option> { move |who| { cache .get(who) - .and_then(|i| >>::try_into(*i).ok()) + .and_then(|i| >>::try_into(*i).ok()) } } @@ -98,37 +98,37 @@ pub fn voter_index_fn_usize( #[cfg(test)] pub fn voter_index_fn_linear( snapshot: &Vec<(T::AccountId, VoteWeight, Vec)>, -) -> impl Fn(&T::AccountId) -> Option> + '_ { +) -> impl Fn(&T::AccountId) -> Option> + '_ { move |who| { snapshot .iter() .position(|(x, _, _)| x == who) - .and_then(|i| >>::try_into(i).ok()) + .and_then(|i| >>::try_into(i).ok()) } } /// 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`. +/// The returned index type is the same as the one defined in `T::Solution::Target`. /// /// Note: to the extent possible, the returned function should be cached and reused. Producing that /// function requires a `O(n log n)` data transform. Each invocation of that function completes /// in `O(log n)`. pub fn target_index_fn( snapshot: &Vec, -) -> impl Fn(&T::AccountId) -> Option> + '_ { +) -> impl Fn(&T::AccountId) -> Option> + '_ { let cache: BTreeMap<_, _> = snapshot.iter().enumerate().map(|(idx, account_id)| (account_id, idx)).collect(); move |who| { cache .get(who) - .and_then(|i| >>::try_into(*i).ok()) + .and_then(|i| >>::try_into(*i).ok()) } } /// Create a function the returns the index to a target in the snapshot. /// -/// The returned index type is the same as the one defined in `T::CompactSolution::Target`. +/// The returned index type is the same as the one defined in `T::Solution::Target`. /// /// ## Warning /// @@ -136,34 +136,34 @@ pub fn target_index_fn( #[cfg(test)] pub fn target_index_fn_linear( snapshot: &Vec, -) -> impl Fn(&T::AccountId) -> Option> + '_ { +) -> impl Fn(&T::AccountId) -> Option> + '_ { move |who| { snapshot .iter() .position(|x| x == who) - .and_then(|i| >>::try_into(i).ok()) + .and_then(|i| >>::try_into(i).ok()) } } -/// Create a function that can map a voter index ([`CompactVoterIndexOf`]) to the actual voter +/// Create a function that can map a voter index ([`SolutionVoterIndexOf`]) to the actual voter /// account using a linearly indexible snapshot. pub fn voter_at_fn( snapshot: &Vec<(T::AccountId, VoteWeight, Vec)>, -) -> impl Fn(CompactVoterIndexOf) -> Option + '_ { +) -> impl Fn(SolutionVoterIndexOf) -> Option + '_ { move |i| { - as TryInto>::try_into(i) + as TryInto>::try_into(i) .ok() .and_then(|i| snapshot.get(i).map(|(x, _, _)| x).cloned()) } } -/// Create a function that can map a target index ([`CompactTargetIndexOf`]) to the actual target +/// Create a function that can map a target index ([`SolutionTargetIndexOf`]) to the actual target /// account using a linearly indexible snapshot. pub fn target_at_fn( snapshot: &Vec, -) -> impl Fn(CompactTargetIndexOf) -> Option + '_ { +) -> impl Fn(SolutionTargetIndexOf) -> Option + '_ { move |i| { - as TryInto>::try_into(i) + as TryInto>::try_into(i) .ok() .and_then(|i| snapshot.get(i).cloned()) } diff --git a/substrate/frame/election-provider-multi-phase/src/lib.rs b/substrate/frame/election-provider-multi-phase/src/lib.rs index 44a4d66ca4..0c7fe170ec 100644 --- a/substrate/frame/election-provider-multi-phase/src/lib.rs +++ b/substrate/frame/election-provider-multi-phase/src/lib.rs @@ -148,7 +148,7 @@ //! //! The accuracy of the election is configured via two trait parameters. namely, //! [`OnChainAccuracyOf`] dictates the accuracy used to compute the on-chain fallback election and -//! [`CompactAccuracyOf`] is the accuracy that the submitted solutions must adhere to. +//! [`SolutionAccuracyOf`] is the accuracy that the submitted solutions must adhere to. //! //! Note that both accuracies are of great importance. The offchain solution should be as small as //! possible, reducing solutions size/weight. The on-chain solution can use more space for accuracy, @@ -212,7 +212,7 @@ //! there is a tie. Even more harsh should be to enforce the bound of the `reduce` algorithm. //! //! **Make the number of nominators configurable from the runtime**. Remove `sp_npos_elections` -//! dependency from staking and the compact solution type. It should be generated at runtime, there +//! dependency from staking and the solution type. It should be generated at runtime, there //! it should be encoded how many votes each nominators have. Essentially translate //! to this pallet. //! @@ -241,7 +241,7 @@ use sp_arithmetic::{ UpperOf, }; use sp_npos_elections::{ - assignment_ratio_to_staked_normalized, CompactSolution, ElectionScore, EvaluateSupport, + assignment_ratio_to_staked_normalized, ElectionScore, EvaluateSupport, NposSolution, PerThing128, Supports, VoteWeight, }; use sp_runtime::{ @@ -273,15 +273,15 @@ pub use signed::{ }; pub use weights::WeightInfo; -/// The compact solution type used by this crate. -pub type CompactOf = ::CompactSolution; +/// The solution type used by this crate. +pub type SolutionOf = ::Solution; -/// The voter index. Derived from [`CompactOf`]. -pub type CompactVoterIndexOf = as CompactSolution>::Voter; -/// The target index. Derived from [`CompactOf`]. -pub type CompactTargetIndexOf = as CompactSolution>::Target; -/// The accuracy of the election, when submitted from offchain. Derived from [`CompactOf`]. -pub type CompactAccuracyOf = as CompactSolution>::Accuracy; +/// The voter index. Derived from [`SolutionOf`]. +pub type SolutionVoterIndexOf = as NposSolution>::VoterIndex; +/// The target index. Derived from [`SolutionOf`]. +pub type SolutionTargetIndexOf = as NposSolution>::TargetIndex; +/// The accuracy of the election, when submitted from offchain. Derived from [`SolutionOf`]. +pub type SolutionAccuracyOf = as NposSolution>::Accuracy; /// The accuracy of the election, when computed on-chain. Equal to [`Config::OnChainAccuracy`]. pub type OnChainAccuracyOf = ::OnChainAccuracy; @@ -422,11 +422,11 @@ impl Default for ElectionCompute { /// This is what will get submitted to the chain. /// /// Such a solution should never become effective in anyway before being checked by the -/// `Pallet::feasibility_check` +/// `Pallet::feasibility_check`. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, PartialOrd, Ord)] -pub struct RawSolution { - /// Compact election edges. - pub compact: C, +pub struct RawSolution { + /// the solution itself. + pub solution: S, /// The _claimed_ score of the solution. pub score: ElectionScore, /// The round at which this solution should be submitted. @@ -436,7 +436,7 @@ pub struct RawSolution { impl Default for RawSolution { fn default() -> Self { // Round 0 is always invalid, only set this to 1. - Self { round: 1, compact: Default::default(), score: Default::default() } + Self { round: 1, solution: Default::default(), score: Default::default() } } } @@ -651,15 +651,15 @@ pub mod pallet { /// Something that will provide the election data. type DataProvider: ElectionDataProvider; - /// The compact solution type - type CompactSolution: codec::Codec + /// The solution type. + type Solution: codec::Codec + Default + PartialEq + Eq + Clone + sp_std::fmt::Debug + Ord - + CompactSolution; + + NposSolution; /// Accuracy used for fallback on-chain election. type OnChainAccuracy: PerThing128; @@ -790,12 +790,12 @@ pub mod pallet { use sp_std::mem::size_of; // The index type of both voters and targets need to be smaller than that of usize (very // unlikely to be the case, but anyhow). - assert!(size_of::>() <= size_of::()); - assert!(size_of::>() <= size_of::()); + assert!(size_of::>() <= size_of::()); + assert!(size_of::>() <= size_of::()); // ---------------------------- // Based on the requirements of [`sp_npos_elections::Assignment::try_normalize`]. - let max_vote: usize = as CompactSolution>::LIMIT; + let max_vote: usize = as NposSolution>::LIMIT; // 1. Maximum sum of [ChainAccuracy; 16] must fit into `UpperOf`.. let maximum_chain_accuracy: Vec>> = (0..max_vote) @@ -809,26 +809,26 @@ pub mod pallet { .iter() .fold(Zero::zero(), |acc, x| acc.checked_add(x).unwrap()); - // 2. Maximum sum of [CompactAccuracy; 16] must fit into `UpperOf`. - let maximum_chain_accuracy: Vec>> = (0..max_vote) + // 2. Maximum sum of [SolutionAccuracy; 16] must fit into `UpperOf`. + let maximum_chain_accuracy: Vec>> = (0..max_vote) .map(|_| { - >>::from( - >::one().deconstruct(), + >>::from( + >::one().deconstruct(), ) }) .collect(); - let _: UpperOf> = maximum_chain_accuracy + let _: UpperOf> = maximum_chain_accuracy .iter() .fold(Zero::zero(), |acc, x| acc.checked_add(x).unwrap()); // We only accept data provider who's maximum votes per voter matches our - // `T::CompactSolution`'s `LIMIT`. + // `T::Solution`'s `LIMIT`. // - // NOTE that this pallet does not really need to enforce this in runtime. The compact + // NOTE that this pallet does not really need to enforce this in runtime. The // solution cannot represent any voters more than `LIMIT` anyhow. assert_eq!( >::MAXIMUM_VOTES_PER_VOTER, - as CompactSolution>::LIMIT as u32, + as NposSolution>::LIMIT as u32, ); } } @@ -853,14 +853,14 @@ pub mod pallet { T::WeightInfo::submit_unsigned( witness.voters, witness.targets, - solution.compact.voter_count() as u32, - solution.compact.unique_targets().len() as u32 + raw_solution.solution.voter_count() as u32, + raw_solution.solution.unique_targets().len() as u32 ), DispatchClass::Operational, ))] pub fn submit_unsigned( origin: OriginFor, - solution: Box>>, + raw_solution: Box>>, witness: SolutionOrSnapshotSize, ) -> DispatchResultWithPostInfo { ensure_none(origin)?; @@ -868,7 +868,7 @@ pub mod pallet { deprive validator from their authoring reward."; // Check score being an improvement, phase, and desired targets. - Self::unsigned_pre_dispatch_checks(&solution).expect(error_message); + Self::unsigned_pre_dispatch_checks(&raw_solution).expect(error_message); // Ensure witness was correct. let SolutionOrSnapshotSize { voters, targets } = @@ -878,8 +878,8 @@ pub mod pallet { assert!(voters as u32 == witness.voters, "{}", error_message); assert!(targets as u32 == witness.targets, "{}", error_message); - let ready = - Self::feasibility_check(*solution, ElectionCompute::Unsigned).expect(error_message); + let ready = Self::feasibility_check(*raw_solution, ElectionCompute::Unsigned) + .expect(error_message); // Store the newly received solution. log!(info, "queued unsigned solution with score {:?}", ready.score); @@ -950,7 +950,7 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::submit(*num_signed_submissions))] pub fn submit( origin: OriginFor, - solution: Box>>, + raw_solution: Box>>, num_signed_submissions: u32, ) -> DispatchResult { let who = ensure_signed(origin)?; @@ -973,20 +973,20 @@ pub mod pallet { let size = Self::snapshot_metadata().ok_or(Error::::MissingSnapshotMetadata)?; ensure!( - Self::feasibility_weight_of(&solution, size) < T::SignedMaxWeight::get(), + Self::feasibility_weight_of(&raw_solution, size) < T::SignedMaxWeight::get(), Error::::SignedTooMuchWeight, ); // create the submission - let deposit = Self::deposit_for(&solution, size); + let deposit = Self::deposit_for(&raw_solution, size); let reward = { - let call = Call::submit(solution.clone(), num_signed_submissions); + let call = Call::submit(raw_solution.clone(), num_signed_submissions); let call_fee = T::EstimateCallFee::estimate_call_fee(&call, None.into()); T::SignedRewardBase::get().saturating_add(call_fee) }; let submission = - SignedSubmission { who: who.clone(), deposit, solution: *solution, reward }; + SignedSubmission { who: who.clone(), deposit, raw_solution: *raw_solution, reward }; // insert the submission if the queue has space or it's better than the weakest // eject the weakest if the queue was full @@ -1299,8 +1299,8 @@ impl Pallet { /// /// Returns `Ok(consumed_weight)` if operation is okay. pub fn create_snapshot() -> Result { - let target_limit = >::max_value().saturated_into::(); - let voter_limit = >::max_value().saturated_into::(); + let target_limit = >::max_value().saturated_into::(); + let voter_limit = >::max_value().saturated_into::(); let (targets, w1) = T::DataProvider::targets(Some(target_limit)).map_err(ElectionError::DataProvider)?; @@ -1353,16 +1353,16 @@ impl Pallet { /// Checks the feasibility of a solution. pub fn feasibility_check( - solution: RawSolution>, + raw_solution: RawSolution>, compute: ElectionCompute, ) -> Result, FeasibilityError> { - let RawSolution { compact, score, round } = solution; + let RawSolution { solution, score, round } = raw_solution; // First, check round. ensure!(Self::round() == round, FeasibilityError::InvalidRound); // Winners are not directly encoded in the solution. - let winners = compact.unique_targets(); + let winners = solution.unique_targets(); let desired_targets = Self::desired_targets().ok_or(FeasibilityError::SnapshotUnavailable)?; @@ -1373,7 +1373,7 @@ impl Pallet { ensure!(winners.len() as u32 == desired_targets, FeasibilityError::WrongWinnerCount); // Ensure that the solution's score can pass absolute min-score. - let submitted_score = solution.score.clone(); + let submitted_score = raw_solution.score.clone(); ensure!( Self::minimum_untrusted_score().map_or(true, |min_score| { sp_npos_elections::is_score_better(submitted_score, min_score, Perbill::zero()) @@ -1394,15 +1394,15 @@ impl Pallet { // First, make sure that all the winners are sane. // OPTIMIZATION: we could first build the assignments, and then extract the winners directly // from that, as that would eliminate a little bit of duplicate work. For now, we keep them - // separate: First extract winners separately from compact, and then assignments. This is + // separate: First extract winners separately from solution, and then assignments. This is // also better, because we can reject solutions that don't meet `desired_targets` early on. let winners = winners .into_iter() .map(|i| target_at(i).ok_or(FeasibilityError::InvalidWinner)) .collect::, FeasibilityError>>()?; - // Then convert compact -> assignment. This will fail if any of the indices are gibberish. - let assignments = compact + // Then convert solution -> assignment. This will fail if any of the indices are gibberish. + let assignments = solution .into_assignment(voter_at, target_at) .map_err::(Into::into)?; @@ -1413,7 +1413,7 @@ impl Pallet { // Check that assignment.who is actually a voter (defensive-only). // NOTE: while using the index map from `voter_index` is better than a blind linear // search, this *still* has room for optimization. Note that we had the index when - // we did `compact -> assignment` and we lost it. Ideal is to keep the index around. + // we did `solution -> assignment` and we lost it. Ideal is to keep the index around. // Defensive-only: must exist in the snapshot. let snapshot_index = @@ -1438,7 +1438,7 @@ impl Pallet { .map_err::(Into::into)?; // This might fail if one of the voter edges is pointing to a non-winner, which is not - // really possible anymore because all the winners come from the same `compact`. + // really possible anymore because all the winners come from the same `solution`. let supports = sp_npos_elections::to_supports(&winners, &staked_assignments) .map_err::(Into::into)?; @@ -1611,13 +1611,13 @@ mod feasibility_check { roll_to(::get() - ::get() - ::get()); assert!(MultiPhase::current_phase().is_signed()); - let solution = raw_solution(); + let raw = raw_solution(); - assert_eq!(solution.compact.unique_targets().len(), 4); + assert_eq!(raw.solution.unique_targets().len(), 4); assert_eq!(MultiPhase::desired_targets().unwrap(), 8); assert_noop!( - MultiPhase::feasibility_check(solution, COMPUTE), + MultiPhase::feasibility_check(raw, COMPUTE), FeasibilityError::WrongWinnerCount, ); }) @@ -1629,20 +1629,19 @@ mod feasibility_check { roll_to(::get() - ::get() - ::get()); assert!(MultiPhase::current_phase().is_signed()); - let mut solution = raw_solution(); + let mut raw = raw_solution(); assert_eq!(MultiPhase::snapshot().unwrap().targets.len(), 4); // ----------------------------------------------------^^ valid range is [0..3]. - // Swap all votes from 3 to 4. This will ensure that the number of unique winners - // will still be 4, but one of the indices will be gibberish. Requirement is to make - // sure 3 a winner, which we don't do here. - solution - .compact + // Swap all votes from 3 to 4. This will ensure that the number of unique winners will + // still be 4, but one of the indices will be gibberish. Requirement is to make sure 3 a + // winner, which we don't do here. + raw.solution .votes1 .iter_mut() .filter(|(_, t)| *t == TargetIndex::from(3u16)) .for_each(|(_, t)| *t += 1); - solution.compact.votes2.iter_mut().for_each(|(_, (t0, _), t1)| { + raw.solution.votes2.iter_mut().for_each(|(_, [(t0, _)], t1)| { if *t0 == TargetIndex::from(3u16) { *t0 += 1 }; @@ -1651,7 +1650,7 @@ mod feasibility_check { }; }); assert_noop!( - MultiPhase::feasibility_check(solution, COMPUTE), + MultiPhase::feasibility_check(raw, COMPUTE), FeasibilityError::InvalidWinner ); }) @@ -1659,7 +1658,7 @@ mod feasibility_check { #[test] fn voter_indices() { - // Should be caught in `compact.into_assignment`. + // Should be caught in `solution.into_assignment`. ExtBuilder::default().desired_targets(2).build_and_execute(|| { roll_to(::get() - ::get() - ::get()); assert!(MultiPhase::current_phase().is_signed()); @@ -1671,7 +1670,7 @@ mod feasibility_check { // Check that there is an index 7 in votes1, and flip to 8. assert!( solution - .compact + .solution .votes1 .iter_mut() .filter(|(v, _)| *v == VoterIndex::from(7u32)) @@ -1680,7 +1679,7 @@ mod feasibility_check { ); assert_noop!( MultiPhase::feasibility_check(solution, COMPUTE), - FeasibilityError::NposElection(sp_npos_elections::Error::CompactInvalidIndex), + FeasibilityError::NposElection(sp_npos_elections::Error::SolutionInvalidIndex), ); }) } @@ -1699,7 +1698,7 @@ mod feasibility_check { // vote. Then, change the vote to 2 (30). assert_eq!( solution - .compact + .solution .votes1 .iter_mut() .filter(|(v, t)| *v == 7 && *t == 3) diff --git a/substrate/frame/election-provider-multi-phase/src/mock.rs b/substrate/frame/election-provider-multi-phase/src/mock.rs index 94fdb45590..f760676abf 100644 --- a/substrate/frame/election-provider-multi-phase/src/mock.rs +++ b/substrate/frame/election-provider-multi-phase/src/mock.rs @@ -31,7 +31,7 @@ use sp_core::{ }; use sp_npos_elections::{ assignment_ratio_to_staked_normalized, seq_phragmen, to_supports, to_without_backing, - CompactSolution, ElectionResult, EvaluateSupport, + ElectionResult, EvaluateSupport, NposSolution, }; use sp_runtime::{ testing::Header, @@ -63,7 +63,7 @@ pub(crate) type TargetIndex = u16; sp_npos_elections::generate_solution_type!( #[compact] - pub struct TestCompact::(16) + pub struct TestNposSolution::(16) ); /// All events of this pallet. @@ -101,7 +101,7 @@ pub struct TrimHelpers { pub voter_index: Box< dyn Fn( &::AccountId, - ) -> Option>, + ) -> Option>, >, } @@ -113,11 +113,11 @@ pub fn trim_helpers() -> TrimHelpers { let stakes: std::collections::HashMap<_, _> = voters.iter().map(|(id, stake, _)| (*id, *stake)).collect(); - // Compute the size of a compact solution comprised of the selected arguments. + // Compute the size of a solution comprised of the selected arguments. // // This function completes in `O(edges)`; it's expensive, but linear. let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf]| { - CompactOf::::try_from(assignments).map(|compact| compact.encoded_size()) + SolutionOf::::try_from(assignments).map(|s| s.encoded_size()) }); let cache = helpers::generate_voter_cache::(&voters); let voter_index = helpers::voter_index_fn_owned::(cache); @@ -125,7 +125,7 @@ pub fn trim_helpers() -> TrimHelpers { let desired_targets = MultiPhase::desired_targets().unwrap(); - let ElectionResult { mut assignments, .. } = seq_phragmen::<_, CompactAccuracyOf>( + let ElectionResult { mut assignments, .. } = seq_phragmen::<_, SolutionAccuracyOf>( desired_targets as usize, targets.clone(), voters.clone(), @@ -153,11 +153,11 @@ pub fn trim_helpers() -> TrimHelpers { /// Spit out a verifiable raw solution. /// /// This is a good example of what an offchain miner would do. -pub fn raw_solution() -> RawSolution> { +pub fn raw_solution() -> RawSolution> { let RoundSnapshot { voters, targets } = MultiPhase::snapshot().unwrap(); let desired_targets = MultiPhase::desired_targets().unwrap(); - let ElectionResult { winners, assignments } = seq_phragmen::<_, CompactAccuracyOf>( + let ElectionResult { winners, assignments } = seq_phragmen::<_, SolutionAccuracyOf>( desired_targets as usize, targets.clone(), voters.clone(), @@ -177,11 +177,11 @@ pub fn raw_solution() -> RawSolution> { let staked = assignment_ratio_to_staked_normalized(assignments.clone(), &stake_of).unwrap(); to_supports(&winners, &staked).unwrap().evaluate() }; - let compact = - >::from_assignment(&assignments, &voter_index, &target_index).unwrap(); + let solution = + >::from_assignment(&assignments, &voter_index, &target_index).unwrap(); let round = MultiPhase::round(); - RawSolution { compact, score, round } + RawSolution { solution, score, round } } pub fn witness() -> SolutionOrSnapshotSize { @@ -378,7 +378,7 @@ impl crate::Config for Runtime { type OnChainAccuracy = Perbill; type Fallback = Fallback; type ForceOrigin = frame_system::EnsureRoot; - type CompactSolution = TestCompact; + type Solution = TestNposSolution; } impl frame_system::offchain::SendTransactionTypes for Runtime @@ -396,7 +396,7 @@ pub struct ExtBuilder {} pub struct StakingMock; impl ElectionDataProvider for StakingMock { - const MAXIMUM_VOTES_PER_VOTER: u32 = ::LIMIT as u32; + const MAXIMUM_VOTES_PER_VOTER: u32 = ::LIMIT as u32; fn targets(maybe_max_len: Option) -> data_provider::Result<(Vec, Weight)> { let targets = Targets::get(); diff --git a/substrate/frame/election-provider-multi-phase/src/signed.rs b/substrate/frame/election-provider-multi-phase/src/signed.rs index 6d491b9d71..39d2e37765 100644 --- a/substrate/frame/election-provider-multi-phase/src/signed.rs +++ b/substrate/frame/election-provider-multi-phase/src/signed.rs @@ -18,8 +18,8 @@ //! The signed phase implementation. use crate::{ - CompactOf, Config, ElectionCompute, Pallet, QueuedSolution, RawSolution, ReadySolution, - SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap, + Config, ElectionCompute, Pallet, QueuedSolution, RawSolution, ReadySolution, + SignedSubmissionIndices, SignedSubmissionNextIndex, SignedSubmissionsMap, SolutionOf, SolutionOrSnapshotSize, Weight, WeightInfo, }; use codec::{Decode, Encode, HasCompact}; @@ -29,7 +29,7 @@ use frame_support::{ DebugNoBound, }; use sp_arithmetic::traits::SaturatedConversion; -use sp_npos_elections::{is_score_better, CompactSolution, ElectionScore}; +use sp_npos_elections::{is_score_better, ElectionScore, NposSolution}; use sp_runtime::{ traits::{Saturating, Zero}, RuntimeDebug, @@ -44,42 +44,40 @@ use sp_std::{ /// /// This is just a wrapper around [`RawSolution`] and some additional info. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, Default)] -pub struct SignedSubmission { +pub struct SignedSubmission { /// Who submitted this solution. pub who: AccountId, /// The deposit reserved for storing this solution. pub deposit: Balance, /// The raw solution itself. - pub solution: RawSolution, + pub raw_solution: RawSolution, /// The reward that should potentially be paid for this solution, if accepted. pub reward: Balance, } -impl Ord - for SignedSubmission +impl Ord for SignedSubmission where AccountId: Ord, Balance: Ord + HasCompact, - CompactSolution: Ord, - RawSolution: Ord, + Solution: Ord, + RawSolution: Ord, { fn cmp(&self, other: &Self) -> Ordering { - self.solution + self.raw_solution .score - .cmp(&other.solution.score) - .then_with(|| self.solution.cmp(&other.solution)) + .cmp(&other.raw_solution.score) + .then_with(|| self.raw_solution.cmp(&other.raw_solution)) .then_with(|| self.deposit.cmp(&other.deposit)) .then_with(|| self.who.cmp(&other.who)) } } -impl PartialOrd - for SignedSubmission +impl PartialOrd for SignedSubmission where AccountId: Ord, Balance: Ord + HasCompact, - CompactSolution: Ord, - RawSolution: Ord, + Solution: Ord, + RawSolution: Ord, { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -95,7 +93,7 @@ pub type NegativeImbalanceOf = <::Currency as Currency< ::AccountId, >>::NegativeImbalance; pub type SignedSubmissionOf = - SignedSubmission<::AccountId, BalanceOf, CompactOf>; + SignedSubmission<::AccountId, BalanceOf, SolutionOf>; pub type SubmissionIndicesOf = BoundedBTreeMap::SignedMaxSubmissions>; @@ -270,13 +268,13 @@ impl SignedSubmissions { // 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) { + let weakest = match self.indices.try_insert(submission.raw_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) + .try_insert(submission.raw_solution.score, prev_idx) .expect("didn't change the map size; qed"); return InsertResult::NotInserted }, @@ -354,8 +352,8 @@ impl Pallet { Self::snapshot_metadata().unwrap_or_default(); while let Some(best) = all_submissions.pop_last() { - let SignedSubmission { solution, who, deposit, reward } = best; - let active_voters = solution.compact.voter_count() as u32; + let SignedSubmission { raw_solution, who, deposit, reward } = best; + let active_voters = raw_solution.solution.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(); @@ -363,7 +361,7 @@ impl Pallet { }; // the feasibility check itself has some weight weight = weight.saturating_add(feasibility_weight); - match Self::feasibility_check(solution, ElectionCompute::Signed) { + match Self::feasibility_check(raw_solution, ElectionCompute::Signed) { Ok(ready_solution) => { Self::finalize_signed_phase_accept_solution( ready_solution, @@ -447,14 +445,14 @@ impl Pallet { /// The feasibility weight of the given raw solution. pub fn feasibility_weight_of( - solution: &RawSolution>, + raw_solution: &RawSolution>, size: SolutionOrSnapshotSize, ) -> Weight { T::WeightInfo::feasibility_check( size.voters, size.targets, - solution.compact.voter_count() as u32, - solution.compact.unique_targets().len() as u32, + raw_solution.solution.voter_count() as u32, + raw_solution.solution.unique_targets().len() as u32, ) } @@ -466,12 +464,12 @@ impl Pallet { /// 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>, + raw_solution: &RawSolution>, size: SolutionOrSnapshotSize, ) -> BalanceOf { - let encoded_len: u32 = solution.encoded_size().saturated_into(); + let encoded_len: u32 = raw_solution.encoded_size().saturated_into(); let encoded_len: BalanceOf = encoded_len.into(); - let feasibility_weight = Self::feasibility_weight_of(solution, size); + let feasibility_weight = Self::feasibility_weight_of(raw_solution, size); let len_deposit = T::SignedDepositByte::get().saturating_mul(encoded_len); let weight_deposit = @@ -497,7 +495,7 @@ mod tests { fn submit_with_witness( origin: Origin, - solution: RawSolution>, + solution: RawSolution>, ) -> DispatchResult { MultiPhase::submit( origin, @@ -663,7 +661,7 @@ mod tests { assert_eq!( MultiPhase::signed_submissions() .iter() - .map(|s| s.solution.score[0]) + .map(|s| s.raw_solution.score[0]) .collect::>(), vec![5, 6, 7, 8, 9] ); @@ -676,7 +674,7 @@ mod tests { assert_eq!( MultiPhase::signed_submissions() .iter() - .map(|s| s.solution.score[0]) + .map(|s| s.raw_solution.score[0]) .collect::>(), vec![6, 7, 8, 9, 20] ); @@ -701,7 +699,7 @@ mod tests { assert_eq!( MultiPhase::signed_submissions() .iter() - .map(|s| s.solution.score[0]) + .map(|s| s.raw_solution.score[0]) .collect::>(), vec![4, 6, 7, 8, 9], ); @@ -714,7 +712,7 @@ mod tests { assert_eq!( MultiPhase::signed_submissions() .iter() - .map(|s| s.solution.score[0]) + .map(|s| s.raw_solution.score[0]) .collect::>(), vec![5, 6, 7, 8, 9], ); @@ -759,7 +757,7 @@ mod tests { assert_eq!( MultiPhase::signed_submissions() .iter() - .map(|s| s.solution.score[0]) + .map(|s| s.raw_solution.score[0]) .collect::>(), vec![5, 6, 7] ); @@ -828,33 +826,33 @@ mod tests { roll_to(15); assert!(MultiPhase::current_phase().is_signed()); - let (solution, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); let solution_weight = ::WeightInfo::feasibility_check( witness.voters, witness.targets, - solution.compact.voter_count() as u32, - solution.compact.unique_targets().len() as u32, + raw.solution.voter_count() as u32, + raw.solution.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!(raw.solution.voter_count(), 5); assert_eq!(::SignedMaxWeight::get(), 40); - assert_ok!(submit_with_witness(Origin::signed(99), solution.clone())); + assert_ok!(submit_with_witness(Origin::signed(99), raw.clone())); ::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), + submit_with_witness(Origin::signed(99), raw), Error::::SignedTooMuchWeight, ); }) } #[test] - fn insufficient_deposit_doesnt_store_submission() { + fn insufficient_deposit_does_not_store_submission() { ExtBuilder::default().build_and_execute(|| { roll_to(15); assert!(MultiPhase::current_phase().is_signed()); diff --git a/substrate/frame/election-provider-multi-phase/src/unsigned.rs b/substrate/frame/election-provider-multi-phase/src/unsigned.rs index 41f8ced0ce..abb4f2c47d 100644 --- a/substrate/frame/election-provider-multi-phase/src/unsigned.rs +++ b/substrate/frame/election-provider-multi-phase/src/unsigned.rs @@ -18,8 +18,9 @@ //! The unsigned phase, and its miner. use crate::{ - helpers, Call, CompactAccuracyOf, CompactOf, Config, ElectionCompute, Error, FeasibilityError, - Pallet, RawSolution, ReadySolution, RoundSnapshot, SolutionOrSnapshotSize, Weight, WeightInfo, + helpers, Call, Config, ElectionCompute, Error, FeasibilityError, Pallet, RawSolution, + ReadySolution, RoundSnapshot, SolutionAccuracyOf, SolutionOf, SolutionOrSnapshotSize, Weight, + WeightInfo, }; use codec::{Decode, Encode}; use frame_support::{dispatch::DispatchResult, ensure, traits::Get}; @@ -27,7 +28,7 @@ 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, CompactSolution, ElectionResult, + seq_phragmen, ElectionResult, NposSolution, }; use sp_runtime::{ offchain::storage::{MutateStorageError, StorageValueRef}, @@ -54,11 +55,11 @@ pub type Voter = ( /// The relative distribution of a voter's stake among the winning targets. pub type Assignment = - sp_npos_elections::Assignment<::AccountId, CompactAccuracyOf>; + sp_npos_elections::Assignment<::AccountId, SolutionAccuracyOf>; /// The [`IndexAssignment`][sp_npos_elections::IndexAssignment] type specialized for a particular /// runtime `T`. -pub type IndexAssignmentOf = sp_npos_elections::IndexAssignmentOf>; +pub type IndexAssignmentOf = sp_npos_elections::IndexAssignmentOf>; #[derive(Debug, Eq, PartialEq)] pub enum MinerError { @@ -231,7 +232,7 @@ impl Pallet { // // Performance: note that it internally clones the provided solution. pub fn basic_checks( - raw_solution: &RawSolution>, + raw_solution: &RawSolution>, solution_type: &str, ) -> Result<(), MinerError> { Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| { @@ -257,7 +258,7 @@ impl Pallet { /// [`Pallet::mine_check_save_submit`]. pub fn mine_and_check( iters: usize, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { let (raw_solution, witness) = Self::mine_solution(iters)?; Self::basic_checks(&raw_solution, "mined")?; Ok((raw_solution, witness)) @@ -266,12 +267,12 @@ impl Pallet { /// Mine a new npos solution. pub fn mine_solution( iters: usize, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { let RoundSnapshot { voters, targets } = Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?; let desired_targets = Self::desired_targets().ok_or(MinerError::SnapshotUnAvailable)?; - seq_phragmen::<_, CompactAccuracyOf>( + seq_phragmen::<_, SolutionAccuracyOf>( desired_targets as usize, targets, voters, @@ -286,8 +287,8 @@ impl Pallet { /// /// Will always reduce the solution as well. pub fn prepare_election_result( - election_result: ElectionResult>, - ) -> Result<(RawSolution>, SolutionOrSnapshotSize), MinerError> { + 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. @@ -304,11 +305,11 @@ impl Pallet { let target_at = helpers::target_at_fn::(&targets); let stake_of = helpers::stake_of_fn::(&voters, &cache); - // Compute the size of a compact solution comprised of the selected arguments. + // Compute the size of a solution comprised of the selected arguments. // // This function completes in `O(edges)`; it's expensive, but linear. let encoded_size_of = |assignments: &[IndexAssignmentOf]| { - CompactOf::::try_from(assignments).map(|compact| compact.encoded_size()) + SolutionOf::::try_from(assignments).map(|s| s.encoded_size()) }; let ElectionResult { assignments, winners } = election_result; @@ -345,7 +346,7 @@ impl Pallet { }; // convert to `IndexAssignment`. This improves the runtime complexity of repeatedly - // converting to `Compact`. + // converting to `Solution`. let mut index_assignments = sorted_assignments .into_iter() .map(|assignment| IndexAssignmentOf::::new(&assignment, &voter_index, &target_index)) @@ -366,15 +367,15 @@ impl Pallet { &encoded_size_of, )?; - // now make compact. - let compact = CompactOf::::try_from(&index_assignments)?; + // now make solution. + let solution = SolutionOf::::try_from(&index_assignments)?; // re-calc score. let winners = sp_npos_elections::to_without_backing(winners); - let score = compact.clone().score(&winners, stake_of, voter_at, target_at)?; + let score = solution.clone().score(&winners, stake_of, voter_at, target_at)?; let round = Self::round(); - Ok((RawSolution { compact, score, round }, size)) + Ok((RawSolution { solution, score, round }, size)) } /// Get a random number of iterations to run the balancing in the OCW. @@ -502,7 +503,7 @@ impl Pallet { Ok(()) } - /// Find the maximum `len` that a compact can have in order to fit into the block weight. + /// Find the maximum `len` that a solution can have in order to fit into the block weight. /// /// This only returns a value between zero and `size.nominators`. pub fn maximum_voter_for_weight( @@ -623,24 +624,26 @@ impl Pallet { /// /// NOTE: Ideally, these tests should move more and more outside of this and more to the miner's /// code, so that we do less and less storage reads here. - pub fn unsigned_pre_dispatch_checks(solution: &RawSolution>) -> DispatchResult { + pub fn unsigned_pre_dispatch_checks( + raw_solution: &RawSolution>, + ) -> DispatchResult { // ensure solution is timely. Don't panic yet. This is a cheap check. ensure!(Self::current_phase().is_unsigned_open(), Error::::PreDispatchEarlySubmission); // ensure round is current - ensure!(Self::round() == solution.round, Error::::OcwCallWrongEra); + ensure!(Self::round() == raw_solution.round, Error::::OcwCallWrongEra); // ensure correct number of winners. ensure!( Self::desired_targets().unwrap_or_default() == - solution.compact.unique_targets().len() as u32, + raw_solution.solution.unique_targets().len() as u32, Error::::PreDispatchWrongWinnerCount, ); // ensure score is being improved. Panic henceforth. ensure!( Self::queued_solution().map_or(true, |q: ReadySolution<_>| is_score_better::( - solution.score, + raw_solution.score, q.score, T::SolutionImprovementThreshold::get() )), @@ -753,7 +756,7 @@ mod tests { mock::{ roll_to, roll_to_with_ocw, trim_helpers, witness, BlockNumber, Call as OuterCall, ExtBuilder, Extrinsic, MinerMaxWeight, MultiPhase, Origin, Runtime, System, - TestCompact, TrimHelpers, UnsignedPhase, + TestNposSolution, TrimHelpers, UnsignedPhase, }, CurrentPhase, InvalidTransaction, Phase, QueuedSolution, TransactionSource, TransactionValidityError, @@ -772,7 +775,8 @@ mod tests { #[test] fn validate_unsigned_retracts_wrong_phase() { ExtBuilder::default().desired_targets(0).build_and_execute(|| { - let solution = RawSolution:: { score: [5, 0, 0], ..Default::default() }; + let solution = + RawSolution:: { score: [5, 0, 0], ..Default::default() }; let call = Call::submit_unsigned(Box::new(solution.clone()), witness()); // initial @@ -841,7 +845,8 @@ mod tests { roll_to(25); assert!(MultiPhase::current_phase().is_unsigned()); - let solution = RawSolution:: { score: [5, 0, 0], ..Default::default() }; + let solution = + RawSolution:: { score: [5, 0, 0], ..Default::default() }; let call = Call::submit_unsigned(Box::new(solution.clone()), witness()); // initial @@ -878,9 +883,9 @@ mod tests { roll_to(25); assert!(MultiPhase::current_phase().is_unsigned()); - let solution = RawSolution:: { score: [5, 0, 0], ..Default::default() }; - let call = Call::submit_unsigned(Box::new(solution.clone()), witness()); - assert_eq!(solution.compact.unique_targets().len(), 0); + let raw = RawSolution:: { score: [5, 0, 0], ..Default::default() }; + let call = Call::submit_unsigned(Box::new(raw.clone()), witness()); + assert_eq!(raw.solution.unique_targets().len(), 0); // won't work anymore. assert!(matches!( @@ -904,7 +909,7 @@ mod tests { assert!(MultiPhase::current_phase().is_unsigned()); let solution = - RawSolution:: { score: [5, 0, 0], ..Default::default() }; + RawSolution:: { score: [5, 0, 0], ..Default::default() }; let call = Call::submit_unsigned(Box::new(solution.clone()), witness()); assert_eq!( @@ -930,7 +935,8 @@ mod tests { assert!(MultiPhase::current_phase().is_unsigned()); // This is in itself an invalid BS solution. - let solution = RawSolution:: { score: [5, 0, 0], ..Default::default() }; + let solution = + RawSolution:: { score: [5, 0, 0], ..Default::default() }; let call = Call::submit_unsigned(Box::new(solution.clone()), witness()); let outer_call: OuterCall = call.into(); let _ = outer_call.dispatch(Origin::none()); @@ -946,7 +952,8 @@ mod tests { assert!(MultiPhase::current_phase().is_unsigned()); // This solution is unfeasible as well, but we won't even get there. - let solution = RawSolution:: { score: [5, 0, 0], ..Default::default() }; + let solution = + RawSolution:: { score: [5, 0, 0], ..Default::default() }; let mut correct_witness = witness(); correct_witness.voters += 1; @@ -986,30 +993,30 @@ mod tests { roll_to(25); assert!(MultiPhase::current_phase().is_unsigned()); - let (solution, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); let solution_weight = ::WeightInfo::submit_unsigned( witness.voters, witness.targets, - solution.compact.voter_count() as u32, - solution.compact.unique_targets().len() as u32, + raw.solution.voter_count() as u32, + raw.solution.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!(raw.solution.voter_count(), 5); // now reduce the max weight ::set(25); - let (solution, witness) = MultiPhase::mine_solution(2).unwrap(); + let (raw, witness) = MultiPhase::mine_solution(2).unwrap(); let solution_weight = ::WeightInfo::submit_unsigned( witness.voters, witness.targets, - solution.compact.voter_count() as u32, - solution.compact.unique_targets().len() as u32, + raw.solution.voter_count() as u32, + raw.solution.unique_targets().len() as u32, ); // default solution will have 5 edges (5 * 5 + 10) assert_eq!(solution_weight, 25); - assert_eq!(solution.compact.voter_count(), 3); + assert_eq!(raw.solution.voter_count(), 3); }) } @@ -1068,7 +1075,7 @@ mod tests { Assignment { who: 10, distribution: vec![(10, PerU16::one())] }, Assignment { who: 7, - // note: this percent doesn't even matter, in compact it is 100%. + // note: this percent doesn't even matter, in solution it is 100%. distribution: vec![(10, PerU16::one())], }, ], @@ -1090,7 +1097,7 @@ mod tests { Assignment { who: 7, distribution: vec![(10, PerU16::one())] }, Assignment { who: 8, - // note: this percent doesn't even matter, in compact it is 100%. + // note: this percent doesn't even matter, in solution it is 100%. distribution: vec![(10, PerU16::one())], }, ], @@ -1400,17 +1407,17 @@ mod tests { // given let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers(); - let compact = CompactOf::::try_from(assignments.as_slice()).unwrap(); - let encoded_len = compact.encoded_size() as u32; - let compact_clone = compact.clone(); + let solution = SolutionOf::::try_from(assignments.as_slice()).unwrap(); + let encoded_len = solution.encoded_size() as u32; + let solution_clone = solution.clone(); // when MultiPhase::trim_assignments_length(encoded_len, &mut assignments, encoded_size_of) .unwrap(); // then - let compact = CompactOf::::try_from(assignments.as_slice()).unwrap(); - assert_eq!(compact, compact_clone); + let solution = SolutionOf::::try_from(assignments.as_slice()).unwrap(); + assert_eq!(solution, solution_clone); }); } @@ -1421,9 +1428,9 @@ mod tests { // given let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers(); - let compact = CompactOf::::try_from(assignments.as_slice()).unwrap(); - let encoded_len = compact.encoded_size(); - let compact_clone = compact.clone(); + let solution = SolutionOf::::try_from(assignments.as_slice()).unwrap(); + let encoded_len = solution.encoded_size(); + let solution_clone = solution.clone(); // when MultiPhase::trim_assignments_length( @@ -1434,9 +1441,9 @@ mod tests { .unwrap(); // then - let compact = CompactOf::::try_from(assignments.as_slice()).unwrap(); - assert_ne!(compact, compact_clone); - assert!(compact.encoded_size() < encoded_len); + let solution = SolutionOf::::try_from(assignments.as_slice()).unwrap(); + assert_ne!(solution, solution_clone); + assert!(solution.encoded_size() < encoded_len); }); } @@ -1448,8 +1455,8 @@ mod tests { // given let TrimHelpers { voters, mut assignments, encoded_size_of, voter_index } = trim_helpers(); - let compact = CompactOf::::try_from(assignments.as_slice()).unwrap(); - let encoded_len = compact.encoded_size() as u32; + let solution = SolutionOf::::try_from(assignments.as_slice()).unwrap(); + let encoded_len = solution.encoded_size() as u32; let count = assignments.len(); let min_stake_voter = voters .iter() @@ -1476,15 +1483,15 @@ mod tests { // we shan't panic if assignments are initially empty. ExtBuilder::default().build_and_execute(|| { let encoded_size_of = Box::new(|assignments: &[IndexAssignmentOf]| { - CompactOf::::try_from(assignments).map(|compact| compact.encoded_size()) + SolutionOf::::try_from(assignments).map(|solution| solution.encoded_size()) }); let mut assignments = vec![]; // since we have 16 fields, we need to store the length fields of 16 vecs, thus 16 bytes // minimum. - let min_compact_size = encoded_size_of(&assignments).unwrap(); - assert_eq!(min_compact_size, CompactOf::::LIMIT); + let min_solution_size = encoded_size_of(&assignments).unwrap(); + assert_eq!(min_solution_size, SolutionOf::::LIMIT); // all of this should not panic. MultiPhase::trim_assignments_length(0, &mut assignments, encoded_size_of.clone()) @@ -1492,7 +1499,7 @@ mod tests { MultiPhase::trim_assignments_length(1, &mut assignments, encoded_size_of.clone()) .unwrap(); MultiPhase::trim_assignments_length( - min_compact_size as u32, + min_solution_size as u32, &mut assignments, encoded_size_of, ) @@ -1506,10 +1513,10 @@ mod tests { let TrimHelpers { mut assignments, encoded_size_of, .. } = trim_helpers(); assert!(assignments.len() > 0); - // trim to min compact size. - let min_compact_size = CompactOf::::LIMIT as u32; + // trim to min solution size. + let min_solution_size = SolutionOf::::LIMIT as u32; MultiPhase::trim_assignments_length( - min_compact_size, + min_solution_size, &mut assignments, encoded_size_of, ) @@ -1529,14 +1536,14 @@ mod tests { // how long would the default solution be? let solution = MultiPhase::mine_solution(0).unwrap(); let max_length = ::MinerMaxLength::get(); - let solution_size = solution.0.compact.encoded_size(); + 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 max_length = ::MinerMaxLength::get(); - let solution_size = solution.0.compact.encoded_size(); + let solution_size = solution.0.solution.encoded_size(); assert!(solution_size <= max_length as usize); }); } diff --git a/substrate/primitives/npos-elections/Cargo.toml b/substrate/primitives/npos-elections/Cargo.toml index 902b3040ba..0d1834a94a 100644 --- a/substrate/primitives/npos-elections/Cargo.toml +++ b/substrate/primitives/npos-elections/Cargo.toml @@ -16,7 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } serde = { version = "1.0.126", optional = true, features = ["derive"] } sp-std = { version = "4.0.0-dev", default-features = false, path = "../std" } -sp-npos-elections-compact = { version = "4.0.0-dev", path = "./compact" } +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" } @@ -28,7 +28,6 @@ sp-runtime = { version = "4.0.0-dev", path = "../runtime" } [features] default = ["std"] bench = [] -mocks = [] std = [ "codec/std", "serde", diff --git a/substrate/primitives/npos-elections/compact/src/assignment.rs b/substrate/primitives/npos-elections/compact/src/assignment.rs deleted file mode 100644 index bd5b1bf0c1..0000000000 --- a/substrate/primitives/npos-elections/compact/src/assignment.rs +++ /dev/null @@ -1,161 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2020-2021 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. - -//! Code generation for the ratio assignment type' compact representation. - -use crate::field_name_for; -use proc_macro2::TokenStream as TokenStream2; -use quote::quote; - -pub(crate) fn from_impl(count: usize) -> TokenStream2 { - let from_impl_single = { - let name = field_name_for(1); - quote!(1 => compact.#name.push( - ( - index_of_voter(&who).or_invalid_index()?, - index_of_target(&distribution[0].0).or_invalid_index()?, - ) - ),) - }; - - let from_impl_double = { - let name = field_name_for(2); - quote!(2 => compact.#name.push( - ( - index_of_voter(&who).or_invalid_index()?, - ( - index_of_target(&distribution[0].0).or_invalid_index()?, - distribution[0].1, - ), - index_of_target(&distribution[1].0).or_invalid_index()?, - ) - ),) - }; - - let from_impl_rest = (3..=count) - .map(|c| { - let inner = (0..c - 1) - .map( - |i| quote!((index_of_target(&distribution[#i].0).or_invalid_index()?, distribution[#i].1),), - ) - .collect::(); - - let field_name = field_name_for(c); - let last_index = c - 1; - let last = quote!(index_of_target(&distribution[#last_index].0).or_invalid_index()?); - - quote!( - #c => compact.#field_name.push( - ( - index_of_voter(&who).or_invalid_index()?, - [#inner], - #last, - ) - ), - ) - }) - .collect::(); - - quote!( - #from_impl_single - #from_impl_double - #from_impl_rest - ) -} - -pub(crate) fn into_impl(count: usize, per_thing: syn::Type) -> TokenStream2 { - let into_impl_single = { - let name = field_name_for(1); - quote!( - for (voter_index, target_index) in self.#name { - assignments.push(_npos::Assignment { - who: voter_at(voter_index).or_invalid_index()?, - distribution: vec![ - (target_at(target_index).or_invalid_index()?, #per_thing::one()) - ], - }) - } - ) - }; - - let into_impl_double = { - let name = field_name_for(2); - quote!( - for (voter_index, (t1_idx, p1), t2_idx) in self.#name { - if p1 >= #per_thing::one() { - return Err(_npos::Error::CompactStakeOverflow); - } - - // defensive only. Since Percent doesn't have `Sub`. - let p2 = _npos::sp_arithmetic::traits::Saturating::saturating_sub( - #per_thing::one(), - p1, - ); - - assignments.push( _npos::Assignment { - who: voter_at(voter_index).or_invalid_index()?, - distribution: vec![ - (target_at(t1_idx).or_invalid_index()?, p1), - (target_at(t2_idx).or_invalid_index()?, p2), - ] - }); - } - ) - }; - - let into_impl_rest = (3..=count) - .map(|c| { - let name = field_name_for(c); - quote!( - for (voter_index, inners, t_last_idx) in self.#name { - let mut sum = #per_thing::zero(); - let mut inners_parsed = inners - .iter() - .map(|(ref t_idx, p)| { - sum = _npos::sp_arithmetic::traits::Saturating::saturating_add(sum, *p); - let target = target_at(*t_idx).or_invalid_index()?; - Ok((target, *p)) - }) - .collect::, _npos::Error>>()?; - - if sum >= #per_thing::one() { - return Err(_npos::Error::CompactStakeOverflow); - } - - // defensive only. Since Percent doesn't have `Sub`. - let p_last = _npos::sp_arithmetic::traits::Saturating::saturating_sub( - #per_thing::one(), - sum, - ); - - inners_parsed.push((target_at(t_last_idx).or_invalid_index()?, p_last)); - - assignments.push(_npos::Assignment { - who: voter_at(voter_index).or_invalid_index()?, - distribution: inners_parsed, - }); - } - ) - }) - .collect::(); - - quote!( - #into_impl_single - #into_impl_double - #into_impl_rest - ) -} diff --git a/substrate/primitives/npos-elections/compact/src/lib.rs b/substrate/primitives/npos-elections/compact/src/lib.rs deleted file mode 100644 index 5897e607cf..0000000000 --- a/substrate/primitives/npos-elections/compact/src/lib.rs +++ /dev/null @@ -1,499 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2020-2021 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. - -//! Proc macro for a npos compact assignment. - -use proc_macro::TokenStream; -use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; -use proc_macro_crate::{crate_name, FoundCrate}; -use quote::quote; -use syn::parse::{Parse, ParseStream, Result}; - -mod assignment; -mod codec; -mod index_assignment; - -// prefix used for struct fields in compact. -const PREFIX: &'static str = "votes"; - -pub(crate) fn syn_err(message: &'static str) -> syn::Error { - syn::Error::new(Span::call_site(), message) -} - -/// Generates a struct to store the election result in a small way. This can encode a structure -/// which is the equivalent of a `sp_npos_elections::Assignment<_>`. -/// -/// The following data types can be configured by the macro. -/// -/// - The identifier of the voter. This can be any type that supports `parity-scale-codec`'s compact -/// encoding. -/// - The identifier of the target. This can be any type that supports `parity-scale-codec`'s -/// compact encoding. -/// - The accuracy of the ratios. This must be one of the `PerThing` types defined in -/// `sp-arithmetic`. -/// -/// Moreover, the maximum number of edges per voter (distribution per assignment) also need to be -/// specified. Attempting to convert from/to an assignment with more distributions will fail. -/// -/// -/// For example, the following generates a public struct with name `TestSolution` with `u16` voter -/// type, `u8` target type and `Perbill` accuracy with maximum of 8 edges per voter. -/// -/// ``` -/// # use sp_npos_elections_compact::generate_solution_type; -/// # use sp_arithmetic::per_things::Perbill; -/// generate_solution_type!(pub struct TestSolution::< -/// VoterIndex = u16, -/// TargetIndex = u8, -/// Accuracy = Perbill, -/// >(8)); -/// ``` -/// -/// The given struct provides function to convert from/to Assignment: -/// -/// - `fn from_assignment<..>(..)` -/// - `fn into_assignment<..>(..)` -/// -/// The generated struct is by default deriving both `Encode` and `Decode`. This is okay but could -/// lead to many 0s in the solution. If prefixed with `#[compact]`, then a custom compact encoding -/// for numbers will be used, similar to how `parity-scale-codec`'s `Compact` works. -/// -/// ``` -/// # use sp_npos_elections_compact::generate_solution_type; -/// # use sp_arithmetic::per_things::Perbill; -/// generate_solution_type!( -/// #[compact] -/// pub struct TestSolutionCompact::(8) -/// ); -/// ``` -#[proc_macro] -pub fn generate_solution_type(item: TokenStream) -> TokenStream { - let SolutionDef { vis, ident, count, voter_type, target_type, weight_type, compact_encoding } = - syn::parse_macro_input!(item as SolutionDef); - - let imports = imports().unwrap_or_else(|e| e.to_compile_error()); - - let solution_struct = struct_def( - vis, - ident, - count, - voter_type.clone(), - target_type.clone(), - weight_type.clone(), - compact_encoding, - ) - .unwrap_or_else(|e| e.to_compile_error()); - - quote!( - #imports - #solution_struct - ) - .into() -} - -fn struct_def( - vis: syn::Visibility, - ident: syn::Ident, - count: usize, - voter_type: syn::Type, - target_type: syn::Type, - weight_type: syn::Type, - compact_encoding: bool, -) -> Result { - if count <= 2 { - Err(syn_err("cannot build compact solution struct with capacity less than 3."))? - } - - let singles = { - let name = field_name_for(1); - // NOTE: we use the visibility of the struct for the fields as well.. could be made better. - quote!( - #vis #name: _npos::sp_std::prelude::Vec<(#voter_type, #target_type)>, - ) - }; - - let doubles = { - let name = field_name_for(2); - quote!( - #vis #name: _npos::sp_std::prelude::Vec<(#voter_type, (#target_type, #weight_type), #target_type)>, - ) - }; - - let rest = (3..=count) - .map(|c| { - let field_name = field_name_for(c); - let array_len = c - 1; - quote!( - #vis #field_name: _npos::sp_std::prelude::Vec<( - #voter_type, - [(#target_type, #weight_type); #array_len], - #target_type - )>, - ) - }) - .collect::(); - - let len_impl = len_impl(count); - let edge_count_impl = edge_count_impl(count); - let unique_targets_impl = unique_targets_impl(count); - let remove_voter_impl = remove_voter_impl(count); - - let derives_and_maybe_compact_encoding = if compact_encoding { - // custom compact encoding. - let compact_impl = codec::codec_impl( - ident.clone(), - voter_type.clone(), - target_type.clone(), - weight_type.clone(), - count, - ); - quote! { - #compact_impl - #[derive(Default, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)] - } - } else { - // automatically derived. - quote!(#[derive(Default, PartialEq, Eq, Clone, Debug, _npos::codec::Encode, _npos::codec::Decode)]) - }; - - let from_impl = assignment::from_impl(count); - let into_impl = assignment::into_impl(count, weight_type.clone()); - let from_index_impl = index_assignment::from_impl(count); - - Ok(quote! ( - /// A struct to encode a election assignment in a compact way. - #derives_and_maybe_compact_encoding - #vis struct #ident { #singles #doubles #rest } - - use _npos::__OrInvalidIndex; - impl _npos::CompactSolution for #ident { - const LIMIT: usize = #count; - type Voter = #voter_type; - type Target = #target_type; - type Accuracy = #weight_type; - - fn voter_count(&self) -> usize { - let mut all_len = 0usize; - #len_impl - all_len - } - - fn edge_count(&self) -> usize { - let mut all_edges = 0usize; - #edge_count_impl - all_edges - } - - fn unique_targets(&self) -> _npos::sp_std::prelude::Vec { - // NOTE: this implementation returns the targets sorted, but we don't use it yet per - // se, nor is the API enforcing it. - use _npos::sp_std::collections::btree_set::BTreeSet; - - let mut all_targets: BTreeSet = BTreeSet::new(); - let mut maybe_insert_target = |t: Self::Target| { - all_targets.insert(t); - }; - - #unique_targets_impl - - all_targets.into_iter().collect() - } - - fn remove_voter(&mut self, to_remove: Self::Voter) -> bool { - #remove_voter_impl - return false - } - - fn from_assignment( - assignments: &[_npos::Assignment], - index_of_voter: FV, - index_of_target: FT, - ) -> Result - where - A: _npos::IdentifierT, - for<'r> FV: Fn(&'r A) -> Option, - for<'r> FT: Fn(&'r A) -> Option, - { - let mut compact: #ident = Default::default(); - - for _npos::Assignment { who, distribution } in assignments { - match distribution.len() { - 0 => continue, - #from_impl - _ => { - return Err(_npos::Error::CompactTargetOverflow); - } - } - }; - Ok(compact) - } - - fn into_assignment( - self, - voter_at: impl Fn(Self::Voter) -> Option, - target_at: impl Fn(Self::Target) -> Option, - ) -> Result<_npos::sp_std::prelude::Vec<_npos::Assignment>, _npos::Error> { - let mut assignments: _npos::sp_std::prelude::Vec<_npos::Assignment> = Default::default(); - #into_impl - Ok(assignments) - } - } - type __IndexAssignment = _npos::IndexAssignment< - <#ident as _npos::CompactSolution>::Voter, - <#ident as _npos::CompactSolution>::Target, - <#ident as _npos::CompactSolution>::Accuracy, - >; - impl<'a> _npos::sp_std::convert::TryFrom<&'a [__IndexAssignment]> for #ident { - type Error = _npos::Error; - fn try_from(index_assignments: &'a [__IndexAssignment]) -> Result { - let mut compact = #ident::default(); - - for _npos::IndexAssignment { who, distribution } in index_assignments { - match distribution.len() { - 0 => {} - #from_index_impl - _ => { - return Err(_npos::Error::CompactTargetOverflow); - } - } - }; - - Ok(compact) - } - } - )) -} - -fn remove_voter_impl(count: usize) -> TokenStream2 { - let field_name = field_name_for(1); - let single = quote! { - if let Some(idx) = self.#field_name.iter().position(|(x, _)| *x == to_remove) { - self.#field_name.remove(idx); - return true - } - }; - - let field_name = field_name_for(2); - let double = quote! { - if let Some(idx) = self.#field_name.iter().position(|(x, _, _)| *x == to_remove) { - self.#field_name.remove(idx); - return true - } - }; - - let rest = (3..=count) - .map(|c| { - let field_name = field_name_for(c); - quote! { - if let Some(idx) = self.#field_name.iter().position(|(x, _, _)| *x == to_remove) { - self.#field_name.remove(idx); - return true - } - } - }) - .collect::(); - - quote! { - #single - #double - #rest - } -} - -fn len_impl(count: usize) -> TokenStream2 { - (1..=count) - .map(|c| { - let field_name = field_name_for(c); - quote!( - all_len = all_len.saturating_add(self.#field_name.len()); - ) - }) - .collect::() -} - -fn edge_count_impl(count: usize) -> TokenStream2 { - (1..=count) - .map(|c| { - let field_name = field_name_for(c); - quote!( - all_edges = all_edges.saturating_add( - self.#field_name.len().saturating_mul(#c as usize) - ); - ) - }) - .collect::() -} - -fn unique_targets_impl(count: usize) -> TokenStream2 { - let unique_targets_impl_single = { - let field_name = field_name_for(1); - quote! { - self.#field_name.iter().for_each(|(_, t)| { - maybe_insert_target(*t); - }); - } - }; - - let unique_targets_impl_double = { - let field_name = field_name_for(2); - quote! { - self.#field_name.iter().for_each(|(_, (t1, _), t2)| { - maybe_insert_target(*t1); - maybe_insert_target(*t2); - }); - } - }; - - let unique_targets_impl_rest = (3..=count) - .map(|c| { - let field_name = field_name_for(c); - quote! { - self.#field_name.iter().for_each(|(_, inners, t_last)| { - inners.iter().for_each(|(t, _)| { - maybe_insert_target(*t); - }); - maybe_insert_target(*t_last); - }); - } - }) - .collect::(); - - quote! { - #unique_targets_impl_single - #unique_targets_impl_double - #unique_targets_impl_rest - } -} - -fn imports() -> Result { - match crate_name("sp-npos-elections") { - Ok(FoundCrate::Itself) => Ok(quote! { use crate as _npos; }), - Ok(FoundCrate::Name(sp_npos_elections)) => { - let ident = syn::Ident::new(&sp_npos_elections, Span::call_site()); - Ok(quote!( extern crate #ident as _npos; )) - }, - Err(e) => Err(syn::Error::new(Span::call_site(), e)), - } -} - -struct SolutionDef { - vis: syn::Visibility, - ident: syn::Ident, - voter_type: syn::Type, - target_type: syn::Type, - weight_type: syn::Type, - count: usize, - compact_encoding: bool, -} - -fn check_compact_attr(input: ParseStream) -> Result { - let mut attrs = input.call(syn::Attribute::parse_outer).unwrap_or_default(); - if attrs.len() == 1 { - let attr = attrs.pop().expect("Vec with len 1 can be popped."); - if attr.path.segments.len() == 1 { - let segment = attr.path.segments.first().expect("Vec with len 1 can be popped."); - if segment.ident == Ident::new("compact", Span::call_site()) { - Ok(true) - } else { - Err(syn_err("generate_solution_type macro can only accept #[compact] attribute.")) - } - } else { - Err(syn_err("generate_solution_type macro can only accept #[compact] attribute.")) - } - } else { - Ok(false) - } -} - -/// `#[compact] pub struct CompactName::()` -impl Parse for SolutionDef { - fn parse(input: ParseStream) -> syn::Result { - // optional #[compact] - let compact_encoding = check_compact_attr(input)?; - - // struct - let vis: syn::Visibility = input.parse()?; - let _ = ::parse(input)?; - let ident: syn::Ident = input.parse()?; - - // :: - let _ = ::parse(input)?; - let generics: syn::AngleBracketedGenericArguments = input.parse()?; - - if generics.args.len() != 3 { - return Err(syn_err("Must provide 3 generic args.")) - } - - let expected_types = ["VoterIndex", "TargetIndex", "Accuracy"]; - - let mut types: Vec = generics - .args - .iter() - .zip(expected_types.iter()) - .map(|(t, expected)| match t { - syn::GenericArgument::Type(ty) => { - // this is now an error - Err(syn::Error::new_spanned( - ty, - format!("Expected binding: `{} = ...`", expected), - )) - }, - syn::GenericArgument::Binding(syn::Binding { ident, ty, .. }) => { - // check that we have the right keyword for this position in the argument list - if ident == expected { - Ok(ty.clone()) - } else { - Err(syn::Error::new_spanned(ident, format!("Expected `{}`", expected))) - } - }, - _ => Err(syn_err("Wrong type of generic provided. Must be a `type`.")), - }) - .collect::>()?; - - let weight_type = types.pop().expect("Vector of length 3 can be popped; qed"); - let target_type = types.pop().expect("Vector of length 2 can be popped; qed"); - let voter_type = types.pop().expect("Vector of length 1 can be popped; qed"); - - // () - let count_expr: syn::ExprParen = input.parse()?; - let expr = count_expr.expr; - let expr_lit = match *expr { - syn::Expr::Lit(count_lit) => count_lit.lit, - _ => return Err(syn_err("Count must be literal.")), - }; - let int_lit = match expr_lit { - syn::Lit::Int(int_lit) => int_lit, - _ => return Err(syn_err("Count must be int literal.")), - }; - let count = int_lit.base10_parse::()?; - - Ok(Self { vis, ident, voter_type, target_type, weight_type, count, compact_encoding }) - } -} - -fn field_name_for(n: usize) -> Ident { - Ident::new(&format!("{}{}", PREFIX, n), Span::call_site()) -} - -#[cfg(test)] -mod tests { - #[test] - fn ui_fail() { - let cases = trybuild::TestCases::new(); - cases.compile_fail("tests/ui/fail/*.rs"); - } -} diff --git a/substrate/primitives/npos-elections/compact/Cargo.toml b/substrate/primitives/npos-elections/solution-type/Cargo.toml similarity index 71% rename from substrate/primitives/npos-elections/compact/Cargo.toml rename to substrate/primitives/npos-elections/solution-type/Cargo.toml index d90bdf373b..4b54e46703 100644 --- a/substrate/primitives/npos-elections/compact/Cargo.toml +++ b/substrate/primitives/npos-elections/solution-type/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "sp-npos-elections-compact" +name = "sp-npos-elections-solution-type" version = "4.0.0-dev" authors = ["Parity Technologies "] edition = "2018" license = "Apache-2.0" homepage = "https://substrate.dev" repository = "https://github.com/paritytech/substrate/" -description = "NPoS Compact Solution Type" +description = "NPoS Solution Type" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] @@ -22,6 +22,6 @@ proc-macro-crate = "1.0.0" [dev-dependencies] parity-scale-codec = "2.0.1" -sp-arithmetic = { path = "../../arithmetic" , version = "4.0.0-dev"} -sp-npos-elections = { path = ".." , version = "4.0.0-dev"} +sp-arithmetic = { path = "../../arithmetic", version = "4.0.0-dev"} +sp-npos-elections = { path = "..", version = "4.0.0-dev"} trybuild = "1.0.43" diff --git a/substrate/primitives/npos-elections/compact/src/codec.rs b/substrate/primitives/npos-elections/solution-type/src/codec.rs similarity index 76% rename from substrate/primitives/npos-elections/compact/src/codec.rs rename to substrate/primitives/npos-elections/solution-type/src/codec.rs index 6d59e11f04..21688c03ac 100644 --- a/substrate/primitives/npos-elections/compact/src/codec.rs +++ b/substrate/primitives/npos-elections/solution-type/src/codec.rs @@ -17,7 +17,7 @@ //! Code generation for the ratio assignment type' encode/decode impl. -use crate::field_name_for; +use crate::vote_field; use proc_macro2::TokenStream as TokenStream2; use quote::quote; @@ -45,7 +45,7 @@ fn decode_impl( count: usize, ) -> TokenStream2 { let decode_impl_single = { - let name = field_name_for(1); + let name = vote_field(1); quote! { let #name = < @@ -60,29 +60,9 @@ fn decode_impl( } }; - let decode_impl_double = { - let name = field_name_for(2); - quote! { - let #name = - < - _npos::sp_std::prelude::Vec<( - _npos::codec::Compact<#voter_type>, - (_npos::codec::Compact<#target_type>, _npos::codec::Compact<#weight_type>), - _npos::codec::Compact<#target_type>, - )> - as - _npos::codec::Decode - >::decode(value)?; - let #name = #name - .into_iter() - .map(|(v, (t1, w), t2)| (v.0, (t1.0, w.0), t2.0)) - .collect::<_npos::sp_std::prelude::Vec<_>>(); - } - }; - - let decode_impl_rest = (3..=count) + let decode_impl_rest = (2..=count) .map(|c| { - let name = field_name_for(c); + let name = vote_field(c); let inner_impl = (0..c - 1) .map(|i| quote! { ( (inner[#i].0).0, (inner[#i].1).0 ), }) @@ -112,7 +92,7 @@ fn decode_impl( let all_field_names = (1..=count) .map(|c| { - let name = field_name_for(c); + let name = vote_field(c); quote! { #name, } }) .collect::(); @@ -121,7 +101,6 @@ fn decode_impl( impl _npos::codec::Decode for #ident { fn decode(value: &mut I) -> Result { #decode_impl_single - #decode_impl_double #decode_impl_rest // The above code generates variables with the decoded value with the same name as @@ -137,7 +116,7 @@ fn decode_impl( // `Encode` implementation. fn encode_impl(ident: syn::Ident, count: usize) -> TokenStream2 { let encode_impl_single = { - let name = field_name_for(1); + let name = vote_field(1); quote! { let #name = self.#name .iter() @@ -150,30 +129,12 @@ fn encode_impl(ident: syn::Ident, count: usize) -> TokenStream2 { } }; - let encode_impl_double = { - let name = field_name_for(2); - quote! { - let #name = self.#name - .iter() - .map(|(v, (t1, w), t2)| ( - _npos::codec::Compact(v.clone()), - ( - _npos::codec::Compact(t1.clone()), - _npos::codec::Compact(w.clone()) - ), - _npos::codec::Compact(t2.clone()), - )) - .collect::<_npos::sp_std::prelude::Vec<_>>(); - #name.encode_to(&mut r); - } - }; - - let encode_impl_rest = (3..=count) + let encode_impl_rest = (2..=count) .map(|c| { - let name = field_name_for(c); + let name = vote_field(c); // we use the knowledge of the length to avoid copy_from_slice. - let inners_compact_array = (0..c - 1) + let inners_solution_array = (0..c - 1) .map(|i| { quote! {( _npos::codec::Compact(inner[#i].0.clone()), @@ -187,7 +148,7 @@ fn encode_impl(ident: syn::Ident, count: usize) -> TokenStream2 { .iter() .map(|(v, inner, t_last)| ( _npos::codec::Compact(v.clone()), - [ #inners_compact_array ], + [ #inners_solution_array ], _npos::codec::Compact(t_last.clone()), )) .collect::<_npos::sp_std::prelude::Vec<_>>(); @@ -201,7 +162,6 @@ fn encode_impl(ident: syn::Ident, count: usize) -> TokenStream2 { fn encode(&self) -> _npos::sp_std::prelude::Vec { let mut r = vec![]; #encode_impl_single - #encode_impl_double #encode_impl_rest r } diff --git a/substrate/primitives/npos-elections/solution-type/src/from_assignment_helpers.rs b/substrate/primitives/npos-elections/solution-type/src/from_assignment_helpers.rs new file mode 100644 index 0000000000..dc194baa6d --- /dev/null +++ b/substrate/primitives/npos-elections/solution-type/src/from_assignment_helpers.rs @@ -0,0 +1,56 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 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. + +//! Helpers to generate the push code for `from_assignment` implementations. This can be shared +//! between both single_page and double_page, thus extracted here. +//! +//! All of the code in this helper module assumes some variable names, namely `who` and +//! `distribution`. + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; + +pub(crate) fn from_impl_single_push_code() -> TokenStream2 { + quote!(push(( + voter_index(&who).or_invalid_index()?, + target_index(&distribution[0].0).or_invalid_index()?, + ))) +} + +pub(crate) fn from_impl_rest_push_code(count: usize) -> TokenStream2 { + let inner = (0..count - 1).map(|i| { + quote!( + ( + target_index(&distribution[#i].0).or_invalid_index()?, + distribution[#i].1 + ) + ) + }); + + let last_index = count - 1; + let last = quote!(target_index(&distribution[#last_index].0).or_invalid_index()?); + + quote!( + push( + ( + voter_index(&who).or_invalid_index()?, + [ #( #inner ),* ], + #last, + ) + ) + ) +} diff --git a/substrate/primitives/npos-elections/compact/src/index_assignment.rs b/substrate/primitives/npos-elections/solution-type/src/index_assignment.rs similarity index 67% rename from substrate/primitives/npos-elections/compact/src/index_assignment.rs rename to substrate/primitives/npos-elections/solution-type/src/index_assignment.rs index 347be7d199..d38dc3ec30 100644 --- a/substrate/primitives/npos-elections/compact/src/index_assignment.rs +++ b/substrate/primitives/npos-elections/solution-type/src/index_assignment.rs @@ -15,16 +15,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Code generation for getting the compact representation from the `IndexAssignment` type. +//! Code generation for getting the solution representation from the `IndexAssignment` type. -use crate::field_name_for; +use crate::vote_field; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -pub(crate) fn from_impl(count: usize) -> TokenStream2 { +pub(crate) fn from_impl(struct_name: &syn::Ident, count: usize) -> TokenStream2 { let from_impl_single = { - let name = field_name_for(1); - quote!(1 => compact.#name.push( + let name = vote_field(1); + quote!(1 => #struct_name.#name.push( ( *who, distribution[0].0, @@ -32,32 +32,18 @@ pub(crate) fn from_impl(count: usize) -> TokenStream2 { ),) }; - let from_impl_double = { - let name = field_name_for(2); - quote!(2 => compact.#name.push( - ( - *who, - ( - distribution[0].0, - distribution[0].1, - ), - distribution[1].0, - ) - ),) - }; - - let from_impl_rest = (3..=count) + let from_impl_rest = (2..=count) .map(|c| { let inner = (0..c - 1) .map(|i| quote!((distribution[#i].0, distribution[#i].1),)) .collect::(); - let field_name = field_name_for(c); + let field_name = vote_field(c); let last_index = c - 1; let last = quote!(distribution[#last_index].0); quote!( - #c => compact.#field_name.push( + #c => #struct_name.#field_name.push( ( *who, [#inner], @@ -70,7 +56,6 @@ pub(crate) fn from_impl(count: usize) -> TokenStream2 { quote!( #from_impl_single - #from_impl_double #from_impl_rest ) } diff --git a/substrate/primitives/npos-elections/solution-type/src/lib.rs b/substrate/primitives/npos-elections/solution-type/src/lib.rs new file mode 100644 index 0000000000..9503f71131 --- /dev/null +++ b/substrate/primitives/npos-elections/solution-type/src/lib.rs @@ -0,0 +1,243 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 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. + +//! Proc macro for a npos solution type. + +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; +use syn::parse::{Parse, ParseStream, Result}; + +mod codec; +mod from_assignment_helpers; +mod index_assignment; +mod single_page; + +/// Get the name of a filed based on voter count. +pub(crate) fn vote_field(n: usize) -> Ident { + quote::format_ident!("votes{}", n) +} + +/// Generate a `syn::Error`. +pub(crate) fn syn_err(message: &'static str) -> syn::Error { + syn::Error::new(Span::call_site(), message) +} + +/// Generates a struct to store the election result in a small/compact way. This can encode a +/// structure which is the equivalent of a `sp_npos_elections::Assignment<_>`. +/// +/// The following data types can be configured by the macro. +/// +/// - The identifier of the voter. This can be any type that supports `parity-scale-codec`'s compact +/// encoding. +/// - The identifier of the target. This can be any type that supports `parity-scale-codec`'s +/// compact encoding. +/// - The accuracy of the ratios. This must be one of the `PerThing` types defined in +/// `sp-arithmetic`. +/// +/// Moreover, the maximum number of edges per voter (distribution per assignment) also need to be +/// specified. Attempting to convert from/to an assignment with more distributions will fail. +/// +/// For example, the following generates a public struct with name `TestSolution` with `u16` voter +/// type, `u8` target type and `Perbill` accuracy with maximum of 4 edges per voter. +/// +/// ``` +/// # use sp_npos_elections_solution_type::generate_solution_type; +/// # use sp_arithmetic::per_things::Perbill; +/// generate_solution_type!(pub struct TestSolution::< +/// VoterIndex = u16, +/// TargetIndex = u8, +/// Accuracy = Perbill, +/// >(4)); +/// ``` +/// +/// The output of this macro will roughly look like: +/// +/// ```ignore +/// struct TestSolution { +/// voters1: vec![(u16 /* voter */, u8 /* target */)] +/// voters2: vec![ +/// (u16 /* voter */, [u8 /* first target*/, Perbill /* proportion for first target */], u8 /* last target */) +/// ] +/// voters3: vec![ +/// (u16 /* voter */, [ +/// (u8 /* first target*/, Perbill /* proportion for first target */ ), +/// (u8 /* second target */, Perbill /* proportion for second target*/) +/// ], u8 /* last target */) +/// ], +/// voters4: ..., +/// } +/// +/// impl NposSolution for TestSolution {}; +/// impl Solution for TestSolution {}; +/// ``` +/// +/// The given struct provides function to convert from/to `Assignment` as part of +/// [`sp_npos_elections::Solution`] trait: +/// +/// - `fn from_assignment<..>(..)` +/// - `fn into_assignment<..>(..)` +/// +/// ## Compact Encoding +/// +/// The generated struct is by default deriving both `Encode` and `Decode`. This is okay but could +/// lead to many `0`s in the solution. If prefixed with `#[compact]`, then a custom compact encoding +/// for numbers will be used, similar to how `parity-scale-codec`'s `Compact` works. +/// +/// ``` +/// # use sp_npos_elections_solution_type::generate_solution_type; +/// # use sp_npos_elections::NposSolution; +/// # use sp_arithmetic::per_things::Perbill; +/// generate_solution_type!( +/// #[compact] +/// pub struct TestSolutionCompact::(8) +/// ); +/// ``` +#[proc_macro] +pub fn generate_solution_type(item: TokenStream) -> TokenStream { + let solution_def = syn::parse_macro_input!(item as SolutionDef); + + let imports = imports().unwrap_or_else(|e| e.to_compile_error()); + + let def = single_page::generate(solution_def).unwrap_or_else(|e| e.to_compile_error()); + + quote!( + #imports + #def + ) + .into() +} + +struct SolutionDef { + vis: syn::Visibility, + ident: syn::Ident, + voter_type: syn::Type, + target_type: syn::Type, + weight_type: syn::Type, + count: usize, + compact_encoding: bool, +} + +fn check_attributes(input: ParseStream) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer).unwrap_or_default(); + if attrs.len() > 1 { + return Err(syn_err("compact solution can accept only #[compact]")) + } + + Ok(attrs.iter().any(|attr| { + if attr.path.segments.len() == 1 { + let segment = attr.path.segments.first().expect("Vec with len 1 can be popped."); + if segment.ident == Ident::new("compact", Span::call_site()) { + return true + } + } + false + })) +} + +impl Parse for SolutionDef { + fn parse(input: ParseStream) -> syn::Result { + // optional #[compact] + let compact_encoding = check_attributes(input)?; + + // struct + let vis: syn::Visibility = input.parse()?; + let _ = ::parse(input)?; + let ident: syn::Ident = input.parse()?; + + // :: + let _ = ::parse(input)?; + let generics: syn::AngleBracketedGenericArguments = input.parse()?; + + if generics.args.len() != 3 { + return Err(syn_err("Must provide 3 generic args.")) + } + + let expected_types = ["VoterIndex", "TargetIndex", "Accuracy"]; + + let mut types: Vec = generics + .args + .iter() + .zip(expected_types.iter()) + .map(|(t, expected)| match t { + syn::GenericArgument::Type(ty) => { + // this is now an error + Err(syn::Error::new_spanned( + ty, + format!("Expected binding: `{} = ...`", expected), + )) + }, + syn::GenericArgument::Binding(syn::Binding { ident, ty, .. }) => { + // check that we have the right keyword for this position in the argument list + if ident == expected { + Ok(ty.clone()) + } else { + Err(syn::Error::new_spanned(ident, format!("Expected `{}`", expected))) + } + }, + _ => Err(syn_err("Wrong type of generic provided. Must be a `type`.")), + }) + .collect::>()?; + + let weight_type = types.pop().expect("Vector of length 3 can be popped; qed"); + let target_type = types.pop().expect("Vector of length 2 can be popped; qed"); + let voter_type = types.pop().expect("Vector of length 1 can be popped; qed"); + + // () + let count_expr: syn::ExprParen = input.parse()?; + let count = parse_parenthesized_number::(count_expr)?; + + Ok(Self { vis, ident, voter_type, target_type, weight_type, count, compact_encoding }) + } +} + +fn parse_parenthesized_number(input_expr: syn::ExprParen) -> syn::Result +where + ::Err: std::fmt::Display, +{ + let expr = input_expr.expr; + let expr_lit = match *expr { + syn::Expr::Lit(count_lit) => count_lit.lit, + _ => return Err(syn_err("Count must be literal.")), + }; + let int_lit = match expr_lit { + syn::Lit::Int(int_lit) => int_lit, + _ => return Err(syn_err("Count must be int literal.")), + }; + int_lit.base10_parse::() +} + +fn imports() -> Result { + match crate_name("sp-npos-elections") { + Ok(FoundCrate::Itself) => Ok(quote! { use crate as _npos; }), + Ok(FoundCrate::Name(sp_npos_elections)) => { + let ident = syn::Ident::new(&sp_npos_elections, Span::call_site()); + Ok(quote!( extern crate #ident as _npos; )) + }, + Err(e) => Err(syn::Error::new(Span::call_site(), e)), + } +} + +#[cfg(test)] +mod tests { + #[test] + fn ui_fail() { + let cases = trybuild::TestCases::new(); + cases.compile_fail("tests/ui/fail/*.rs"); + } +} diff --git a/substrate/primitives/npos-elections/solution-type/src/single_page.rs b/substrate/primitives/npos-elections/solution-type/src/single_page.rs new file mode 100644 index 0000000000..7dfd0e5661 --- /dev/null +++ b/substrate/primitives/npos-elections/solution-type/src/single_page.rs @@ -0,0 +1,354 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{from_assignment_helpers::*, syn_err, vote_field}; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::Result; + +pub(crate) fn generate(def: crate::SolutionDef) -> Result { + let crate::SolutionDef { + vis, + ident, + count, + voter_type, + target_type, + weight_type, + compact_encoding, + } = def; + + if count <= 2 { + Err(syn_err("cannot build solution struct with capacity less than 3."))? + } + + let single = { + let name = vote_field(1); + // NOTE: we use the visibility of the struct for the fields as well.. could be made better. + quote!( + #vis #name: _npos::sp_std::prelude::Vec<(#voter_type, #target_type)>, + ) + }; + + let rest = (2..=count) + .map(|c| { + let field_name = vote_field(c); + let array_len = c - 1; + quote!( + #vis #field_name: _npos::sp_std::prelude::Vec<( + #voter_type, + [(#target_type, #weight_type); #array_len], + #target_type + )>, + ) + }) + .collect::(); + + let len_impl = len_impl(count); + let edge_count_impl = edge_count_impl(count); + let unique_targets_impl = unique_targets_impl(count); + let remove_voter_impl = remove_voter_impl(count); + + let derives_and_maybe_compact_encoding = if compact_encoding { + // custom compact encoding. + let compact_impl = crate::codec::codec_impl( + ident.clone(), + voter_type.clone(), + target_type.clone(), + weight_type.clone(), + count, + ); + quote! { + #compact_impl + #[derive(Default, PartialEq, Eq, Clone, Debug, PartialOrd, Ord)] + } + } else { + // automatically derived. + quote!(#[derive(Default, PartialEq, Eq, Clone, Debug, _npos::codec::Encode, _npos::codec::Decode)]) + }; + + let struct_name = syn::Ident::new("solution", proc_macro2::Span::call_site()); + let assignment_name = syn::Ident::new("all_assignments", proc_macro2::Span::call_site()); + + let from_impl = from_impl(&struct_name, count); + let into_impl = into_impl(&assignment_name, count, weight_type.clone()); + let from_index_impl = crate::index_assignment::from_impl(&struct_name, count); + + Ok(quote! ( + /// A struct to encode a election assignment in a compact way. + #derives_and_maybe_compact_encoding + #vis struct #ident { #single #rest } + + use _npos::__OrInvalidIndex; + impl _npos::NposSolution for #ident { + const LIMIT: usize = #count; + type VoterIndex = #voter_type; + type TargetIndex = #target_type; + type Accuracy = #weight_type; + + fn remove_voter(&mut self, to_remove: Self::VoterIndex) -> bool { + #remove_voter_impl + return false + } + + fn from_assignment( + assignments: &[_npos::Assignment], + voter_index: FV, + target_index: FT, + ) -> Result + where + A: _npos::IdentifierT, + for<'r> FV: Fn(&'r A) -> Option, + for<'r> FT: Fn(&'r A) -> Option, + { + let mut #struct_name: #ident = Default::default(); + for _npos::Assignment { who, distribution } in assignments { + match distribution.len() { + 0 => continue, + #from_impl + _ => { + return Err(_npos::Error::SolutionTargetOverflow); + } + } + }; + Ok(#struct_name) + } + + fn into_assignment( + self, + voter_at: impl Fn(Self::VoterIndex) -> Option, + target_at: impl Fn(Self::TargetIndex) -> Option, + ) -> Result<_npos::sp_std::prelude::Vec<_npos::Assignment>, _npos::Error> { + let mut #assignment_name: _npos::sp_std::prelude::Vec<_npos::Assignment> = Default::default(); + #into_impl + Ok(#assignment_name) + } + + fn voter_count(&self) -> usize { + let mut all_len = 0usize; + #len_impl + all_len + } + + fn edge_count(&self) -> usize { + let mut all_edges = 0usize; + #edge_count_impl + all_edges + } + + fn unique_targets(&self) -> _npos::sp_std::prelude::Vec { + // NOTE: this implementation returns the targets sorted, but we don't use it yet per + // se, nor is the API enforcing it. + use _npos::sp_std::collections::btree_set::BTreeSet; + let mut all_targets: BTreeSet = BTreeSet::new(); + let mut maybe_insert_target = |t: Self::TargetIndex| { + all_targets.insert(t); + }; + + #unique_targets_impl + + all_targets.into_iter().collect() + } + } + + type __IndexAssignment = _npos::IndexAssignment< + <#ident as _npos::NposSolution>::VoterIndex, + <#ident as _npos::NposSolution>::TargetIndex, + <#ident as _npos::NposSolution>::Accuracy, + >; + impl<'a> _npos::sp_std::convert::TryFrom<&'a [__IndexAssignment]> for #ident { + type Error = _npos::Error; + fn try_from(index_assignments: &'a [__IndexAssignment]) -> Result { + let mut #struct_name = #ident::default(); + + for _npos::IndexAssignment { who, distribution } in index_assignments { + match distribution.len() { + 0 => {} + #from_index_impl + _ => { + return Err(_npos::Error::SolutionTargetOverflow); + } + } + }; + + Ok(#struct_name) + } + } + )) +} + +fn remove_voter_impl(count: usize) -> TokenStream2 { + let field_name = vote_field(1); + let single = quote! { + if let Some(idx) = self.#field_name.iter().position(|(x, _)| *x == to_remove) { + self.#field_name.remove(idx); + return true + } + }; + + let rest = (2..=count) + .map(|c| { + let field_name = vote_field(c); + quote! { + if let Some(idx) = self.#field_name.iter().position(|(x, _, _)| *x == to_remove) { + self.#field_name.remove(idx); + return true + } + } + }) + .collect::(); + + quote! { + #single + #rest + } +} + +fn len_impl(count: usize) -> TokenStream2 { + (1..=count) + .map(|c| { + let field_name = vote_field(c); + quote!( + all_len = all_len.saturating_add(self.#field_name.len()); + ) + }) + .collect::() +} + +fn edge_count_impl(count: usize) -> TokenStream2 { + (1..=count) + .map(|c| { + let field_name = vote_field(c); + quote!( + all_edges = all_edges.saturating_add( + self.#field_name.len().saturating_mul(#c as usize) + ); + ) + }) + .collect::() +} + +fn unique_targets_impl(count: usize) -> TokenStream2 { + let unique_targets_impl_single = { + let field_name = vote_field(1); + quote! { + self.#field_name.iter().for_each(|(_, t)| { + maybe_insert_target(*t); + }); + } + }; + + let unique_targets_impl_rest = (2..=count) + .map(|c| { + let field_name = vote_field(c); + quote! { + self.#field_name.iter().for_each(|(_, inners, t_last)| { + inners.iter().for_each(|(t, _)| { + maybe_insert_target(*t); + }); + maybe_insert_target(*t_last); + }); + } + }) + .collect::(); + + quote! { + #unique_targets_impl_single + #unique_targets_impl_rest + } +} + +pub(crate) fn from_impl(struct_name: &syn::Ident, count: usize) -> TokenStream2 { + let from_impl_single = { + let field = vote_field(1); + let push_code = from_impl_single_push_code(); + quote!(1 => #struct_name.#field.#push_code,) + }; + + let from_impl_rest = (2..=count) + .map(|c| { + let field = vote_field(c); + let push_code = from_impl_rest_push_code(c); + quote!(#c => #struct_name.#field.#push_code,) + }) + .collect::(); + + quote!( + #from_impl_single + #from_impl_rest + ) +} + +pub(crate) fn into_impl( + assignments: &syn::Ident, + count: usize, + per_thing: syn::Type, +) -> TokenStream2 { + let into_impl_single = { + let name = vote_field(1); + quote!( + for (voter_index, target_index) in self.#name { + #assignments.push(_npos::Assignment { + who: voter_at(voter_index).or_invalid_index()?, + distribution: vec![ + (target_at(target_index).or_invalid_index()?, #per_thing::one()) + ], + }) + } + ) + }; + + let into_impl_rest = (2..=count) + .map(|c| { + let name = vote_field(c); + quote!( + for (voter_index, inners, t_last_idx) in self.#name { + let mut sum = #per_thing::zero(); + let mut inners_parsed = inners + .iter() + .map(|(ref t_idx, p)| { + sum = _npos::sp_arithmetic::traits::Saturating::saturating_add(sum, *p); + let target = target_at(*t_idx).or_invalid_index()?; + Ok((target, *p)) + }) + .collect::, _npos::Error>>()?; + + if sum >= #per_thing::one() { + return Err(_npos::Error::SolutionWeightOverflow); + } + + // defensive only. Since Percent doesn't have `Sub`. + let p_last = _npos::sp_arithmetic::traits::Saturating::saturating_sub( + #per_thing::one(), + sum, + ); + + inners_parsed.push((target_at(t_last_idx).or_invalid_index()?, p_last)); + + #assignments.push(_npos::Assignment { + who: voter_at(voter_index).or_invalid_index()?, + distribution: inners_parsed, + }); + } + ) + }) + .collect::(); + + quote!( + #into_impl_single + #into_impl_rest + ) +} diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_accuracy.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_accuracy.rs similarity index 66% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_accuracy.rs rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_accuracy.rs index 4bbf4960a9..b74b857e45 100644 --- a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_accuracy.rs +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_accuracy.rs @@ -1,4 +1,4 @@ -use sp_npos_elections_compact::generate_solution_type; +use sp_npos_elections_solution_type::generate_solution_type; generate_solution_type!(pub struct TestSolution::< VoterIndex = u16, diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_accuracy.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_accuracy.stderr similarity index 100% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_accuracy.stderr rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_accuracy.stderr diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_target.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_target.rs similarity index 65% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_target.rs rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_target.rs index 7d75843407..4c9cd51a32 100644 --- a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_target.rs +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_target.rs @@ -1,4 +1,4 @@ -use sp_npos_elections_compact::generate_solution_type; +use sp_npos_elections_solution_type::generate_solution_type; generate_solution_type!(pub struct TestSolution::< VoterIndex = u16, diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_target.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_target.stderr similarity index 100% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_target.stderr rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_target.stderr diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_voter.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_voter.rs similarity index 66% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_voter.rs rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_voter.rs index 3ad77dc104..b87037f77f 100644 --- a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_voter.rs +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_voter.rs @@ -1,4 +1,4 @@ -use sp_npos_elections_compact::generate_solution_type; +use sp_npos_elections_solution_type::generate_solution_type; generate_solution_type!(pub struct TestSolution::< u16, diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/missing_voter.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_voter.stderr similarity index 100% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/missing_voter.stderr rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/missing_voter.stderr diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/no_annotations.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/no_annotations.rs similarity index 60% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/no_annotations.rs rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/no_annotations.rs index aaebb857b3..cfca2841db 100644 --- a/substrate/primitives/npos-elections/compact/tests/ui/fail/no_annotations.rs +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/no_annotations.rs @@ -1,4 +1,4 @@ -use sp_npos_elections_compact::generate_solution_type; +use sp_npos_elections_solution_type::generate_solution_type; generate_solution_type!(pub struct TestSolution::< u16, diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/no_annotations.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/no_annotations.stderr similarity index 100% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/no_annotations.stderr rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/no_annotations.stderr diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/swap_voter_target.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/swap_voter_target.rs similarity index 68% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/swap_voter_target.rs rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/swap_voter_target.rs index 37124256b3..443202d11b 100644 --- a/substrate/primitives/npos-elections/compact/tests/ui/fail/swap_voter_target.rs +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/swap_voter_target.rs @@ -1,4 +1,4 @@ -use sp_npos_elections_compact::generate_solution_type; +use sp_npos_elections_solution_type::generate_solution_type; generate_solution_type!(pub struct TestSolution::< TargetIndex = u16, diff --git a/substrate/primitives/npos-elections/compact/tests/ui/fail/swap_voter_target.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/swap_voter_target.stderr similarity index 100% rename from substrate/primitives/npos-elections/compact/tests/ui/fail/swap_voter_target.stderr rename to substrate/primitives/npos-elections/solution-type/tests/ui/fail/swap_voter_target.stderr diff --git a/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.rs b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.rs new file mode 100644 index 0000000000..3008277e36 --- /dev/null +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.rs @@ -0,0 +1,11 @@ +use sp_npos_elections_solution_type::generate_solution_type; + +generate_solution_type!( + #[pages(1)] pub struct TestSolution::< + VoterIndex = u8, + TargetIndex = u16, + Accuracy = Perbill, + >(8) +); + +fn main() {} diff --git a/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.stderr b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.stderr new file mode 100644 index 0000000000..7104305a9e --- /dev/null +++ b/substrate/primitives/npos-elections/solution-type/tests/ui/fail/wrong_page.stderr @@ -0,0 +1,38 @@ +error[E0412]: cannot find type `Perbill` in this scope + --> $DIR/wrong_page.rs:7:14 + | +7 | Accuracy = Perbill, + | ^^^^^^^ not found in this scope + | +help: consider importing this struct + | +1 | use sp_arithmetic::Perbill; + | + +error[E0433]: failed to resolve: use of undeclared type `Perbill` + --> $DIR/wrong_page.rs:7:14 + | +7 | Accuracy = Perbill, + | ^^^^^^^ not found in this scope + | +help: consider importing this struct + | +1 | use sp_arithmetic::Perbill; + | + +error[E0119]: conflicting implementations of trait `std::convert::TryFrom<&[_npos::IndexAssignment]>` for type `TestSolution` + --> $DIR/wrong_page.rs:3:1 + | +3 | / generate_solution_type!( +4 | | #[pages(1)] pub struct TestSolution::< +5 | | VoterIndex = u8, +6 | | TargetIndex = u16, +7 | | Accuracy = Perbill, +8 | | >(8) +9 | | ); + | |__^ + | + = note: conflicting implementation in crate `core`: + - impl TryFrom for T + where U: Into; + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/substrate/primitives/npos-elections/src/assignments.rs b/substrate/primitives/npos-elections/src/assignments.rs index da101e64a7..bdd1e2cd28 100644 --- a/substrate/primitives/npos-elections/src/assignments.rs +++ b/substrate/primitives/npos-elections/src/assignments.rs @@ -167,11 +167,11 @@ impl StakedAssignment { } } /// The [`IndexAssignment`] type is an intermediate between the assignments list -/// ([`&[Assignment]`][Assignment]) and `CompactOf`. +/// ([`&[Assignment]`][Assignment]) and `SolutionOf`. /// /// The voter and target identifiers have already been replaced with appropriate indices, -/// making it fast to repeatedly encode into a `CompactOf`. This property turns out -/// to be important when trimming for compact length. +/// making it fast to repeatedly encode into a `SolutionOf`. This property turns out +/// to be important when trimming for solution length. #[derive(RuntimeDebug, Clone, Default)] #[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))] pub struct IndexAssignment { @@ -201,9 +201,9 @@ impl IndexAssignment = IndexAssignment< - ::Voter, - ::Target, - ::Accuracy, + ::VoterIndex, + ::TargetIndex, + ::Accuracy, >; diff --git a/substrate/primitives/npos-elections/src/lib.rs b/substrate/primitives/npos-elections/src/lib.rs index ece5be33b1..6a7e7e8c23 100644 --- a/substrate/primitives/npos-elections/src/lib.rs +++ b/substrate/primitives/npos-elections/src/lib.rs @@ -74,21 +74,9 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_arithmetic::{ - traits::{Bounded, UniqueSaturatedInto, Zero}, - Normalizable, PerThing, Rational128, ThresholdOrd, -}; +use sp_arithmetic::{traits::Zero, Normalizable, PerThing, Rational128, ThresholdOrd}; use sp_core::RuntimeDebug; -use sp_std::{ - cell::RefCell, - cmp::Ordering, - collections::btree_map::BTreeMap, - convert::{TryFrom, TryInto}, - fmt::Debug, - ops::Mul, - prelude::*, - rc::Rc, -}; +use sp_std::{cell::RefCell, cmp::Ordering, collections::btree_map::BTreeMap, prelude::*, rc::Rc}; use codec::{Decode, Encode}; #[cfg(feature = "std")] @@ -107,6 +95,7 @@ pub mod phragmen; pub mod phragmms; pub mod pjr; pub mod reduce; +pub mod traits; pub use assignments::{Assignment, IndexAssignment, IndexAssignmentOf, StakedAssignment}; pub use balancing::*; @@ -115,8 +104,9 @@ pub use phragmen::*; pub use phragmms::*; pub use pjr::*; pub use reduce::reduce; +pub use traits::{IdentifierT, NposSolution, PerThing128, __OrInvalidIndex}; -// re-export the compact macro, with the dependencies of the macro. +// re-export for the solution macro, with the dependencies of the macro. #[doc(hidden)] pub use codec; #[doc(hidden)] @@ -124,141 +114,21 @@ pub use sp_arithmetic; #[doc(hidden)] pub use sp_std; -/// Simple Extension trait to easily convert `None` from index closures to `Err`. -/// -/// This is only generated and re-exported for the compact solution code to use. -#[doc(hidden)] -pub trait __OrInvalidIndex { - fn or_invalid_index(self) -> Result; -} +// re-export the solution type macro. +pub use sp_npos_elections_solution_type::generate_solution_type; -impl __OrInvalidIndex for Option { - fn or_invalid_index(self) -> Result { - self.ok_or(Error::CompactInvalidIndex) - } -} - -/// A common interface for all compact solutions. -/// -/// See [`sp-npos-elections-compact`] for more info. -pub trait CompactSolution -where - Self: Sized + for<'a> sp_std::convert::TryFrom<&'a [IndexAssignmentOf], Error = Error>, -{ - /// The maximum number of votes that are allowed. - const LIMIT: usize; - - /// The voter type. Needs to be an index (convert to usize). - type Voter: UniqueSaturatedInto - + TryInto - + TryFrom - + Debug - + Copy - + Clone - + Bounded; - - /// The target type. Needs to be an index (convert to usize). - type Target: UniqueSaturatedInto - + TryInto - + TryFrom - + Debug - + Copy - + Clone - + Bounded; - - /// The weight/accuracy type of each vote. - type Accuracy: PerThing128; - - /// Build self from a list of assignments. - fn from_assignment( - assignments: &[Assignment], - voter_index: FV, - target_index: FT, - ) -> Result - where - A: IdentifierT, - for<'r> FV: Fn(&'r A) -> Option, - for<'r> FT: Fn(&'r A) -> Option; - - /// Convert self into a `Vec>` - fn into_assignment( - self, - voter_at: impl Fn(Self::Voter) -> Option, - target_at: impl Fn(Self::Target) -> Option, - ) -> Result>, Error>; - - /// Get the length of all the voters that this type is encoding. - /// - /// This is basically the same as the number of assignments, or number of active voters. - fn voter_count(&self) -> usize; - - /// Get the total count of edges. - /// - /// This is effectively in the range of {[`Self::voter_count`], [`Self::voter_count`] * - /// [`Self::LIMIT`]}. - fn edge_count(&self) -> usize; - - /// Get the number of unique targets in the whole struct. - /// - /// Once presented with a list of winners, this set and the set of winners must be - /// equal. - fn unique_targets(&self) -> Vec; - - /// Get the average edge count. - fn average_edge_count(&self) -> usize { - self.edge_count().checked_div(self.voter_count()).unwrap_or(0) - } - - /// Remove a certain voter. - /// - /// This will only search until the first instance of `to_remove`, and return true. If - /// no instance is found (no-op), then it returns false. - /// - /// In other words, if this return true, exactly **one** element must have been removed from - /// `self.len()`. - fn remove_voter(&mut self, to_remove: Self::Voter) -> bool; - - /// Compute the score of this compact solution type. - fn score( - self, - winners: &[A], - stake_of: FS, - voter_at: impl Fn(Self::Voter) -> Option, - target_at: impl Fn(Self::Target) -> Option, - ) -> Result - where - for<'r> FS: Fn(&'r A) -> VoteWeight, - A: IdentifierT, - { - let ratio = self.into_assignment(voter_at, target_at)?; - let staked = helpers::assignment_ratio_to_staked_normalized(ratio, stake_of)?; - let supports = to_supports(winners, &staked)?; - Ok(supports.evaluate()) - } -} - -// re-export the compact solution type. -pub use sp_npos_elections_compact::generate_solution_type; - -/// an aggregator trait for a generic type of a voter/target identifier. This usually maps to -/// substrate's account id. -pub trait IdentifierT: Clone + Eq + Default + Ord + Debug + codec::Codec {} -impl IdentifierT for T {} - -/// Aggregator trait for a PerThing that can be multiplied by u128 (ExtendedBalance). -pub trait PerThing128: PerThing + Mul {} -impl> PerThing128 for T {} - -/// The errors that might occur in the this crate and compact. +/// The errors that might occur in the this crate and solution-type. #[derive(Eq, PartialEq, RuntimeDebug)] pub enum Error { - /// While going from compact to staked, the stake of all the edges has gone above the total and - /// the last stake cannot be assigned. - CompactStakeOverflow, - /// The compact type has a voter who's number of targets is out of bound. - CompactTargetOverflow, + /// While going from solution indices to ratio, the weight of all the edges has gone above the + /// total. + SolutionWeightOverflow, + /// The solution type has a voter who's number of targets is out of bound. + SolutionTargetOverflow, /// One of the index functions returned none. - CompactInvalidIndex, + SolutionInvalidIndex, + /// One of the page indices was invalid + SolutionInvalidPageIndex, /// An error occurred in some arithmetic operation. ArithmeticError(&'static str), /// The data provided to create support map was invalid. @@ -507,12 +377,12 @@ impl FlattenSupportMap for SupportMap { /// /// The list of winners is basically a redundancy for error checking only; It ensures that all the /// targets pointed to by the [`Assignment`] are present in the `winners`. -pub fn to_support_map( - winners: &[A], - assignments: &[StakedAssignment], -) -> Result, Error> { +pub fn to_support_map( + winners: &[AccountId], + assignments: &[StakedAssignment], +) -> Result, Error> { // Initialize the support of each candidate. - let mut supports = >::new(); + let mut supports = >::new(); winners.iter().for_each(|e| { supports.insert(e.clone(), Default::default()); }); @@ -535,10 +405,10 @@ pub fn to_support_map( /// flat vector. /// /// Similar to [`to_support_map`], `winners` is used for error checking. -pub fn to_supports( - winners: &[A], - assignments: &[StakedAssignment], -) -> Result, Error> { +pub fn to_supports( + winners: &[AccountId], + assignments: &[StakedAssignment], +) -> Result, Error> { to_support_map(winners, assignments).map(FlattenSupportMap::flatten) } diff --git a/substrate/primitives/npos-elections/src/mock.rs b/substrate/primitives/npos-elections/src/mock.rs index 8de0c09959..36fd78b575 100644 --- a/substrate/primitives/npos-elections/src/mock.rs +++ b/substrate/primitives/npos-elections/src/mock.rs @@ -17,7 +17,7 @@ //! Mock file for npos-elections. -#![cfg(any(test, mocks))] +#![cfg(test)] use std::{ collections::{HashMap, HashSet}, @@ -35,20 +35,27 @@ use sp_std::collections::btree_map::BTreeMap; use crate::{seq_phragmen, Assignment, ElectionResult, ExtendedBalance, PerThing128, VoteWeight}; -sp_npos_elections_compact::generate_solution_type!( - #[compact] - pub struct Compact::(16) -); - pub type AccountId = u64; + /// The candidate mask allows easy disambiguation between voters and candidates: accounts /// for which this bit is set are candidates, and without it, are voters. pub const CANDIDATE_MASK: AccountId = 1 << ((std::mem::size_of::() * 8) - 1); -pub type CandidateId = AccountId; -pub type Accuracy = sp_runtime::Perbill; +pub type TestAccuracy = sp_runtime::Perbill; -pub type MockAssignment = crate::Assignment; +crate::generate_solution_type! { + pub struct TestSolution::< + VoterIndex = u32, + TargetIndex = u16, + Accuracy = TestAccuracy, + >(16) +} + +pub fn p(p: u8) -> TestAccuracy { + TestAccuracy::from_percent(p.into()) +} + +pub type MockAssignment = crate::Assignment; pub type Voter = (AccountId, VoteWeight, Vec); #[derive(Default, Debug)] @@ -422,7 +429,7 @@ pub fn generate_random_votes( candidate_count: usize, voter_count: usize, mut rng: impl Rng, -) -> (Vec, Vec, Vec) { +) -> (Vec, Vec, Vec) { // cache for fast generation of unique candidate and voter ids let mut used_ids = HashSet::with_capacity(candidate_count + voter_count); @@ -452,7 +459,8 @@ pub fn generate_random_votes( // it's not interesting if a voter chooses 0 or all candidates, so rule those cases out. // also, let's not generate any cases which result in a compact overflow. - let n_candidates_chosen = rng.gen_range(1, candidates.len().min(16)); + let n_candidates_chosen = + rng.gen_range(1, candidates.len().min(::LIMIT)); let mut chosen_candidates = Vec::with_capacity(n_candidates_chosen); chosen_candidates.extend(candidates.choose_multiple(&mut rng, n_candidates_chosen)); @@ -473,16 +481,16 @@ pub fn generate_random_votes( // distribute the available stake randomly let stake_distribution = if num_chosen_winners == 0 { - Vec::new() + continue } else { let mut available_stake = 1000; let mut stake_distribution = Vec::with_capacity(num_chosen_winners); for _ in 0..num_chosen_winners - 1 { - let stake = rng.gen_range(0, available_stake); - stake_distribution.push(Accuracy::from_perthousand(stake)); + let stake = rng.gen_range(0, available_stake).min(1); + stake_distribution.push(TestAccuracy::from_perthousand(stake)); available_stake -= stake; } - stake_distribution.push(Accuracy::from_perthousand(available_stake)); + stake_distribution.push(TestAccuracy::from_perthousand(available_stake)); stake_distribution.shuffle(&mut rng); stake_distribution }; @@ -514,16 +522,26 @@ where usize: TryInto, { let cache = generate_cache(voters.iter().map(|(id, _, _)| *id)); - move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok()) + move |who| { + if cache.get(who).is_none() { + println!("WARNING: voter {} will raise InvalidIndex", who); + } + cache.get(who).cloned().and_then(|i| i.try_into().ok()) + } } /// Create a function that returns the index of a candidate in the candidates list. pub fn make_target_fn( - candidates: &[CandidateId], -) -> impl Fn(&CandidateId) -> Option + candidates: &[AccountId], +) -> impl Fn(&AccountId) -> Option where usize: TryInto, { let cache = generate_cache(candidates.iter().cloned()); - move |who| cache.get(who).cloned().and_then(|i| i.try_into().ok()) + move |who| { + if cache.get(who).is_none() { + println!("WARNING: target {} will raise InvalidIndex", who); + } + cache.get(who).cloned().and_then(|i| i.try_into().ok()) + } } diff --git a/substrate/primitives/npos-elections/src/tests.rs b/substrate/primitives/npos-elections/src/tests.rs index da6b417b61..eac218f77e 100644 --- a/substrate/primitives/npos-elections/src/tests.rs +++ b/substrate/primitives/npos-elections/src/tests.rs @@ -19,8 +19,8 @@ use crate::{ balancing, helpers::*, is_score_better, mock::*, seq_phragmen, seq_phragmen_core, setup_inputs, - to_support_map, to_supports, Assignment, CompactSolution, ElectionResult, EvaluateSupport, - ExtendedBalance, IndexAssignment, StakedAssignment, Support, Voter, + to_support_map, to_supports, Assignment, ElectionResult, EvaluateSupport, ExtendedBalance, + IndexAssignment, NposSolution, StakedAssignment, Support, Voter, }; use rand::{self, SeedableRng}; use sp_arithmetic::{PerU16, Perbill, Percent, Permill}; @@ -917,30 +917,20 @@ mod score { } mod solution_type { - use super::AccountId; + use super::*; use codec::{Decode, Encode}; // these need to come from the same dev-dependency `sp-npos-elections`, not from the crate. - use crate::{generate_solution_type, Assignment, CompactSolution, Error as PhragmenError}; - use sp_arithmetic::Percent; + use crate::{generate_solution_type, Assignment, Error as NposError, NposSolution}; use sp_std::{convert::TryInto, fmt::Debug}; - type TestAccuracy = Percent; - - generate_solution_type!(pub struct TestSolutionCompact::< - VoterIndex = u32, - TargetIndex = u8, - Accuracy = TestAccuracy, - >(16)); - #[allow(dead_code)] mod __private { - // This is just to make sure that that the compact can be generated in a scope without any + // This is just to make sure that the solution can be generated in a scope without any // imports. use crate::generate_solution_type; - use sp_arithmetic::Percent; generate_solution_type!( #[compact] - struct InnerTestSolutionCompact::(12) + struct InnerTestSolutionIsolated::(12) ); } @@ -948,35 +938,34 @@ mod solution_type { fn solution_struct_works_with_and_without_compact() { // we use u32 size to make sure compact is smaller. let without_compact = { - generate_solution_type!(pub struct InnerTestSolution::< - VoterIndex = u32, - TargetIndex = u32, - Accuracy = Percent, - >(16)); - let compact = InnerTestSolution { + generate_solution_type!( + pub struct InnerTestSolution::< + VoterIndex = u32, + TargetIndex = u32, + Accuracy = TestAccuracy, + >(16) + ); + let solution = InnerTestSolution { votes1: vec![(2, 20), (4, 40)], - votes2: vec![ - (1, (10, TestAccuracy::from_percent(80)), 11), - (5, (50, TestAccuracy::from_percent(85)), 51), - ], + votes2: vec![(1, [(10, p(80))], 11), (5, [(50, p(85))], 51)], ..Default::default() }; - compact.encode().len() + solution.encode().len() }; let with_compact = { - generate_solution_type!(#[compact] pub struct InnerTestSolutionCompact::< - VoterIndex = u32, - TargetIndex = u32, - Accuracy = Percent, - >(16)); + generate_solution_type!( + #[compact] + pub struct InnerTestSolutionCompact::< + VoterIndex = u32, + TargetIndex = u32, + Accuracy = TestAccuracy, + >(16) + ); let compact = InnerTestSolutionCompact { votes1: vec![(2, 20), (4, 40)], - votes2: vec![ - (1, (10, TestAccuracy::from_percent(80)), 11), - (5, (50, TestAccuracy::from_percent(85)), 51), - ], + votes2: vec![(1, [(10, p(80))], 11), (5, [(50, p(85))], 51)], ..Default::default() }; @@ -988,78 +977,64 @@ mod solution_type { #[test] fn solution_struct_is_codec() { - let compact = TestSolutionCompact { + let solution = TestSolution { votes1: vec![(2, 20), (4, 40)], - votes2: vec![ - (1, (10, TestAccuracy::from_percent(80)), 11), - (5, (50, TestAccuracy::from_percent(85)), 51), - ], + votes2: vec![(1, [(10, p(80))], 11), (5, [(50, p(85))], 51)], ..Default::default() }; - let encoded = compact.encode(); + let encoded = solution.encode(); - assert_eq!(compact, Decode::decode(&mut &encoded[..]).unwrap()); - assert_eq!(compact.voter_count(), 4); - assert_eq!(compact.edge_count(), 2 + 4); - assert_eq!(compact.unique_targets(), vec![10, 11, 20, 40, 50, 51]); + assert_eq!(solution, Decode::decode(&mut &encoded[..]).unwrap()); + assert_eq!(solution.voter_count(), 4); + assert_eq!(solution.edge_count(), 2 + 4); + assert_eq!(solution.unique_targets(), vec![10, 11, 20, 40, 50, 51]); } #[test] fn remove_voter_works() { - let mut compact = TestSolutionCompact { + let mut solution = TestSolution { votes1: vec![(0, 2), (1, 6)], - votes2: vec![ - (2, (0, TestAccuracy::from_percent(80)), 1), - (3, (7, TestAccuracy::from_percent(85)), 8), - ], - votes3: vec![( - 4, - [(3, TestAccuracy::from_percent(50)), (4, TestAccuracy::from_percent(25))], - 5, - )], + votes2: vec![(2, [(0, p(80))], 1), (3, [(7, p(85))], 8)], + votes3: vec![(4, [(3, p(50)), (4, p(25))], 5)], ..Default::default() }; - assert!(!compact.remove_voter(11)); - assert!(compact.remove_voter(2)); + assert!(!solution.remove_voter(11)); + assert!(solution.remove_voter(2)); assert_eq!( - compact, - TestSolutionCompact { + solution, + TestSolution { votes1: vec![(0, 2), (1, 6)], - votes2: vec![(3, (7, TestAccuracy::from_percent(85)), 8),], - votes3: vec![( - 4, - [(3, TestAccuracy::from_percent(50)), (4, TestAccuracy::from_percent(25))], - 5, - ),], + votes2: vec![(3, [(7, p(85))], 8)], + votes3: vec![(4, [(3, p(50)), (4, p(25))], 5,)], ..Default::default() }, ); - assert!(compact.remove_voter(4)); + assert!(solution.remove_voter(4)); assert_eq!( - compact, - TestSolutionCompact { + solution, + TestSolution { votes1: vec![(0, 2), (1, 6)], - votes2: vec![(3, (7, TestAccuracy::from_percent(85)), 8),], + votes2: vec![(3, [(7, p(85))], 8)], ..Default::default() }, ); - assert!(compact.remove_voter(1)); + assert!(solution.remove_voter(1)); assert_eq!( - compact, - TestSolutionCompact { + solution, + TestSolution { votes1: vec![(0, 2)], - votes2: vec![(3, (7, TestAccuracy::from_percent(85)), 8),], + votes2: vec![(3, [(7, p(85))], 8),], ..Default::default() }, ); } #[test] - fn basic_from_and_into_compact_works_assignments() { + fn from_and_into_assignment_works() { let voters = vec![2 as AccountId, 4, 1, 5, 3]; let targets = vec![ 10 as AccountId, @@ -1074,182 +1049,144 @@ mod solution_type { ]; let assignments = vec![ - Assignment { - who: 2 as AccountId, - distribution: vec![(20u64, TestAccuracy::from_percent(100))], - }, - Assignment { who: 4, distribution: vec![(40, TestAccuracy::from_percent(100))] }, - Assignment { - who: 1, - distribution: vec![ - (10, TestAccuracy::from_percent(80)), - (11, TestAccuracy::from_percent(20)), - ], - }, - Assignment { - who: 5, - distribution: vec![ - (50, TestAccuracy::from_percent(85)), - (51, TestAccuracy::from_percent(15)), - ], - }, - Assignment { - who: 3, - distribution: vec![ - (30, TestAccuracy::from_percent(50)), - (31, TestAccuracy::from_percent(25)), - (32, TestAccuracy::from_percent(25)), - ], - }, + Assignment { who: 2 as AccountId, distribution: vec![(20u64, p(100))] }, + Assignment { who: 4, distribution: vec![(40, p(100))] }, + Assignment { who: 1, distribution: vec![(10, p(80)), (11, p(20))] }, + Assignment { who: 5, distribution: vec![(50, p(85)), (51, p(15))] }, + Assignment { who: 3, distribution: vec![(30, p(50)), (31, p(25)), (32, p(25))] }, ]; let voter_index = |a: &AccountId| -> Option { voters.iter().position(|x| x == a).map(TryInto::try_into).unwrap().ok() }; - let target_index = |a: &AccountId| -> Option { + let target_index = |a: &AccountId| -> Option { targets.iter().position(|x| x == a).map(TryInto::try_into).unwrap().ok() }; - let compacted = - TestSolutionCompact::from_assignment(&assignments, voter_index, target_index).unwrap(); + let solution = + TestSolution::from_assignment(&assignments, voter_index, target_index).unwrap(); // basically number of assignments that it is encoding. - assert_eq!(compacted.voter_count(), assignments.len()); + assert_eq!(solution.voter_count(), assignments.len()); assert_eq!( - compacted.edge_count(), + solution.edge_count(), assignments.iter().fold(0, |a, b| a + b.distribution.len()), ); assert_eq!( - compacted, - TestSolutionCompact { + solution, + TestSolution { votes1: vec![(0, 2), (1, 6)], - votes2: vec![ - (2, (0, TestAccuracy::from_percent(80)), 1), - (3, (7, TestAccuracy::from_percent(85)), 8), - ], - votes3: vec![( - 4, - [(3, TestAccuracy::from_percent(50)), (4, TestAccuracy::from_percent(25))], - 5, - ),], + votes2: vec![(2, [(0, p(80))], 1), (3, [(7, p(85))], 8)], + votes3: vec![(4, [(3, p(50)), (4, p(25))], 5)], ..Default::default() } ); - assert_eq!(compacted.unique_targets(), vec![0, 1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!(solution.unique_targets(), vec![0, 1, 2, 3, 4, 5, 6, 7, 8]); let voter_at = |a: u32| -> Option { voters.get(>::try_into(a).unwrap()).cloned() }; - let target_at = |a: u8| -> Option { - targets.get(>::try_into(a).unwrap()).cloned() + let target_at = |a: u16| -> Option { + targets.get(>::try_into(a).unwrap()).cloned() }; - assert_eq!(compacted.into_assignment(voter_at, target_at).unwrap(), assignments); + assert_eq!(solution.into_assignment(voter_at, target_at).unwrap(), assignments); } #[test] fn unique_targets_len_edge_count_works() { - const ACC: TestAccuracy = TestAccuracy::from_percent(10); - // we don't really care about voters here so all duplicates. This is not invalid per se. - let compact = TestSolutionCompact { + let solution = TestSolution { votes1: vec![(99, 1), (99, 2)], - votes2: vec![(99, (3, ACC.clone()), 7), (99, (4, ACC.clone()), 8)], - votes3: vec![(99, [(11, ACC.clone()), (12, ACC.clone())], 13)], + votes2: vec![(99, [(3, p(10))], 7), (99, [(4, p(10))], 8)], + votes3: vec![(99, [(11, p(10)), (12, p(10))], 13)], // ensure the last one is also counted. votes16: vec![( 99, [ - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), - (66, ACC.clone()), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), + (66, p(10)), ], 67, )], ..Default::default() }; - assert_eq!(compact.unique_targets(), vec![1, 2, 3, 4, 7, 8, 11, 12, 13, 66, 67]); - assert_eq!(compact.edge_count(), 2 + (2 * 2) + 3 + 16); - assert_eq!(compact.voter_count(), 6); + assert_eq!(solution.unique_targets(), vec![1, 2, 3, 4, 7, 8, 11, 12, 13, 66, 67]); + assert_eq!(solution.edge_count(), 2 + (2 * 2) + 3 + 16); + assert_eq!(solution.voter_count(), 6); // this one has some duplicates. - let compact = TestSolutionCompact { + let solution = TestSolution { votes1: vec![(99, 1), (99, 1)], - votes2: vec![(99, (3, ACC.clone()), 7), (99, (4, ACC.clone()), 8)], - votes3: vec![(99, [(11, ACC.clone()), (11, ACC.clone())], 13)], + votes2: vec![(99, [(3, p(10))], 7), (99, [(4, p(10))], 8)], + votes3: vec![(99, [(11, p(10)), (11, p(10))], 13)], ..Default::default() }; - assert_eq!(compact.unique_targets(), vec![1, 3, 4, 7, 8, 11, 13]); - assert_eq!(compact.edge_count(), 2 + (2 * 2) + 3); - assert_eq!(compact.voter_count(), 5); + assert_eq!(solution.unique_targets(), vec![1, 3, 4, 7, 8, 11, 13]); + assert_eq!(solution.edge_count(), 2 + (2 * 2) + 3); + assert_eq!(solution.voter_count(), 5); } #[test] - fn compact_into_assignment_must_report_overflow() { + fn solution_into_assignment_must_report_overflow() { // in votes2 - let compact = TestSolutionCompact { + let solution = TestSolution { votes1: Default::default(), - votes2: vec![(0, (1, TestAccuracy::from_percent(100)), 2)], + votes2: vec![(0, [(1, p(100))], 2)], ..Default::default() }; let voter_at = |a: u32| -> Option { Some(a as AccountId) }; - let target_at = |a: u8| -> Option { Some(a as AccountId) }; + let target_at = |a: u16| -> Option { Some(a as AccountId) }; assert_eq!( - compact.into_assignment(&voter_at, &target_at).unwrap_err(), - PhragmenError::CompactStakeOverflow, + solution.into_assignment(&voter_at, &target_at).unwrap_err(), + NposError::SolutionWeightOverflow, ); // in votes3 onwards - let compact = TestSolutionCompact { + let solution = TestSolution { votes1: Default::default(), votes2: Default::default(), - votes3: vec![( - 0, - [(1, TestAccuracy::from_percent(70)), (2, TestAccuracy::from_percent(80))], - 3, - )], + votes3: vec![(0, [(1, p(70)), (2, p(80))], 3)], ..Default::default() }; assert_eq!( - compact.into_assignment(&voter_at, &target_at).unwrap_err(), - PhragmenError::CompactStakeOverflow, + solution.into_assignment(&voter_at, &target_at).unwrap_err(), + NposError::SolutionWeightOverflow, ); } #[test] fn target_count_overflow_is_detected() { let voter_index = |a: &AccountId| -> Option { Some(*a as u32) }; - let target_index = |a: &AccountId| -> Option { Some(*a as u8) }; + let target_index = |a: &AccountId| -> Option { Some(*a as u16) }; let assignments = vec![Assignment { who: 1 as AccountId, - distribution: (10..27) - .map(|i| (i as AccountId, Percent::from_parts(i as u8))) - .collect::>(), + distribution: (10..27).map(|i| (i as AccountId, p(i as u8))).collect::>(), }]; - let compacted = - TestSolutionCompact::from_assignment(&assignments, voter_index, target_index); - assert_eq!(compacted.unwrap_err(), PhragmenError::CompactTargetOverflow); + let solution = TestSolution::from_assignment(&assignments, voter_index, target_index); + assert_eq!(solution.unwrap_err(), NposError::SolutionTargetOverflow); } #[test] @@ -1258,31 +1195,25 @@ mod solution_type { let targets = vec![10 as AccountId, 11]; let assignments = vec![ - Assignment { - who: 1 as AccountId, - distribution: vec![ - (10, Percent::from_percent(50)), - (11, Percent::from_percent(50)), - ], - }, + Assignment { who: 1 as AccountId, distribution: vec![(10, p(50)), (11, p(50))] }, Assignment { who: 2, distribution: vec![] }, ]; let voter_index = |a: &AccountId| -> Option { voters.iter().position(|x| x == a).map(TryInto::try_into).unwrap().ok() }; - let target_index = |a: &AccountId| -> Option { + let target_index = |a: &AccountId| -> Option { targets.iter().position(|x| x == a).map(TryInto::try_into).unwrap().ok() }; - let compacted = - TestSolutionCompact::from_assignment(&assignments, voter_index, target_index).unwrap(); + let solution = + TestSolution::from_assignment(&assignments, voter_index, target_index).unwrap(); assert_eq!( - compacted, - TestSolutionCompact { + solution, + TestSolution { votes1: Default::default(), - votes2: vec![(0, (0, Percent::from_percent(50)), 1)], + votes2: vec![(0, [(0, p(50))], 1)], ..Default::default() } ); @@ -1290,14 +1221,15 @@ mod solution_type { } #[test] -fn index_assignments_generate_same_compact_as_plain_assignments() { +fn index_assignments_generate_same_solution_as_plain_assignments() { let rng = rand::rngs::SmallRng::seed_from_u64(0); let (voters, assignments, candidates) = generate_random_votes(1000, 2500, rng); let voter_index = make_voter_fn(&voters); let target_index = make_target_fn(&candidates); - let compact = Compact::from_assignment(&assignments, &voter_index, &target_index).unwrap(); + let solution = + TestSolution::from_assignment(&assignments, &voter_index, &target_index).unwrap(); let index_assignments = assignments .into_iter() @@ -1307,5 +1239,5 @@ fn index_assignments_generate_same_compact_as_plain_assignments() { let index_compact = index_assignments.as_slice().try_into().unwrap(); - assert_eq!(compact, index_compact); + assert_eq!(solution, index_compact); } diff --git a/substrate/primitives/npos-elections/src/traits.rs b/substrate/primitives/npos-elections/src/traits.rs new file mode 100644 index 0000000000..ac07768016 --- /dev/null +++ b/substrate/primitives/npos-elections/src/traits.rs @@ -0,0 +1,155 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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. + +//! Traits for the npos-election operations. + +use crate::{ + Assignment, ElectionScore, Error, EvaluateSupport, ExtendedBalance, IndexAssignmentOf, + VoteWeight, +}; +use codec::Encode; +use sp_arithmetic::{ + traits::{Bounded, UniqueSaturatedInto}, + PerThing, +}; +use sp_std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Mul, + prelude::*, +}; + +/// an aggregator trait for a generic type of a voter/target identifier. This usually maps to +/// substrate's account id. +pub trait IdentifierT: Clone + Eq + Default + Ord + Debug + codec::Codec {} +impl IdentifierT for T {} + +/// Aggregator trait for a PerThing that can be multiplied by u128 (ExtendedBalance). +pub trait PerThing128: PerThing + Mul {} +impl> PerThing128 for T {} + +/// Simple Extension trait to easily convert `None` from index closures to `Err`. +/// +/// This is only generated and re-exported for the solution code to use. +#[doc(hidden)] +pub trait __OrInvalidIndex { + fn or_invalid_index(self) -> Result; +} + +impl __OrInvalidIndex for Option { + fn or_invalid_index(self) -> Result { + self.ok_or(Error::SolutionInvalidIndex) + } +} + +/// An opaque index-based, NPoS solution type. +pub trait NposSolution +where + Self: Sized + for<'a> sp_std::convert::TryFrom<&'a [IndexAssignmentOf], Error = Error>, +{ + /// The maximum number of votes that are allowed. + const LIMIT: usize; + + /// The voter type. Needs to be an index (convert to usize). + type VoterIndex: UniqueSaturatedInto + + TryInto + + TryFrom + + Debug + + Copy + + Clone + + Bounded + + Encode; + + /// The target type. Needs to be an index (convert to usize). + type TargetIndex: UniqueSaturatedInto + + TryInto + + TryFrom + + Debug + + Copy + + Clone + + Bounded + + Encode; + + /// The weight/accuracy type of each vote. + type Accuracy: PerThing128; + + /// Get the length of all the voters that this type is encoding. + /// + /// This is basically the same as the number of assignments, or number of active voters. + fn voter_count(&self) -> usize; + + /// Get the total count of edges. + /// + /// This is effectively in the range of {[`Self::voter_count`], [`Self::voter_count`] * + /// [`Self::LIMIT`]}. + fn edge_count(&self) -> usize; + + /// Get the number of unique targets in the whole struct. + /// + /// Once presented with a list of winners, this set and the set of winners must be + /// equal. + fn unique_targets(&self) -> Vec; + + /// Get the average edge count. + fn average_edge_count(&self) -> usize { + self.edge_count().checked_div(self.voter_count()).unwrap_or(0) + } + + /// Compute the score of this solution type. + fn score( + self, + winners: &[A], + stake_of: FS, + voter_at: impl Fn(Self::VoterIndex) -> Option, + target_at: impl Fn(Self::TargetIndex) -> Option, + ) -> Result + where + for<'r> FS: Fn(&'r A) -> VoteWeight, + A: IdentifierT, + { + let ratio = self.into_assignment(voter_at, target_at)?; + let staked = crate::helpers::assignment_ratio_to_staked_normalized(ratio, stake_of)?; + let supports = crate::to_supports(winners, &staked)?; + Ok(supports.evaluate()) + } + + /// Remove a certain voter. + /// + /// This will only search until the first instance of `to_remove`, and return true. If + /// no instance is found (no-op), then it returns false. + /// + /// In other words, if this return true, exactly **one** element must have been removed self. + fn remove_voter(&mut self, to_remove: Self::VoterIndex) -> bool; + + /// Build self from a list of assignments. + fn from_assignment( + assignments: &[Assignment], + voter_index: FV, + target_index: FT, + ) -> Result + where + A: IdentifierT, + for<'r> FV: Fn(&'r A) -> Option, + for<'r> FT: Fn(&'r A) -> Option; + + /// Convert self into a `Vec>` + fn into_assignment( + self, + voter_at: impl Fn(Self::VoterIndex) -> Option, + target_at: impl Fn(Self::TargetIndex) -> Option, + ) -> Result>, Error>; +}