Multi-Block Election part 0: preparation and some cleanup. (#9442)

* Partially applied

* Everything builds, need to implement compact encoding as well.

* Fix some tests, add a ui test as well.

* Fix everything and everything.

* small nits

* a bunch more rename

* more reorg

* more reorg

* last nit of self-review

* Seemingly fixed the build now

* Fix build

* make it work again

* Update primitives/npos-elections/solution-type/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Update primitives/npos-elections/solution-type/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* nits

* factor out double type

* fix try-build

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
This commit is contained in:
Kian Paimani
2021-08-11 17:45:53 +02:00
committed by GitHub
parent abd08e29ce
commit f7bcbdd261
36 changed files with 1327 additions and 1364 deletions
@@ -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<T> = (
/// The relative distribution of a voter's stake among the winning targets.
pub type Assignment<T> =
sp_npos_elections::Assignment<<T as frame_system::Config>::AccountId, CompactAccuracyOf<T>>;
sp_npos_elections::Assignment<<T as frame_system::Config>::AccountId, SolutionAccuracyOf<T>>;
/// The [`IndexAssignment`][sp_npos_elections::IndexAssignment] type specialized for a particular
/// runtime `T`.
pub type IndexAssignmentOf<T> = sp_npos_elections::IndexAssignmentOf<CompactOf<T>>;
pub type IndexAssignmentOf<T> = sp_npos_elections::IndexAssignmentOf<SolutionOf<T>>;
#[derive(Debug, Eq, PartialEq)]
pub enum MinerError {
@@ -231,7 +232,7 @@ impl<T: Config> Pallet<T> {
//
// Performance: note that it internally clones the provided solution.
pub fn basic_checks(
raw_solution: &RawSolution<CompactOf<T>>,
raw_solution: &RawSolution<SolutionOf<T>>,
solution_type: &str,
) -> Result<(), MinerError> {
Self::unsigned_pre_dispatch_checks(raw_solution).map_err(|err| {
@@ -257,7 +258,7 @@ impl<T: Config> Pallet<T> {
/// [`Pallet::mine_check_save_submit`].
pub fn mine_and_check(
iters: usize,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
) -> Result<(RawSolution<SolutionOf<T>>, 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<T: Config> Pallet<T> {
/// Mine a new npos solution.
pub fn mine_solution(
iters: usize,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
) -> Result<(RawSolution<SolutionOf<T>>, 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<T>>(
seq_phragmen::<_, SolutionAccuracyOf<T>>(
desired_targets as usize,
targets,
voters,
@@ -286,8 +287,8 @@ impl<T: Config> Pallet<T> {
///
/// Will always reduce the solution as well.
pub fn prepare_election_result(
election_result: ElectionResult<T::AccountId, CompactAccuracyOf<T>>,
) -> Result<(RawSolution<CompactOf<T>>, SolutionOrSnapshotSize), MinerError> {
election_result: ElectionResult<T::AccountId, SolutionAccuracyOf<T>>,
) -> Result<(RawSolution<SolutionOf<T>>, 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<T: Config> Pallet<T> {
let target_at = helpers::target_at_fn::<T>(&targets);
let stake_of = helpers::stake_of_fn::<T>(&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<T>]| {
CompactOf::<T>::try_from(assignments).map(|compact| compact.encoded_size())
SolutionOf::<T>::try_from(assignments).map(|s| s.encoded_size())
};
let ElectionResult { assignments, winners } = election_result;
@@ -345,7 +346,7 @@ impl<T: Config> Pallet<T> {
};
// 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::<T>::new(&assignment, &voter_index, &target_index))
@@ -366,15 +367,15 @@ impl<T: Config> Pallet<T> {
&encoded_size_of,
)?;
// now make compact.
let compact = CompactOf::<T>::try_from(&index_assignments)?;
// now make solution.
let solution = SolutionOf::<T>::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<T: Config> Pallet<T> {
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<W: WeightInfo>(
@@ -623,24 +624,26 @@ impl<T: Config> Pallet<T> {
///
/// 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<CompactOf<T>>) -> DispatchResult {
pub fn unsigned_pre_dispatch_checks(
raw_solution: &RawSolution<SolutionOf<T>>,
) -> DispatchResult {
// ensure solution is timely. Don't panic yet. This is a cheap check.
ensure!(Self::current_phase().is_unsigned_open(), Error::<T>::PreDispatchEarlySubmission);
// ensure round is current
ensure!(Self::round() == solution.round, Error::<T>::OcwCallWrongEra);
ensure!(Self::round() == raw_solution.round, Error::<T>::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::<T>::PreDispatchWrongWinnerCount,
);
// ensure score is being improved. Panic henceforth.
ensure!(
Self::queued_solution().map_or(true, |q: ReadySolution<_>| is_score_better::<Perbill>(
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::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let solution =
RawSolution::<TestNposSolution> { 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::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let solution =
RawSolution::<TestNposSolution> { 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::<TestCompact> { 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::<TestNposSolution> { 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::<TestCompact> { score: [5, 0, 0], ..Default::default() };
RawSolution::<TestNposSolution> { 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::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let solution =
RawSolution::<TestNposSolution> { 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::<TestCompact> { score: [5, 0, 0], ..Default::default() };
let solution =
RawSolution::<TestNposSolution> { 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 = <Runtime as Config>::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
<MinerMaxWeight>::set(25);
let (solution, witness) = MultiPhase::mine_solution(2).unwrap();
let (raw, witness) = MultiPhase::mine_solution(2).unwrap();
let solution_weight = <Runtime as Config>::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::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = compact.encoded_size() as u32;
let compact_clone = compact.clone();
let solution = SolutionOf::<Runtime>::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::<Runtime>::try_from(assignments.as_slice()).unwrap();
assert_eq!(compact, compact_clone);
let solution = SolutionOf::<Runtime>::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::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = compact.encoded_size();
let compact_clone = compact.clone();
let solution = SolutionOf::<Runtime>::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::<Runtime>::try_from(assignments.as_slice()).unwrap();
assert_ne!(compact, compact_clone);
assert!(compact.encoded_size() < encoded_len);
let solution = SolutionOf::<Runtime>::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::<Runtime>::try_from(assignments.as_slice()).unwrap();
let encoded_len = compact.encoded_size() as u32;
let solution = SolutionOf::<Runtime>::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<Runtime>]| {
CompactOf::<Runtime>::try_from(assignments).map(|compact| compact.encoded_size())
SolutionOf::<Runtime>::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::<Runtime>::LIMIT);
let min_solution_size = encoded_size_of(&assignments).unwrap();
assert_eq!(min_solution_size, SolutionOf::<Runtime>::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::<Runtime>::LIMIT as u32;
// trim to min solution size.
let min_solution_size = SolutionOf::<Runtime>::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 = <Runtime as Config>::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
<Runtime as Config>::MinerMaxLength::set(solution_size as u32 - 1);
let solution = MultiPhase::mine_solution(0).unwrap();
let max_length = <Runtime as Config>::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);
});
}