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 6 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
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 @@ -519,12 +519,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 @@ -202,6 +202,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 @@ -267,6 +268,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
103 changes: 103 additions & 0 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 @@ -166,6 +168,11 @@ impl<T: Config> Pallet<T> {
maximum_allowed_voters,
);
let compact = Self::trim_compact(maximum_allowed_voters, compact, &voter_index)?;
let compact = Self::trim_compact_for_length(
T::MinerMaxLength::get(),
compact,
&voter_index,
)?;

// re-calc score.
let winners = sp_npos_elections::to_without_backing(winners);
Expand Down Expand Up @@ -252,6 +259,54 @@ 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, and the `ElectionSize` is a proxy for the total number of stakers.
///
/// 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.
///
/// 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_for_length(
max_allowed_length: u32,
mut compact: CompactOf<T>,
voter_index: impl Fn(&T::AccountId) -> Option<CompactVoterIndexOf<T>>,
) -> Result<CompactOf<T>, MinerError> {
// 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.


// we run through the reduction process twice: once using the size_hint function, and once
// by actually encoding the data. Given a well-behaved size_hint implementation, this will
// only actually encode once. Given a misbehaving size_hint implementation, this still ensures
// that we accurately trim the solution.
let size_fns: [Box<dyn Fn(&CompactOf<T>) -> u32>; 2] = [
Box::new(|compact: &CompactOf<T>| compact.size_hint().saturated_into()),
Box::new(|compact: &CompactOf<T>| {
let encoded = compact.encode();
encoded.len().saturated_into()
}),
];
for size_fn in size_fns.iter() {
while size_fn(&compact) > max_allowed_length {
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 @@ -486,6 +541,7 @@ mod tests {
Call, *,
};
use frame_support::{dispatch::Dispatchable, traits::OffchainWorker};
use helpers::voter_index_fn_linear;
use mock::Call as OuterCall;
use sp_election_providers::Assignment;
use sp_runtime::{traits::ValidateUnsigned, PerU16};
Expand Down Expand Up @@ -870,4 +926,51 @@ mod tests {
assert!(matches!(call, OuterCall::MultiPhase(Call::submit_unsigned(_, _))));
})
}

#[test]
fn trim_compact_for_length_does_not_modify_when_short_enough() {
let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to(25);

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();

compact = MultiPhase::trim_compact_for_length(
encoded_len,
compact,
voter_index_fn_linear::<Runtime>(&voters),
).unwrap();

assert_eq!(compact, compact_clone);
});
}

#[test]
fn trim_compact_for_length_modifies_when_too_long() {
let (mut ext, _) = ExtBuilder::default().build_offchainify(0);
ext.execute_with(|| {
roll_to(25);

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();

compact = MultiPhase::trim_compact_for_length(
encoded_len - 1,
compact,
voter_index_fn_linear::<Runtime>(&voters),
).unwrap();

assert_ne!(compact, compact_clone);
assert!((compact.encode().len() as u32) < encoded_len);
});
}
}