This repository was archived by the owner on Nov 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Trim compact solution for length during preparation #8317
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
4e1e09c
Trim compact solution for length during preparation
coriolinus 79c339f
cause tests to compile
coriolinus d80a18c
wip: add test for trim_compact_for_length
coriolinus 212de4c
finish fn trim_compact_for_length_does_not_modify_when_short_enough
coriolinus 53eef6b
add fn trim_compact_for_length_modifies_when_too_long
coriolinus 78ddb64
it's impractical to test for MinerError::NoMoreVoters
coriolinus 909309c
trim_compact_ naming consistency
coriolinus d05ce23
fix trim_compact_length docs copypasta error
coriolinus f0221df
short-circuit length trim when known short enough
coriolinus 99fdfae
set a default MaxMinerLength value for the node
coriolinus b29efa5
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus d41b07f
simplify size calculation for reduction function
coriolinus 1814015
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus 12519d5
use more efficient short-circuit check
coriolinus c230d91
use a real value for node MinerMaxLength
coriolinus 467348f
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus da74f0a
add test that trimming compact for length reduces the compact solutio…
coriolinus b39dac1
remove possibility of saturation in MinerMaxLength calculation
coriolinus c4fe6b4
Update frame/election-provider-multi-phase/src/unsigned.rs
kianenigma daf128e
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus a75e935
Merge branch 'prgn-multi-phase-trim-length' of github.com:paritytech/…
coriolinus 506ec9d
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus 89c32c3
Apply suggestions from code review
coriolinus 5651457
use encoded_size instead of encode().len() in test
coriolinus 5b52572
don't unnecessarily offchainify tests
coriolinus f86ae65
reorganize trim noop test
coriolinus 2b68b83
typos
coriolinus 4c9f8d7
add test that trimming by length trims lowest stake
coriolinus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,8 @@ pub enum MinerError { | |
| PreDispatchChecksFailed, | ||
| /// The solution generated from the miner is not feasible. | ||
| Feasibility(FeasibilityError), | ||
| /// There are no more voters to remove to trim the solution. | ||
| NoMoreVoters, | ||
| } | ||
|
|
||
| impl From<sp_npos_elections::Error> for MinerError { | ||
|
|
@@ -177,8 +179,13 @@ impl<T: Config> Pallet<T> { | |
| maximum_allowed_voters, | ||
| ); | ||
|
|
||
| // trim weight. | ||
| let compact = Self::trim_compact(maximum_allowed_voters, compact, &voter_index)?; | ||
| // trim length and weight | ||
| let compact = Self::trim_compact_weight(maximum_allowed_voters, compact, &voter_index)?; | ||
| let compact = Self::trim_compact_length( | ||
| T::MinerMaxLength::get(), | ||
| compact, | ||
| &voter_index, | ||
| )?; | ||
|
|
||
| // re-calc score. | ||
| let winners = sp_npos_elections::to_without_backing(winners); | ||
|
|
@@ -221,7 +228,7 @@ impl<T: Config> Pallet<T> { | |
| /// | ||
| /// Indeed, the score must be computed **after** this step. If this step reduces the score too | ||
| /// much or remove a winner, then the solution must be discarded **after** this step. | ||
| pub fn trim_compact<FN>( | ||
| pub fn trim_compact_weight<FN>( | ||
| maximum_allowed_voters: u32, | ||
| mut compact: CompactOf<T>, | ||
| voter_index: FN, | ||
|
|
@@ -267,6 +274,50 @@ impl<T: Config> Pallet<T> { | |
| } | ||
| } | ||
|
|
||
| /// Greedily reduce the size of the solution to fit into the block w.r.t length. | ||
| /// | ||
| /// The length of the solution is largely a function of the number of voters. The number of | ||
| /// winners cannot be changed. Thus, to reduce the solution size, we need to strip voters. | ||
kianenigma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// Note that this solution is already computed, and winners are elected based on the merit of | ||
| /// the total stake in the system. Nevertheless, some of the voters may be removed here. | ||
kianenigma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// Sometimes, removing a voter can cause a validator to also be implicitly removed, if | ||
| /// that voter was the only backer of that winner. In such cases, this solution is invalid, which | ||
| /// will be caught prior to submission. | ||
| /// | ||
| /// The score must be computed **after** this step. If this step reduces the score too much, | ||
| /// then the solution must be discarded. | ||
| pub fn trim_compact_length( | ||
| max_allowed_length: u32, | ||
| mut compact: CompactOf<T>, | ||
| voter_index: impl Fn(&T::AccountId) -> Option<CompactVoterIndexOf<T>>, | ||
| ) -> Result<CompactOf<T>, MinerError> { | ||
| // short-circuit to avoid getting the voters if possible | ||
| // this involves a redundant encoding, but that should hopefully be relatively cheap | ||
| if (compact.encoded_size().saturated_into::<u32>()) <= max_allowed_length { | ||
| return Ok(compact); | ||
| } | ||
|
|
||
| // grab all voters and sort them by least stake. | ||
coriolinus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let RoundSnapshot { voters, .. } = | ||
| Self::snapshot().ok_or(MinerError::SnapshotUnAvailable)?; | ||
| let mut voters_sorted = voters | ||
| .into_iter() | ||
| .map(|(who, stake, _)| (who.clone(), stake)) | ||
| .collect::<Vec<_>>(); | ||
| voters_sorted.sort_by_key(|(_, y)| *y); | ||
coriolinus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| voters_sorted.reverse(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While we are waiting for audit, mind adding a test to ensure that we always remove based on least stake as well? i.e. say if we mess up the sorting order here, then we'd remove highest stake.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, will do.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| while compact.encoded_size() > max_allowed_length.saturated_into() { | ||
kianenigma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let (smallest_stake_voter, _) = voters_sorted.pop().ok_or(MinerError::NoMoreVoters)?; | ||
| let index = voter_index(&smallest_stake_voter).ok_or(MinerError::SnapshotUnAvailable)?; | ||
| compact.remove_voter(index); | ||
| } | ||
|
|
||
| Ok(compact) | ||
| } | ||
|
|
||
| /// Find the maximum `len` that a compact can have in order to fit into the block weight. | ||
| /// | ||
| /// This only returns a value between zero and `size.nominators`. | ||
|
|
@@ -506,6 +557,7 @@ mod tests { | |
| Call, *, | ||
| }; | ||
| use frame_support::{dispatch::Dispatchable, traits::OffchainWorker}; | ||
| use helpers::voter_index_fn_linear; | ||
| use mock::Call as OuterCall; | ||
| use frame_election_provider_support::Assignment; | ||
| use sp_runtime::{traits::ValidateUnsigned, PerU16}; | ||
|
|
@@ -889,4 +941,116 @@ mod tests { | |
| assert!(matches!(call, OuterCall::MultiPhase(Call::submit_unsigned(_, _)))); | ||
| }) | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_compact_length_does_not_modify_when_short_enough() { | ||
| let mut ext = ExtBuilder::default().build(); | ||
| ext.execute_with(|| { | ||
| roll_to(25); | ||
|
|
||
| // given | ||
| let RoundSnapshot { voters, ..} = MultiPhase::snapshot().unwrap(); | ||
| let RawSolution { mut compact, .. } = raw_solution(); | ||
| let encoded_len = compact.encode().len() as u32; | ||
| let compact_clone = compact.clone(); | ||
coriolinus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // when | ||
| assert!(encoded_len < <Runtime as Config>::MinerMaxLength::get()); | ||
|
|
||
| // then | ||
| compact = MultiPhase::trim_compact_length( | ||
| encoded_len, | ||
| compact, | ||
| voter_index_fn_linear::<Runtime>(&voters), | ||
| ).unwrap(); | ||
| assert_eq!(compact, compact_clone); | ||
| }); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_compact_length_modifies_when_too_long() { | ||
| let mut ext = ExtBuilder::default().build(); | ||
| ext.execute_with(|| { | ||
| roll_to(25); | ||
|
|
||
| let RoundSnapshot { voters, ..} = | ||
| MultiPhase::snapshot().unwrap(); | ||
|
|
||
| let RawSolution { mut compact, .. } = raw_solution(); | ||
| let encoded_len = compact.encoded_size() as u32; | ||
| let compact_clone = compact.clone(); | ||
|
|
||
| compact = MultiPhase::trim_compact_length( | ||
| encoded_len - 1, | ||
| compact, | ||
| voter_index_fn_linear::<Runtime>(&voters), | ||
| ).unwrap(); | ||
|
|
||
| assert_ne!(compact, compact_clone); | ||
| assert!((compact.encoded_size() as u32) < encoded_len); | ||
| }); | ||
| } | ||
|
|
||
| #[test] | ||
| fn trim_compact_length_trims_lowest_stake() { | ||
| let mut ext = ExtBuilder::default().build(); | ||
| ext.execute_with(|| { | ||
| roll_to(25); | ||
|
|
||
| let RoundSnapshot { voters, ..} = | ||
| MultiPhase::snapshot().unwrap(); | ||
|
|
||
| let RawSolution { mut compact, .. } = raw_solution(); | ||
| let encoded_len = compact.encoded_size() as u32; | ||
| let voter_count = compact.voter_count(); | ||
| let min_stake_voter = voters.iter() | ||
| .map(|(id, weight, _)| (weight, id)) | ||
| .min() | ||
| .map(|(_, id)| id) | ||
| .unwrap(); | ||
|
|
||
|
|
||
| compact = MultiPhase::trim_compact_length( | ||
| encoded_len - 1, | ||
| compact, | ||
| voter_index_fn_linear::<Runtime>(&voters), | ||
| ).unwrap(); | ||
|
|
||
| assert_eq!(compact.voter_count(), voter_count - 1, "we must have removed exactly 1 voter"); | ||
|
|
||
| let assignments = compact.into_assignment( | ||
| |voter| Some(voter as AccountId), | ||
| |target| Some(target as AccountId), | ||
| ).unwrap(); | ||
| assert!( | ||
| assignments.iter() | ||
| .all(|Assignment{ who, ..}| who != min_stake_voter), | ||
| "min_stake_voter must no longer be in the set of voters", | ||
| ); | ||
| }); | ||
| } | ||
|
|
||
| // all the other solution-generation functions end up delegating to `mine_solution`, so if we | ||
| // demonstrate that `mine_solution` solutions are all trimmed to an acceptable length, then | ||
| // we know that higher-level functions will all also have short-enough solutions. | ||
| #[test] | ||
| fn mine_solution_solutions_always_within_acceptable_length() { | ||
| let mut ext = ExtBuilder::default().build(); | ||
| ext.execute_with(|| { | ||
| roll_to(25); | ||
|
|
||
| // 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(); | ||
| 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(); | ||
| assert!(solution_size <= max_length as usize); | ||
| }); | ||
| } | ||
kianenigma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sidenote: I think we should have to substract base block weight here: https://github.com/paritytech/substrate/issues/8434