-
Notifications
You must be signed in to change notification settings - Fork 690
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
prospective-parachains: respond with multiple backable candidates #3160
Changes from all commits
9bfdc59
a1f208d
d4f38bd
baf0fd0
cfada9a
b5c1584
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -756,53 +756,127 @@ impl FragmentTree { | |
depths.iter_ones().collect() | ||
} | ||
|
||
/// Select a candidate after the given `required_path` which passes | ||
/// the predicate. | ||
/// Select `count` candidates after the given `required_path` which pass | ||
/// the predicate and have not already been backed on chain. | ||
/// | ||
/// If there are multiple possibilities, this will select the first one. | ||
/// | ||
/// This returns `None` if there is no candidate meeting those criteria. | ||
/// Does an exhaustive search into the tree starting after `required_path`. | ||
/// If there are multiple possibilities of size `count`, this will select the first one. | ||
/// If there is no chain of size `count` that matches the criteria, this will return the largest | ||
/// chain it could find with the criteria. | ||
/// If there are no candidates meeting those criteria, returns an empty `Vec`. | ||
/// Cycles are accepted, see module docs for the `Cycles` section. | ||
/// | ||
/// The intention of the `required_path` is to allow queries on the basis of | ||
/// one or more candidates which were previously pending availability becoming | ||
/// available and opening up more room on the core. | ||
pub(crate) fn select_child( | ||
pub(crate) fn select_children( | ||
&self, | ||
required_path: &[CandidateHash], | ||
count: u32, | ||
pred: impl Fn(&CandidateHash) -> bool, | ||
) -> Option<CandidateHash> { | ||
) -> Vec<CandidateHash> { | ||
let base_node = { | ||
// traverse the required path. | ||
let mut node = NodePointer::Root; | ||
for required_step in required_path { | ||
node = self.node_candidate_child(node, &required_step)?; | ||
if let Some(next_node) = self.node_candidate_child(node, &required_step) { | ||
node = next_node; | ||
} else { | ||
return vec![] | ||
}; | ||
} | ||
|
||
node | ||
}; | ||
|
||
// TODO [now]: taking the first selection might introduce bias | ||
// TODO: taking the first best selection might introduce bias | ||
// or become gameable. | ||
// | ||
// For plausibly unique parachains, this shouldn't matter much. | ||
// figure out alternative selection criteria? | ||
match base_node { | ||
self.select_children_inner(base_node, count, count, &pred, &mut vec![]) | ||
} | ||
|
||
// Try finding a candidate chain starting from `base_node` of length `expected_count`. | ||
// If not possible, return the longest one we could find. | ||
// Does a depth-first search, since we're optimistic that there won't be more than one such | ||
// chains (parachains shouldn't usually have forks). So in the usual case, this will conclude | ||
// in `O(expected_count)`. | ||
// Cycles are accepted, but this doesn't allow for infinite execution time, because the maximum | ||
// depth we'll reach is `expected_count`. | ||
// | ||
// Worst case performance is `O(num_forks ^ expected_count)`. | ||
// Although an exponential function, this is actually a constant that can only be altered via | ||
// sudo/governance, because: | ||
// 1. `num_forks` at a given level is at most `max_candidate_depth * max_validators_per_core` | ||
// (because each validator in the assigned group can second `max_candidate_depth` | ||
// candidates). The prospective-parachains subsystem assumes that the number of para forks is | ||
// limited by collator-protocol and backing subsystems. In practice, this is a constant which | ||
// can only be altered by sudo or governance. | ||
// 2. `expected_count` is equal to the number of cores a para is scheduled on (in an elastic | ||
// scaling scenario). For non-elastic-scaling, this is just 1. In practice, this should be a | ||
// small number (1-3), capped by the total number of available cores (a constant alterable | ||
// only via governance/sudo). | ||
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. So with realistic numbers (current and expected/exaggerated future numbers): How bad does it get? What would be limits to those parameters? 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. Elastic scaling will eventually enable chains to be bounded by the rate of a single node's block production. In Cumulus currently, block authorship is typically slower than block verification (in validators) due to the need to read from disk. Most of the time is spent in accessing the trie for storage, not in code execution. Access patterns for the trie are also very inefficient. So it's not feasible to use more than 3 cores until that bottleneck is cleared. If this bottleneck is alleviated, then I could see parachains using many more cores. 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. thinking about this a bit more, I don't think this traversal is any different in terms of worst-case complexity from the routine used for populating the fragment tree (which populates the tree breadth-first). Is it? The complexity is linear in the number of nodes, but the number of nodes is an exponential function (because it's an n-ary tree, at least in theory, of arity If we'll hit a bottleneck, this wouldn't be the first place where that'd surface. I think we could limit the number of total forks allowed for a parachain (regardless of their level in the tree). So that the total number of branches in the tree would bring down the worst-case complexity massively. WDYT? 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. anyway, I think this is somewhat orthogonal to this PR.
we should revisit this assumption maybe, but it doesn't seem like it'll be a problem for the forseeable future 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.
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. opened a separate issue to track this security item and added it to the elastic scaling task: #3219 |
||
fn select_children_inner( | ||
&self, | ||
base_node: NodePointer, | ||
expected_count: u32, | ||
remaining_count: u32, | ||
pred: &dyn Fn(&CandidateHash) -> bool, | ||
accumulator: &mut Vec<CandidateHash>, | ||
) -> Vec<CandidateHash> { | ||
if remaining_count == 0 { | ||
// The best option is the chain we've accumulated so far. | ||
return accumulator.to_vec(); | ||
} | ||
|
||
let children: Vec<_> = match base_node { | ||
NodePointer::Root => self | ||
.nodes | ||
.iter() | ||
.take_while(|n| n.parent == NodePointer::Root) | ||
.filter(|n| self.scope.get_pending_availability(&n.candidate_hash).is_none()) | ||
.filter(|n| pred(&n.candidate_hash)) | ||
.map(|n| n.candidate_hash) | ||
.next(), | ||
NodePointer::Storage(ptr) => self.nodes[ptr] | ||
.children | ||
.iter() | ||
.filter(|n| self.scope.get_pending_availability(&n.1).is_none()) | ||
.filter(|n| pred(&n.1)) | ||
.map(|n| n.1) | ||
.next(), | ||
.enumerate() | ||
.take_while(|(_, n)| n.parent == NodePointer::Root) | ||
.filter(|(_, n)| self.scope.get_pending_availability(&n.candidate_hash).is_none()) | ||
.filter(|(_, n)| pred(&n.candidate_hash)) | ||
.map(|(ptr, n)| (NodePointer::Storage(ptr), n.candidate_hash)) | ||
.collect(), | ||
NodePointer::Storage(base_node_ptr) => { | ||
let base_node = &self.nodes[base_node_ptr]; | ||
|
||
base_node | ||
.children | ||
.iter() | ||
.filter(|(_, hash)| self.scope.get_pending_availability(&hash).is_none()) | ||
.filter(|(_, hash)| pred(&hash)) | ||
.map(|(ptr, hash)| (*ptr, *hash)) | ||
.collect() | ||
}, | ||
}; | ||
|
||
let mut best_result = accumulator.clone(); | ||
for (child_ptr, child_hash) in children { | ||
accumulator.push(child_hash); | ||
|
||
let result = self.select_children_inner( | ||
child_ptr, | ||
expected_count, | ||
remaining_count - 1, | ||
&pred, | ||
accumulator, | ||
); | ||
|
||
accumulator.pop(); | ||
|
||
// Short-circuit the search if we've found the right length. Otherwise, we'll | ||
// search for a max. | ||
if result.len() == expected_count as usize { | ||
return result | ||
} else if best_result.len() < result.len() { | ||
best_result = result; | ||
} | ||
} | ||
|
||
best_result | ||
} | ||
|
||
fn populate_from_bases(&mut self, storage: &CandidateStorage, initial_bases: Vec<NodePointer>) { | ||
|
@@ -987,6 +1061,7 @@ mod tests { | |
use polkadot_node_subsystem_util::inclusion_emulator::InboundHrmpLimitations; | ||
use polkadot_primitives::{BlockNumber, CandidateCommitments, CandidateDescriptor, HeadData}; | ||
use polkadot_primitives_test_helpers as test_helpers; | ||
use std::iter; | ||
|
||
fn make_constraints( | ||
min_relay_parent_number: BlockNumber, | ||
|
@@ -1524,6 +1599,21 @@ mod tests { | |
assert_eq!(tree.nodes[2].candidate_hash, candidate_a_hash); | ||
assert_eq!(tree.nodes[3].candidate_hash, candidate_a_hash); | ||
assert_eq!(tree.nodes[4].candidate_hash, candidate_a_hash); | ||
|
||
for count in 1..10 { | ||
assert_eq!( | ||
tree.select_children(&[], count, |_| true), | ||
iter::repeat(candidate_a_hash) | ||
.take(std::cmp::min(count as usize, max_depth + 1)) | ||
.collect::<Vec<_>>() | ||
); | ||
assert_eq!( | ||
tree.select_children(&[candidate_a_hash], count - 1, |_| true), | ||
iter::repeat(candidate_a_hash) | ||
.take(std::cmp::min(count as usize - 1, max_depth)) | ||
.collect::<Vec<_>>() | ||
); | ||
} | ||
} | ||
|
||
#[test] | ||
|
@@ -1591,6 +1681,35 @@ mod tests { | |
assert_eq!(tree.nodes[2].candidate_hash, candidate_a_hash); | ||
assert_eq!(tree.nodes[3].candidate_hash, candidate_b_hash); | ||
assert_eq!(tree.nodes[4].candidate_hash, candidate_a_hash); | ||
|
||
assert_eq!(tree.select_children(&[], 1, |_| true), vec![candidate_a_hash],); | ||
assert_eq!( | ||
tree.select_children(&[], 2, |_| true), | ||
vec![candidate_a_hash, candidate_b_hash], | ||
); | ||
assert_eq!( | ||
tree.select_children(&[], 3, |_| true), | ||
vec![candidate_a_hash, candidate_b_hash, candidate_a_hash], | ||
); | ||
assert_eq!( | ||
tree.select_children(&[candidate_a_hash], 2, |_| true), | ||
vec![candidate_b_hash, candidate_a_hash], | ||
); | ||
|
||
assert_eq!( | ||
tree.select_children(&[], 6, |_| true), | ||
vec![ | ||
candidate_a_hash, | ||
candidate_b_hash, | ||
candidate_a_hash, | ||
candidate_b_hash, | ||
candidate_a_hash | ||
], | ||
); | ||
assert_eq!( | ||
tree.select_children(&[candidate_a_hash, candidate_b_hash], 6, |_| true), | ||
vec![candidate_a_hash, candidate_b_hash, candidate_a_hash,], | ||
); | ||
} | ||
|
||
#[test] | ||
|
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.
Is this to be solved in this PR ?
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.
no, this was here beforehand
It could be solved by randomising the order in which we visit a node's children, but that would make tests harder to write.
Since validators don't favour particular collators when requesting collations, the order of potential forks in the fragment tree should already be somewhat "random" based on network latency (unless collators find a way to censor/DOS other collators)