-
Notifications
You must be signed in to change notification settings - Fork 624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: consider all proposals for chunk validators #11252
Conversation
hey @Longarithm this PR is still in draft but I see you requested reviews, just to make sure, is it ready for review? |
core/primitives/src/epoch_manager.rs
Outdated
pub num_chunk_producer_seats: NumSeats, | ||
#[default(300)] | ||
pub num_chunk_validator_seats: NumSeats, | ||
// Deprecated, should be set to zero for future protocol versions. |
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.
Can we make this a TODO (instead of saying deprecated, say deprecate this and set to zero ...) and also indicate after which protocol versions this can change (in case someone considers doing it later)?
// is not enough to iterate over chunk validators. | ||
// So unfortunately we have to look over all roles to get unselected | ||
// proposals. | ||
// TODO: must be simplified. |
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.
can you expand this TODO to indicate simplify in which direction?
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #11252 +/- ##
==========================================
+ Coverage 71.01% 71.03% +0.02%
==========================================
Files 782 782
Lines 156476 156618 +142
Branches 156476 156618 +142
==========================================
+ Hits 111123 111259 +136
- Misses 40525 40530 +5
- Partials 4828 4829 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
// Returns unselected proposals, validator lists for all roles and stake | ||
// threshold to become a validator. | ||
let (unselected_proposals, chunk_producers, block_producers, chunk_validators, threshold) = | ||
if checked_feature!("stable", StatelessValidationV0, next_version) { |
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.
nit: Can you refactor the body of this if to a helper method? Perhaps something like new_validator_selection::select_validators_from_proposals
for symmetry.
let mut block_producer_proposals = order_proposals(proposals.values().cloned()); | ||
let (block_producers, bp_stake_threshold) = select_block_producers( | ||
&mut block_producer_proposals, | ||
epoch_config.num_block_producer_seats as usize, |
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.
I'm curious why chunk producers and chunk validators are configured in validator_selection_config
but block producers aren't. Some legacy stuff?
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.
Yeah, legacy. There should be a separate storage for EpochConfigs #11265
let max_validators_for_role = cmp::max( | ||
chunk_producers.len(), | ||
cmp::max(block_producers.len(), chunk_validators.len()), | ||
); |
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.
nit: maybe a bit shorter version but up to you
chunk_producers.len().max(block_producers.len()).max(chunk_validators.len())
let unselected_proposals = if chunk_producers.len() == max_validators_for_role { | ||
chunk_producer_proposals | ||
} else if block_producers.len() == max_validators_for_role { | ||
block_producer_proposals | ||
} else { | ||
chunk_validator_proposals | ||
}; |
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.
Can you explain what are the unselected proposals and why is it the ones with max validator role? In a typical configuration we're going to have the most chunk validators. Why do we make all chunk validator proposals "unselected"?
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.
This follows current implementation style. When one calls select_validators
, passed proposals ( e.g. chunk_validator_proposals
) are mutated: if proposal is selected, it is popped out of set, so the only ones remaining are unselected.
Then, I use the fact that all roles are selected by stake, so the unselected proposals will come from the role with maximal amount of validators. All other proposals will have at least one role assigned.
This is not ideal but I don't want to tweak this part of implementation as well for now.
@@ -341,11 +381,140 @@ impl Ord for OrderedValidatorStake { | |||
mod old_validator_selection { | |||
use super::*; | |||
|
|||
pub fn assign_chunk_producers_to_shards( | |||
pub(crate) fn select_validators_from_proposals( |
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.
nit: Can you add comments to the pub functions?
BinaryHeap<OrderedValidatorStake>, | ||
Vec<ValidatorStake>, | ||
Vec<ValidatorStake>, | ||
Vec<ValidatorStake>, | ||
Balance, |
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.
nit: Can you put it in a struct with nice names and comments?
} = if checked_feature!("stable", StatelessValidationV0, next_version) { | ||
select_validators_from_proposals(epoch_config, proposals, next_version) | ||
} else { | ||
old_validator_selection::select_validators_from_proposals( |
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.
very nit. maybe not say "old" yet but something like stateful_validator_selection, as this will still be effective until the launch? or V1 vs V2 as in the case of other evolving structs in the code?
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.
I would keep old_validator_selection
for now because this is existing naming for old versions and easy to fix in the future.
// TODO: getting unselected proposals must be simpler. For example, | ||
// we can track all roles assign to each validator in some structure | ||
// and then return all validators which don't have any role. | ||
let max_validators_for_role = |
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.
I do not get this part. What does unselected proposals represent? I first thought these are the validators that we are unable to find a role but you are now assigning the entire set of block or chunk producers/validators to that set. Do I misunderstand the semantics of unselected proposals?
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.
I stated the purpose in comment:
ValidatorRoles {
/// Proposals which were not given any role.
unselected_proposals: BinaryHeap<OrderedValidatorStake>,
So "validators that we are unable to find a role" is right.
you are not assigning the entire set of block or chunk producers/validators to that set.
I don't get the question either... The goal is to get validators without roles, not the opposite.
One way to do it is to take one of chunk_validator_proposals/...
corresponding to maximal number of roles set because all proposals are selected by stake. After select_validators/...
is called, only unselected proposals are left in this set. Selected proposals go to chunk_validators
.
Please note that this is how legacy selection logic currently works and I want to make changes iteratively to make it better with time.
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.
I agree with @tayfunelmas that this is very confusing to read. How about :
a) Rather than mutating the x_proposals
you pass it into the select_x
and have the unselected ones returned?
let ordered_proposals = order_proposals(proposals.values().cloned());
let (chunk_producers, not_chunk_producers, cp_stake_threshold) = select_chunk_producers(
ordered_proposals,
...
);
or
b) To find the unselected ones just iterate over all proposals and check which ones are not in bp, cp, val sets.
for proposal in ordered_proposals {
if chunk_producers.contains(proposal) continue;
if block_producers.contains(proposal) continue;
if chunk_validators.contains(proposal) continue;
unselected.insert(proposal);
}
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.
LGTM
I focused on the new stuff assuming that the old selection code is just moved around to a new method and that it's covered by enough tests. Please let me know if you'd like me to have a proper look at the old_*
methods.
/// Helper struct which is a result of proposals processing. | ||
struct ValidatorRoles { | ||
/// Proposals which were not given any role. | ||
unselected_proposals: BinaryHeap<OrderedValidatorStake>, | ||
/// Validators which are assigned to produce chunks. | ||
chunk_producers: Vec<ValidatorStake>, | ||
/// Validators which are assigned to produce blocks. | ||
block_producers: Vec<ValidatorStake>, | ||
/// Validators which are assigned to validate chunks. | ||
chunk_validators: Vec<ValidatorStake>, | ||
/// Stake threshold to become a validator. | ||
threshold: Balance, | ||
} | ||
|
||
/// Helper struct which is a result of assigning chunk producers to shards. | ||
struct ChunkProducersAssignment { | ||
/// List of all validators in the epoch. | ||
/// Note that it doesn't belong here, but in the legacy code it is computed | ||
/// together with chunk producers assignment. | ||
all_validators: Vec<ValidatorStake>, | ||
/// Maps validator account names to local indices throughout the epoch. | ||
validator_to_index: HashMap<AccountId, ValidatorId>, | ||
/// Maps chunk producers to shards, where i-th list contains local indices | ||
/// of validators producing chunks for i-th shard. | ||
chunk_producers_settlement: Vec<Vec<ValidatorId>>, | ||
} |
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.
That's fantastic, thank you!
// TODO: getting unselected proposals must be simpler. For example, | ||
// we can track all roles assign to each validator in some structure | ||
// and then return all validators which don't have any role. | ||
let max_validators_for_role = |
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.
I agree with @tayfunelmas that this is very confusing to read. How about :
a) Rather than mutating the x_proposals
you pass it into the select_x
and have the unselected ones returned?
let ordered_proposals = order_proposals(proposals.values().cloned());
let (chunk_producers, not_chunk_producers, cp_stake_threshold) = select_chunk_producers(
ordered_proposals,
...
);
or
b) To find the unselected ones just iterate over all proposals and check which ones are not in bp, cp, val sets.
for proposal in ordered_proposals {
if chunk_producers.contains(proposal) continue;
if block_producers.contains(proposal) continue;
if chunk_validators.contains(proposal) continue;
unselected.insert(proposal);
}
} | ||
|
||
/// Selects validator roles for the given proposals. | ||
fn select_validators_from_proposals( |
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.
This is really pretty now :)
Solve #11202 and continue simplifying proposals processing logic.
I want to make epoch info generation as straightforward as possible, by moving code for old protocol versions to local submodule so it won't be distracting. Now it should be more clear that epoch info generation consists of couple independent steps.
Couple notes: