-
Notifications
You must be signed in to change notification settings - Fork 768
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
refactor and harden check_core_index #6217
Changes from 10 commits
6d182a8
dd98fb8
7bc2fed
a9a2960
4cbd46d
e809852
cac050b
36a7222
5587ff5
bbeef78
862df21
86e28cd
ba93f80
b7d3467
533bb6e
027a206
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 |
---|---|---|
|
@@ -39,6 +39,10 @@ use sp_staking::SessionIndex; | |
/// Async backing primitives | ||
pub mod async_backing; | ||
|
||
/// The default claim queue offset to be used if it's not configured/accessible in the parachain | ||
/// runtime | ||
pub const DEFAULT_CLAIM_QUEUE_OFFSET: u8 = 0; | ||
|
||
/// A type representing the version of the candidate descriptor and internal version number. | ||
#[derive(PartialEq, Eq, Encode, Decode, Clone, TypeInfo, RuntimeDebug, Copy)] | ||
#[cfg_attr(feature = "std", derive(Hash))] | ||
|
@@ -424,49 +428,46 @@ pub enum UMPSignal { | |
/// Separator between `XCM` and `UMPSignal`. | ||
pub const UMP_SEPARATOR: Vec<u8> = vec![]; | ||
|
||
impl CandidateCommitments { | ||
/// Returns the core selector and claim queue offset the candidate has committed to, if any. | ||
pub fn selected_core(&self) -> Option<(CoreSelector, ClaimQueueOffset)> { | ||
// We need at least 2 messages for the separator and core selector | ||
if self.upward_messages.len() < 2 { | ||
return None | ||
} | ||
|
||
let separator_pos = | ||
self.upward_messages.iter().rposition(|message| message == &UMP_SEPARATOR)?; | ||
|
||
// Use first commitment | ||
let message = self.upward_messages.get(separator_pos + 1)?; | ||
/// Utility function for skipping the ump signals. | ||
pub fn skip_ump_signals<'a>( | ||
upward_messages: impl Iterator<Item = &'a Vec<u8>>, | ||
) -> impl Iterator<Item = &'a Vec<u8>> { | ||
upward_messages.take_while(|message| *message != &UMP_SEPARATOR) | ||
} | ||
|
||
match UMPSignal::decode(&mut message.as_slice()).ok()? { | ||
UMPSignal::SelectCore(core_selector, cq_offset) => Some((core_selector, cq_offset)), | ||
} | ||
} | ||
impl CandidateCommitments { | ||
/// Returns the core selector and claim queue offset determined by `UMPSignal::SelectCore` | ||
/// commitment, if present. | ||
pub fn core_selector( | ||
&self, | ||
) -> Result<Option<(CoreSelector, ClaimQueueOffset)>, CommittedCandidateReceiptError> { | ||
let mut signals_iter = | ||
self.upward_messages.iter().skip_while(|message| *message != &UMP_SEPARATOR); | ||
|
||
if signals_iter.next().is_some() { | ||
let core_selector_message = | ||
signals_iter.next().ok_or(CommittedCandidateReceiptError::NoCoreSelected)?; | ||
// We should have exactly one signal beyond the separator | ||
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. unrelated q: right now we have a strict check of at most one signal 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 discussed this with @sandreim a while ago. Yes, we'll probably use a new node feature 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. Yes, we'll need a feature anyway if we want to introduce a new signal. |
||
if signals_iter.next().is_some() { | ||
return Err(CommittedCandidateReceiptError::TooManyUMPSignals) | ||
} | ||
|
||
/// Returns the core index determined by `UMPSignal::SelectCore` commitment | ||
/// and `assigned_cores`. | ||
/// | ||
/// Returns `None` if there is no `UMPSignal::SelectCore` commitment or | ||
/// assigned cores is empty. | ||
/// | ||
/// `assigned_cores` must be a sorted vec of all core indices assigned to a parachain. | ||
pub fn committed_core_index(&self, assigned_cores: &[&CoreIndex]) -> Option<CoreIndex> { | ||
if assigned_cores.is_empty() { | ||
return None | ||
match UMPSignal::decode(&mut core_selector_message.as_slice()) | ||
.map_err(|_| CommittedCandidateReceiptError::InvalidSelectedCore)? | ||
{ | ||
UMPSignal::SelectCore(core_index_selector, cq_offset) => | ||
Ok(Some((core_index_selector, cq_offset))), | ||
} | ||
} else { | ||
Ok(None) | ||
} | ||
|
||
self.selected_core().and_then(|(core_selector, _cq_offset)| { | ||
let core_index = | ||
**assigned_cores.get(core_selector.0 as usize % assigned_cores.len())?; | ||
Some(core_index) | ||
}) | ||
} | ||
} | ||
|
||
/// CandidateReceipt construction errors. | ||
/// CommittedCandidateReceiptError construction errors. | ||
#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)] | ||
#[cfg_attr(feature = "std", derive(thiserror::Error))] | ||
pub enum CandidateReceiptError { | ||
pub enum CommittedCandidateReceiptError { | ||
/// The specified core index is invalid. | ||
#[cfg_attr(feature = "std", error("The specified core index is invalid"))] | ||
InvalidCoreIndex, | ||
|
@@ -479,6 +480,9 @@ pub enum CandidateReceiptError { | |
/// The core selector or claim queue offset is invalid. | ||
#[cfg_attr(feature = "std", error("The core selector or claim queue offset is invalid"))] | ||
InvalidSelectedCore, | ||
#[cfg_attr(feature = "std", error("Could not decode UMP signal"))] | ||
/// Could not decode UMP signal. | ||
UmpSignalDecode, | ||
/// The parachain is not assigned to any core at specified claim queue offset. | ||
#[cfg_attr( | ||
feature = "std", | ||
|
@@ -492,6 +496,10 @@ pub enum CandidateReceiptError { | |
/// Unknown version. | ||
#[cfg_attr(feature = "std", error("Unknown internal version"))] | ||
UnknownVersion(InternalVersion), | ||
/// The allowed number of `UMPSignal` messages in the queue was exceeded. | ||
/// Currenly only one such message is allowed. | ||
#[cfg_attr(feature = "std", error("Too many UMP signals"))] | ||
TooManyUMPSignals, | ||
} | ||
|
||
macro_rules! impl_getter { | ||
|
@@ -586,55 +594,70 @@ impl<H: Copy> CommittedCandidateReceiptV2<H> { | |
/// Checks if descriptor core index is equal to the committed core index. | ||
/// Input `cores_per_para` is a claim queue snapshot stored as a mapping | ||
/// between `ParaId` and the cores assigned per depth. | ||
/// `core_index_enabled` optionally describes the status of the elastic scaling MVP node | ||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// feature. | ||
pub fn check_core_index( | ||
&self, | ||
cores_per_para: &TransposedClaimQueue, | ||
) -> Result<(), CandidateReceiptError> { | ||
core_index_enabled: Option<bool>, | ||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) -> Result<(), CommittedCandidateReceiptError> { | ||
match self.descriptor.version() { | ||
// Don't check v1 descriptors. | ||
CandidateDescriptorVersion::V1 => return Ok(()), | ||
CandidateDescriptorVersion::V2 => {}, | ||
CandidateDescriptorVersion::Unknown => | ||
return Err(CandidateReceiptError::UnknownVersion(self.descriptor.version)), | ||
} | ||
|
||
if cores_per_para.is_empty() { | ||
return Err(CandidateReceiptError::NoAssignment) | ||
return Err(CommittedCandidateReceiptError::UnknownVersion(self.descriptor.version)), | ||
} | ||
|
||
let (offset, core_selected) = | ||
if let Some((_core_selector, cq_offset)) = self.commitments.selected_core() { | ||
(cq_offset.0, true) | ||
} else { | ||
// If no core has been selected then we use offset 0 (top of claim queue) | ||
(0, false) | ||
}; | ||
let (maybe_core_index_selector, cq_offset) = self.commitments.core_selector()?.map_or_else( | ||
|| (None, ClaimQueueOffset(DEFAULT_CLAIM_QUEUE_OFFSET)), | ||
|(sel, off)| (Some(sel), off), | ||
); | ||
|
||
// The cores assigned to the parachain at above computed offset. | ||
let assigned_cores = cores_per_para | ||
.get(&self.descriptor.para_id()) | ||
.ok_or(CandidateReceiptError::NoAssignment)? | ||
.get(&offset) | ||
.ok_or(CandidateReceiptError::NoAssignment)? | ||
.into_iter() | ||
.collect::<Vec<_>>(); | ||
|
||
let core_index = if core_selected { | ||
self.commitments | ||
.committed_core_index(assigned_cores.as_slice()) | ||
.ok_or(CandidateReceiptError::NoAssignment)? | ||
} else { | ||
// `SelectCore` commitment is mandatory for elastic scaling parachains. | ||
if assigned_cores.len() > 1 { | ||
return Err(CandidateReceiptError::NoCoreSelected) | ||
} | ||
.ok_or(CommittedCandidateReceiptError::NoAssignment)? | ||
.get(&cq_offset.0) | ||
.ok_or(CommittedCandidateReceiptError::NoAssignment)?; | ||
|
||
**assigned_cores.get(0).ok_or(CandidateReceiptError::NoAssignment)? | ||
}; | ||
if assigned_cores.is_empty() { | ||
return Err(CommittedCandidateReceiptError::NoAssignment) | ||
} | ||
|
||
let descriptor_core_index = CoreIndex(self.descriptor.core_index as u32); | ||
|
||
let core_index_selector = if let Some(core_index_selector) = maybe_core_index_selector { | ||
// We have a committed core selector, we can use it. | ||
core_index_selector | ||
} else if assigned_cores.len() > 1 { | ||
// We got more than one assigned core and no core selector. Special care is needed. | ||
|
||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
match core_index_enabled { | ||
// Elastic scaling MVP feature is not supplied, nothing more to check. | ||
None => return Ok(()), | ||
// Elastic scaling MVP feature is disabled. Error. | ||
Some(false) => return Err(CommittedCandidateReceiptError::NoCoreSelected), | ||
// Elastic scaling MVP feature is enabled but the core index in the descriptor is | ||
// not assigned to the para. Error. | ||
Some(true) if !assigned_cores.contains(&descriptor_core_index) => | ||
return Err(CommittedCandidateReceiptError::InvalidCoreIndex), | ||
// Elastic scaling MVP feature is enabled and the descriptor core index is indeed | ||
// assigned to the para. This is the most we can check for now. | ||
Some(true) => return Ok(()), | ||
} | ||
} else { | ||
// No core selector but there's only one assigned core, use it. | ||
CoreSelector(0) | ||
}; | ||
|
||
let core_index = assigned_cores | ||
.iter() | ||
.nth(core_index_selector.0 as usize % assigned_cores.len()) | ||
.ok_or(CommittedCandidateReceiptError::InvalidSelectedCore) | ||
.copied()?; | ||
|
||
if core_index != descriptor_core_index { | ||
return Err(CandidateReceiptError::CoreIndexMismatch) | ||
return Err(CommittedCandidateReceiptError::CoreIndexMismatch) | ||
} | ||
|
||
Ok(()) | ||
|
@@ -1031,7 +1054,7 @@ mod tests { | |
assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::Unknown); | ||
assert_eq!( | ||
new_ccr.check_core_index(&BTreeMap::new()), | ||
Err(CandidateReceiptError::UnknownVersion(InternalVersion(100))) | ||
Err(CommittedCandidateReceiptError::UnknownVersion(InternalVersion(100))) | ||
) | ||
} | ||
|
||
|
@@ -1097,7 +1120,7 @@ mod tests { | |
|
||
assert_eq!( | ||
new_ccr.check_core_index(&transpose_claim_queue(cq.clone())), | ||
Err(CandidateReceiptError::NoCoreSelected) | ||
Err(CommittedCandidateReceiptError::NoCoreSelected) | ||
); | ||
|
||
new_ccr.commitments.upward_messages.clear(); | ||
|
@@ -1225,7 +1248,7 @@ mod tests { | |
// Should fail because 2 cores are assigned, | ||
assert_eq!( | ||
new_ccr.check_core_index(&transpose_claim_queue(cq)), | ||
Err(CandidateReceiptError::NoCoreSelected) | ||
Err(CommittedCandidateReceiptError::NoCoreSelected) | ||
); | ||
|
||
// Adding collator signature should make it decode as v1. | ||
|
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.
Any good reason not to put it inside
CandidateCommitments
?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.
yes, in the runtime we use this function after taking the upward messages from the commitments struct