-
Notifications
You must be signed in to change notification settings - Fork 835
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 1 commit
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 | ||
---|---|---|---|---|
|
@@ -763,43 +763,106 @@ impl FragmentTree { | |||
/// 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 we could find. | ||||
// Does a depth-first search, since we're optimistic that there won't be more than one such | ||||
// chains. Assumes that the chain must be acyclic. | ||||
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. What happens if they are cyclic? What is the worst case complexity here, assuming parachains misbehave as much as they possibly can? 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. that's a good question. we stop the search on the respective branch when we see that node A has a child that we've already visited. maybe we should just return an error. I don't think it's a valid state transition to accept. Is it? Even if we were to accept cycles, we'd still be bounded by a function of the requested number of candidates. 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. I modified the code in the latest revision to accept cycles in the fragment trees. This is actually a documented behaviour of prospective-parachains even with some tests for it. See:
I also documented the performance considerations. Cycles are not an issue because we are limited by the core count anyway so we won't loop indefinitely |
||||
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 { | ||||
// We could use a HashSet/BTreeSet for tracking the visited nodes but realistically, | ||||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
// accumulator will not hold more than 2-3 elements (the max number of cores for a | ||||
// parachain). | ||||
if accumulator.contains(&child_hash) { | ||||
// We've already visited this node. There's a cycle in the tree, ignore it. | ||||
continue | ||||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
let mut accumulator_copy = accumulator.clone(); | ||||
|
||||
accumulator_copy.push(child_hash); | ||||
|
||||
let result = self.select_children_inner( | ||||
child_ptr, | ||||
expected_count, | ||||
remaining_count - 1, | ||||
&pred, | ||||
&mut accumulator_copy, | ||||
); | ||||
|
||||
// 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>) { | ||||
|
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)