Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
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 Mar 10, 2021
79c339f
cause tests to compile
coriolinus Mar 12, 2021
d80a18c
wip: add test for trim_compact_for_length
coriolinus Mar 12, 2021
212de4c
finish fn trim_compact_for_length_does_not_modify_when_short_enough
coriolinus Mar 15, 2021
53eef6b
add fn trim_compact_for_length_modifies_when_too_long
coriolinus Mar 15, 2021
78ddb64
it's impractical to test for MinerError::NoMoreVoters
coriolinus Mar 15, 2021
909309c
trim_compact_ naming consistency
coriolinus Mar 15, 2021
d05ce23
fix trim_compact_length docs copypasta error
coriolinus Mar 15, 2021
f0221df
short-circuit length trim when known short enough
coriolinus Mar 15, 2021
99fdfae
set a default MaxMinerLength value for the node
coriolinus Mar 15, 2021
b29efa5
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus Mar 19, 2021
d41b07f
simplify size calculation for reduction function
coriolinus Mar 22, 2021
1814015
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus Mar 22, 2021
12519d5
use more efficient short-circuit check
coriolinus Mar 23, 2021
c230d91
use a real value for node MinerMaxLength
coriolinus Mar 24, 2021
467348f
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus Mar 24, 2021
da74f0a
add test that trimming compact for length reduces the compact solutio…
coriolinus Mar 24, 2021
b39dac1
remove possibility of saturation in MinerMaxLength calculation
coriolinus Mar 26, 2021
c4fe6b4
Update frame/election-provider-multi-phase/src/unsigned.rs
kianenigma Mar 29, 2021
daf128e
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus Mar 29, 2021
a75e935
Merge branch 'prgn-multi-phase-trim-length' of github.com:paritytech/…
coriolinus Mar 30, 2021
506ec9d
Merge remote-tracking branch 'origin/master' into prgn-multi-phase-tr…
coriolinus Mar 30, 2021
89c32c3
Apply suggestions from code review
coriolinus Mar 30, 2021
5651457
use encoded_size instead of encode().len() in test
coriolinus Mar 30, 2021
5b52572
don't unnecessarily offchainify tests
coriolinus Mar 30, 2021
f86ae65
reorganize trim noop test
coriolinus Mar 30, 2021
2b68b83
typos
coriolinus Apr 1, 2021
4c9f8d7
add test that trimming by length trims lowest stake
coriolinus Apr 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,11 @@ parameter_types! {
.get(DispatchClass::Normal)
.max_extrinsic.expect("Normal extrinsics have a weight limit configured; qed")
.saturating_sub(BlockExecutionWeight::get());
Copy link
Contributor

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

// Solution can occupy 90% of normal block size
pub MinerMaxLength: u32 = Perbill::from_rational(9u32, 10) *
*RuntimeBlockLength::get()
.max
.get(DispatchClass::Normal);
}

sp_npos_elections::generate_solution_type!(
Expand All @@ -534,6 +539,7 @@ impl pallet_election_provider_multi_phase::Config for Runtime {
type SolutionImprovementThreshold = SolutionImprovementThreshold;
type MinerMaxIterations = MinerMaxIterations;
type MinerMaxWeight = MinerMaxWeight;
type MinerMaxLength = MinerMaxLength;
type MinerTxPriority = MultiPhaseUnsignedPriority;
type DataProvider = Staking;
type OnChainAccuracy = Perbill;
Expand Down
9 changes: 8 additions & 1 deletion frame/election-provider-multi-phase/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,12 +534,19 @@ pub mod pallet {
/// Maximum number of iteration of balancing that will be executed in the embedded miner of
/// the pallet.
type MinerMaxIterations: Get<u32>;

/// Maximum weight that the miner should consume.
///
/// The miner will ensure that the total weight of the unsigned solution will not exceed
/// this values, based on [`WeightInfo::submit_unsigned`].
/// this value, based on [`WeightInfo::submit_unsigned`].
type MinerMaxWeight: Get<Weight>;

/// Maximum length (bytes) that the mined solution should consume.
///
/// The miner will ensure that the total length of the unsigned solution will not exceed
/// this value.
type MinerMaxLength: Get<u32>;

/// Something that will provide the election data.
type DataProvider: ElectionDataProvider<Self::AccountId, Self::BlockNumber>;

Expand Down
2 changes: 2 additions & 0 deletions frame/election-provider-multi-phase/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ parameter_types! {
pub static MinerTxPriority: u64 = 100;
pub static SolutionImprovementThreshold: Perbill = Perbill::zero();
pub static MinerMaxWeight: Weight = BlockWeights::get().max_block;
pub static MinerMaxLength: u32 = 256;
pub static MockWeightInfo: bool = false;


Expand Down Expand Up @@ -276,6 +277,7 @@ impl crate::Config for Runtime {
type SolutionImprovementThreshold = SolutionImprovementThreshold;
type MinerMaxIterations = MinerMaxIterations;
type MinerMaxWeight = MinerMaxWeight;
type MinerMaxLength = MinerMaxLength;
type MinerTxPriority = MinerTxPriority;
type DataProvider = StakingMock;
type WeightInfo = DualMockWeightInfo;
Expand Down
170 changes: 167 additions & 3 deletions frame/election-provider-multi-phase/src/unsigned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
///
/// 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.
///
/// 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.
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);
voters_sorted.reverse();
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


while compact.encoded_size() > max_allowed_length.saturated_into() {
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`.
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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();

// 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);
});
}
}