-
Notifications
You must be signed in to change notification settings - Fork 646
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
Elastic scaling: runtime v2 descriptor support #5423
base: master
Are you sure you want to change the base?
Changes from 104 commits
793142e
3a29fdf
4a53577
8285ae7
2831c5e
c767d60
96999e3
c5f2dc3
dbb0160
5efab68
c2232e4
87b079f
00e8c13
cd4d02f
5509e33
f8b86d2
fe2fbfb
975e13b
1c7ac55
653873b
dc98149
0a6bce3
5e4dac2
f12ca7a
13734de
effb1cc
3f75cba
75a47bb
12ed853
f2c0882
768e034
4bf0706
0c83201
4296942
e1a7509
6fb7790
e6add9c
d0b3961
66f7a96
5c0c919
38ce589
9f1d611
a6a7329
663817d
a1dacc1
33b80ea
d5b165f
29e4b47
ab85fe3
7d5636b
2954bba
e7abe8b
1db5eb0
cdb49a6
f6f714a
aa925cd
00d7c71
af9f561
fb2cefb
b53787d
e2ef46e
2dfc542
a38a243
da381da
ca5c618
4266665
e93b983
e01bf53
fb9fbe6
c507488
178e201
984e8e1
fab215d
7300552
9bbe2cc
4dda9df
cd3eb5f
f8ef4ce
04e31a1
5fd1279
19d6f32
552078a
4ec3fc8
2ba0a27
e468d62
3fe368f
18a0496
d320269
8490488
db67486
03cf8c1
43f6de7
70e48d2
1e26c73
f4e3fb5
2e87ad3
54432be
cfbecb0
3a518f2
54106e2
b44a604
4c5c707
caff543
218f530
216937a
c0aee8c
1ef7952
5790b8e
ba9d3ff
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 |
---|---|---|
|
@@ -24,12 +24,15 @@ use super::{ | |
HashT, HeadData, Header, Id, Id as ParaId, MultiDisputeStatementSet, ScheduledCore, | ||
UncheckedSignedAvailabilityBitfields, ValidationCodeHash, | ||
}; | ||
use alloc::{ | ||
collections::{BTreeMap, BTreeSet, VecDeque}, | ||
vec, | ||
vec::Vec, | ||
}; | ||
use bitvec::prelude::*; | ||
use sp_application_crypto::ByteArray; | ||
|
||
use alloc::{vec, vec::Vec}; | ||
use codec::{Decode, Encode}; | ||
use scale_info::TypeInfo; | ||
use sp_application_crypto::ByteArray; | ||
use sp_core::RuntimeDebug; | ||
use sp_runtime::traits::Header as HeaderT; | ||
use sp_staking::SessionIndex; | ||
|
@@ -298,9 +301,9 @@ pub struct ClaimQueueOffset(pub u8); | |
/// Signals that a parachain can send to the relay chain via the UMP queue. | ||
#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)] | ||
pub enum UMPSignal { | ||
/// A message sent by a parachain to select the core the candidate is commited to. | ||
/// A message sent by a parachain to select the core the candidate is committed to. | ||
/// Relay chain validators, in particular backers, use the `CoreSelector` and | ||
/// `ClaimQueueOffset` to compute the index of the core the candidate has commited to. | ||
/// `ClaimQueueOffset` to compute the index of the core the candidate has committed to. | ||
SelectCore(CoreSelector, ClaimQueueOffset), | ||
} | ||
/// Separator between `XCM` and `UMPSignal`. | ||
|
@@ -324,6 +327,25 @@ impl CandidateCommitments { | |
UMPSignal::SelectCore(core_selector, cq_offset) => Some((core_selector, cq_offset)), | ||
} | ||
} | ||
|
||
/// 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 | ||
} | ||
|
||
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. | ||
|
@@ -337,7 +359,8 @@ pub enum CandidateReceiptError { | |
InvalidSelectedCore, | ||
/// The parachain is not assigned to any core at specified claim queue offset. | ||
NoAssignment, | ||
/// No core was selected. | ||
/// No core was selected. The `SelectCore` commitment is mandatory for | ||
/// v2 receipts if parachains has multiple cores assigned. | ||
NoCoreSelected, | ||
/// Unknown version. | ||
UnknownVersion(InternalVersion), | ||
|
@@ -432,33 +455,57 @@ impl<H: Copy> CandidateDescriptorV2<H> { | |
} | ||
|
||
impl<H: Copy> CommittedCandidateReceiptV2<H> { | ||
/// Checks if descriptor core index is equal to the commited core index. | ||
/// Input `assigned_cores` must contain the sorted cores assigned to the para at | ||
/// the committed claim queue offset. | ||
pub fn check(&self, assigned_cores: &[CoreIndex]) -> Result<(), CandidateReceiptError> { | ||
// Don't check v1 descriptors. | ||
if self.descriptor.version() == CandidateDescriptorVersion::V1 { | ||
return Ok(()) | ||
} | ||
|
||
if self.descriptor.version() == CandidateDescriptorVersion::Unknown { | ||
return Err(CandidateReceiptError::UnknownVersion(self.descriptor.version)) | ||
/// 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. | ||
pub fn check_core_index( | ||
&self, | ||
cores_per_para: &BTreeMap<ParaId, BTreeMap<u8, BTreeSet<CoreIndex>>>, | ||
) -> Result<(), CandidateReceiptError> { | ||
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 assigned_cores.is_empty() { | ||
if cores_per_para.is_empty() { | ||
return Err(CandidateReceiptError::NoAssignment) | ||
} | ||
|
||
let descriptor_core_index = CoreIndex(self.descriptor.core_index as u32); | ||
|
||
let (core_selector, _cq_offset) = | ||
self.commitments.selected_core().ok_or(CandidateReceiptError::NoCoreSelected)?; | ||
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) | ||
}; | ||
|
||
// 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<_>>(); | ||
alindima marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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) | ||
} | ||
|
||
let core_index = assigned_cores | ||
.get(core_selector.0 as usize % assigned_cores.len()) | ||
.ok_or(CandidateReceiptError::InvalidCoreIndex)?; | ||
**assigned_cores.get(0).ok_or(CandidateReceiptError::NoAssignment)? | ||
}; | ||
|
||
if *core_index != descriptor_core_index { | ||
let descriptor_core_index = CoreIndex(self.descriptor.core_index as u32); | ||
if core_index != descriptor_core_index { | ||
return Err(CandidateReceiptError::CoreIndexMismatch) | ||
} | ||
|
||
|
@@ -512,6 +559,12 @@ impl<H> BackedCandidate<H> { | |
&self.candidate | ||
} | ||
|
||
/// Get a mutable reference to the committed candidate receipt of the candidate. | ||
/// Only for testing. | ||
#[cfg(feature = "test")] | ||
pub fn candidate_mut(&mut self) -> &mut CommittedCandidateReceiptV2<H> { | ||
&mut self.candidate | ||
} | ||
/// Get a reference to the descriptor of the candidate. | ||
pub fn descriptor(&self) -> &CandidateDescriptorV2<H> { | ||
&self.candidate.descriptor | ||
|
@@ -697,6 +750,26 @@ impl<H: Copy> From<CoreState<H>> for super::v8::CoreState<H> { | |
} | ||
} | ||
|
||
/// Returns a mapping between the para id and the core indices assigned at different | ||
/// depths in the claim queue. | ||
pub fn transpose_claim_queue( | ||
claim_queue: BTreeMap<CoreIndex, VecDeque<Id>>, | ||
) -> BTreeMap<ParaId, BTreeMap<u8, BTreeSet<CoreIndex>>> { | ||
Comment on lines
+756
to
+757
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. Some type alias should help here. 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. Agree :) 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 was using a
|
||
let mut per_para_claim_queue = BTreeMap::new(); | ||
|
||
for (core, paras) in claim_queue { | ||
// Iterate paras assigned to this core at each depth. | ||
for (depth, para) in paras.into_iter().enumerate() { | ||
let depths: &mut BTreeMap<u8, BTreeSet<CoreIndex>> = | ||
per_para_claim_queue.entry(para).or_insert_with(|| Default::default()); | ||
|
||
depths.entry(depth as u8).or_default().insert(core); | ||
} | ||
} | ||
|
||
per_para_claim_queue | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
@@ -778,7 +851,7 @@ mod tests { | |
|
||
assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::Unknown); | ||
assert_eq!( | ||
new_ccr.check(&vec![].as_slice()), | ||
new_ccr.check_core_index(&BTreeMap::new()), | ||
Err(CandidateReceiptError::UnknownVersion(InternalVersion(100))) | ||
) | ||
} | ||
|
@@ -802,7 +875,13 @@ mod tests { | |
.upward_messages | ||
.force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode()); | ||
|
||
assert_eq!(new_ccr.check(&vec![CoreIndex(123)]), Ok(())); | ||
let mut cq = BTreeMap::new(); | ||
cq.insert( | ||
CoreIndex(123), | ||
vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), | ||
); | ||
|
||
assert_eq!(new_ccr.check_core_index(&transpose_claim_queue(cq)), Ok(())); | ||
} | ||
|
||
#[test] | ||
|
@@ -814,21 +893,31 @@ mod tests { | |
new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); | ||
new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); | ||
|
||
// The check should fail because no `SelectCore` signal was sent. | ||
assert_eq!( | ||
new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]), | ||
Err(CandidateReceiptError::NoCoreSelected) | ||
); | ||
let mut cq = BTreeMap::new(); | ||
cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); | ||
|
||
// The check should not fail because no `SelectCore` signal was sent. | ||
// The message is optional. | ||
assert!(new_ccr.check_core_index(&transpose_claim_queue(cq)).is_ok()); | ||
|
||
// Garbage message. | ||
new_ccr.commitments.upward_messages.force_push(vec![0, 13, 200].encode()); | ||
|
||
// No `SelectCore` can be decoded. | ||
assert_eq!(new_ccr.commitments.selected_core(), None); | ||
|
||
// Failure is expected. | ||
let mut cq = BTreeMap::new(); | ||
cq.insert( | ||
CoreIndex(0), | ||
vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), | ||
); | ||
cq.insert( | ||
CoreIndex(100), | ||
vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), | ||
); | ||
|
||
assert_eq!( | ||
new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]), | ||
new_ccr.check_core_index(&transpose_claim_queue(cq.clone())), | ||
Err(CandidateReceiptError::NoCoreSelected) | ||
); | ||
|
||
|
@@ -847,7 +936,7 @@ mod tests { | |
.force_push(UMPSignal::SelectCore(CoreSelector(1), ClaimQueueOffset(1)).encode()); | ||
|
||
// Duplicate doesn't override first signal. | ||
assert_eq!(new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]), Ok(())); | ||
assert_eq!(new_ccr.check_core_index(&transpose_claim_queue(cq)), Ok(())); | ||
} | ||
|
||
#[test] | ||
|
@@ -884,13 +973,57 @@ mod tests { | |
Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); | ||
|
||
assert_eq!(v2_ccr.descriptor.core_index(), Some(CoreIndex(123))); | ||
assert_eq!(new_ccr.check(&vec![CoreIndex(123)]), Ok(())); | ||
|
||
let mut cq = BTreeMap::new(); | ||
cq.insert( | ||
CoreIndex(123), | ||
vec![new_ccr.descriptor.para_id(), new_ccr.descriptor.para_id()].into(), | ||
); | ||
|
||
assert_eq!(new_ccr.check_core_index(&transpose_claim_queue(cq)), Ok(())); | ||
|
||
assert_eq!(new_ccr.hash(), v2_ccr.hash()); | ||
} | ||
|
||
// Only check descriptor `core_index` field of v2 descriptors. If it is v1, that field | ||
// will be garbage. | ||
#[test] | ||
fn test_v1_descriptors_with_ump_signal() { | ||
let mut ccr = dummy_old_committed_candidate_receipt(); | ||
ccr.descriptor.para_id = ParaId::new(1024); | ||
// Adding collator signature should make it decode as v1. | ||
ccr.descriptor.signature = dummy_collator_signature(); | ||
ccr.descriptor.collator = dummy_collator_id(); | ||
|
||
ccr.commitments.upward_messages.force_push(UMP_SEPARATOR); | ||
ccr.commitments | ||
.upward_messages | ||
.force_push(UMPSignal::SelectCore(CoreSelector(1), ClaimQueueOffset(1)).encode()); | ||
|
||
let encoded_ccr: Vec<u8> = ccr.encode(); | ||
|
||
let v1_ccr: CommittedCandidateReceiptV2 = | ||
Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); | ||
|
||
assert_eq!(v1_ccr.descriptor.version(), CandidateDescriptorVersion::V1); | ||
assert!(v1_ccr.commitments.selected_core().is_some()); | ||
|
||
let mut cq = BTreeMap::new(); | ||
cq.insert(CoreIndex(0), vec![v1_ccr.descriptor.para_id()].into()); | ||
cq.insert(CoreIndex(1), vec![v1_ccr.descriptor.para_id()].into()); | ||
|
||
assert!(v1_ccr.check_core_index(&transpose_claim_queue(cq)).is_ok()); | ||
|
||
assert_eq!( | ||
v1_ccr.commitments.committed_core_index(&vec![&CoreIndex(10), &CoreIndex(5)]), | ||
Some(CoreIndex(5)), | ||
); | ||
|
||
assert_eq!(v1_ccr.descriptor.core_index(), None); | ||
} | ||
|
||
#[test] | ||
fn test_core_select_is_mandatory() { | ||
fn test_core_select_is_optional() { | ||
// Testing edge case when collators provide zeroed signature and collator id. | ||
let mut old_ccr = dummy_old_committed_candidate_receipt(); | ||
old_ccr.descriptor.para_id = ParaId::new(1000); | ||
|
@@ -899,11 +1032,22 @@ mod tests { | |
let new_ccr: CommittedCandidateReceiptV2 = | ||
Decode::decode(&mut encoded_ccr.as_slice()).unwrap(); | ||
|
||
let mut cq = BTreeMap::new(); | ||
cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); | ||
|
||
// Since collator sig and id are zeroed, it means that the descriptor uses format | ||
// version 2. | ||
// We expect the check to fail in such case because there will be no `SelectCore` | ||
// commitment. | ||
assert_eq!(new_ccr.check(&vec![CoreIndex(0)]), Err(CandidateReceiptError::NoCoreSelected)); | ||
// version 2. Should still pass checks without core selector. | ||
assert!(new_ccr.check_core_index(&transpose_claim_queue(cq)).is_ok()); | ||
|
||
let mut cq = BTreeMap::new(); | ||
cq.insert(CoreIndex(0), vec![new_ccr.descriptor.para_id()].into()); | ||
cq.insert(CoreIndex(1), vec![new_ccr.descriptor.para_id()].into()); | ||
|
||
// Should fail because 2 cores are assigned, | ||
assert_eq!( | ||
new_ccr.check_core_index(&transpose_claim_queue(cq)), | ||
Err(CandidateReceiptError::NoCoreSelected) | ||
); | ||
|
||
// Adding collator signature should make it decode as v1. | ||
old_ccr.descriptor.signature = dummy_collator_signature(); | ||
|
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.
what i didn't get from the RFC is why do we need to do
% assigned_cores
and not assume that thecore_selector < assigned_cores.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.
Core selector is a sequence number that wraps around. We use the LSB of block numbers to fill it in.