Files
pezkuwi-subxt/polkadot/node/core/provisioner/src/tests.rs
T
Alin Dima 62b78a1615 provisioner: allow multiple cores assigned to the same para (#3233)
https://github.com/paritytech/polkadot-sdk/issues/3130

builds on top of https://github.com/paritytech/polkadot-sdk/pull/3160

Processes the availability cores and builds a record of how many
candidates it should request from prospective-parachains and their
predecessors.
Tries to supply as many candidates as the runtime can back. Note that
the runtime changes to back multiple candidates per para are not yet
done, but this paves the way for it.

The following backing/inclusion policy is assumed:
1. the runtime will never back candidates of the same para which don't
form a chain with the already backed candidates. Even if the others are
still pending availability. We're optimistic that they won't time out
and we don't want to back parachain forks (as the complexity would be
huge).
2. if a candidate is timed out of the core before being included, all of
its successors occupying a core will be evicted.
3. only the candidates which are made available and form a chain
starting from the on-chain para head may be included/enacted and cleared
from the cores. In other words, if para head is at A and the cores are
occupied by B->C->D, and B and D are made available, only B will be
included and its core cleared. C and D will remain on the cores awaiting
for C to be made available or timed out. As point (2) above already
says, if C is timed out, D will also be dropped.
4. The runtime will deduplicate candidates which form a cycle. For
example if the provisioner supplies candidates A->B->A, the runtime will
only back A (as the state output will be the same)

Note that if a candidate is timed out, we don't guarantee that in the
next relay chain block the block author will be able to fill all of the
timed out cores of the para. That increases complexity by a lot.
Instead, the provisioner will supply N candidates where N is the number
of candidates timed out, but doesn't include their successors which will
be also deleted by the runtime. This'll be backfilled in the next relay
chain block.

Adjacent changes:
- Also fixes: https://github.com/paritytech/polkadot-sdk/issues/3141
- For non prospective-parachains, don't supply multiple candidates per
para (we can't have elastic scaling without prospective parachains
enabled). paras_inherent should already sanitise this input but it's
more efficient this way.

Note: all of these changes are backwards-compatible with the
non-elastic-scaling scenario (one core per para).
2024-03-01 18:25:24 +00:00

1152 lines
37 KiB
Rust

// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use ::test_helpers::{dummy_candidate_descriptor, dummy_hash};
use bitvec::bitvec;
use polkadot_primitives::{OccupiedCore, ScheduledCore};
const MOCK_GROUP_SIZE: usize = 5;
pub fn occupied_core(para_id: u32) -> CoreState {
let mut candidate_descriptor = dummy_candidate_descriptor(dummy_hash());
candidate_descriptor.para_id = para_id.into();
CoreState::Occupied(OccupiedCore {
group_responsible: para_id.into(),
next_up_on_available: None,
occupied_since: 100_u32,
time_out_at: 200_u32,
next_up_on_time_out: None,
availability: bitvec![u8, bitvec::order::Lsb0; 0; 32],
candidate_descriptor,
candidate_hash: Default::default(),
})
}
pub fn build_occupied_core<Builder>(para_id: u32, builder: Builder) -> CoreState
where
Builder: FnOnce(&mut OccupiedCore),
{
let mut core = match occupied_core(para_id) {
CoreState::Occupied(core) => core,
_ => unreachable!(),
};
builder(&mut core);
CoreState::Occupied(core)
}
pub fn default_bitvec(size: usize) -> CoreAvailability {
bitvec![u8, bitvec::order::Lsb0; 0; size]
}
pub fn scheduled_core(id: u32) -> ScheduledCore {
ScheduledCore { para_id: id.into(), collator: None }
}
mod select_availability_bitfields {
use super::{super::*, default_bitvec, occupied_core};
use polkadot_primitives::{ScheduledCore, SigningContext, ValidatorId, ValidatorIndex};
use sp_application_crypto::AppCrypto;
use sp_keystore::{testing::MemoryKeystore, Keystore, KeystorePtr};
use std::sync::Arc;
fn signed_bitfield(
keystore: &KeystorePtr,
field: CoreAvailability,
validator_idx: ValidatorIndex,
) -> SignedAvailabilityBitfield {
let public = Keystore::sr25519_generate_new(&**keystore, ValidatorId::ID, None)
.expect("generated sr25519 key");
SignedAvailabilityBitfield::sign(
&keystore,
field.into(),
&<SigningContext<Hash>>::default(),
validator_idx,
&public.into(),
)
.ok()
.flatten()
.expect("Should be signed")
}
#[test]
fn not_more_than_one_per_validator() {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let mut bitvec = default_bitvec(2);
bitvec.set(0, true);
bitvec.set(1, true);
let cores = vec![occupied_core(0), occupied_core(1)];
// we pass in three bitfields with two validators
// this helps us check the postcondition that we get two bitfields back, for which the
// validators differ
let bitfields = vec![
signed_bitfield(&keystore, bitvec.clone(), ValidatorIndex(0)),
signed_bitfield(&keystore, bitvec.clone(), ValidatorIndex(1)),
signed_bitfield(&keystore, bitvec, ValidatorIndex(1)),
];
let mut selected_bitfields =
select_availability_bitfields(&cores, &bitfields, &Hash::repeat_byte(0));
selected_bitfields.sort_by_key(|bitfield| bitfield.validator_index());
assert_eq!(selected_bitfields.len(), 2);
assert_eq!(selected_bitfields[0], bitfields[0]);
// we don't know which of the (otherwise equal) bitfields will be selected
assert!(selected_bitfields[1] == bitfields[1] || selected_bitfields[1] == bitfields[2]);
}
#[test]
fn each_corresponds_to_an_occupied_core() {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let bitvec = default_bitvec(3);
// invalid: bit on free core
let mut bitvec0 = bitvec.clone();
bitvec0.set(0, true);
// invalid: bit on scheduled core
let mut bitvec1 = bitvec.clone();
bitvec1.set(1, true);
// valid: bit on occupied core.
let mut bitvec2 = bitvec.clone();
bitvec2.set(2, true);
let cores = vec![
CoreState::Free,
CoreState::Scheduled(ScheduledCore { para_id: Default::default(), collator: None }),
occupied_core(2),
];
let bitfields = vec![
signed_bitfield(&keystore, bitvec0, ValidatorIndex(0)),
signed_bitfield(&keystore, bitvec1, ValidatorIndex(1)),
signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(2)),
];
let selected_bitfields =
select_availability_bitfields(&cores, &bitfields, &Hash::repeat_byte(0));
// selects only the valid bitfield
assert_eq!(selected_bitfields.len(), 1);
assert_eq!(selected_bitfields[0].payload().0, bitvec2);
}
#[test]
fn more_set_bits_win_conflicts() {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let mut bitvec = default_bitvec(2);
bitvec.set(0, true);
let mut bitvec1 = bitvec.clone();
bitvec1.set(1, true);
let cores = vec![occupied_core(0), occupied_core(1)];
let bitfields = vec![
signed_bitfield(&keystore, bitvec, ValidatorIndex(1)),
signed_bitfield(&keystore, bitvec1.clone(), ValidatorIndex(1)),
];
let selected_bitfields =
select_availability_bitfields(&cores, &bitfields, &Hash::repeat_byte(0));
assert_eq!(selected_bitfields.len(), 1);
assert_eq!(selected_bitfields[0].payload().0, bitvec1.clone());
}
#[test]
fn more_complex_bitfields() {
let keystore: KeystorePtr = Arc::new(MemoryKeystore::new());
let cores = vec![occupied_core(0), occupied_core(1), occupied_core(2), occupied_core(3)];
let mut bitvec0 = default_bitvec(4);
bitvec0.set(0, true);
bitvec0.set(2, true);
let mut bitvec1 = default_bitvec(4);
bitvec1.set(1, true);
let mut bitvec2 = default_bitvec(4);
bitvec2.set(2, true);
let mut bitvec3 = default_bitvec(4);
bitvec3.set(0, true);
bitvec3.set(1, true);
bitvec3.set(2, true);
bitvec3.set(3, true);
// these are out of order but will be selected in order. The better
// bitfield for 3 will be selected.
let bitfields = vec![
signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(3)),
signed_bitfield(&keystore, bitvec3.clone(), ValidatorIndex(3)),
signed_bitfield(&keystore, bitvec0.clone(), ValidatorIndex(0)),
signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(2)),
signed_bitfield(&keystore, bitvec1.clone(), ValidatorIndex(1)),
];
let selected_bitfields =
select_availability_bitfields(&cores, &bitfields, &Hash::repeat_byte(0));
assert_eq!(selected_bitfields.len(), 4);
assert_eq!(selected_bitfields[0].payload().0, bitvec0);
assert_eq!(selected_bitfields[1].payload().0, bitvec1);
assert_eq!(selected_bitfields[2].payload().0, bitvec2);
assert_eq!(selected_bitfields[3].payload().0, bitvec3);
}
}
pub(crate) mod common {
use super::super::*;
use futures::channel::mpsc;
use polkadot_node_subsystem::messages::AllMessages;
use polkadot_node_subsystem_test_helpers::TestSubsystemSender;
pub fn test_harness<OverseerFactory, Overseer, TestFactory, Test>(
overseer_factory: OverseerFactory,
test_factory: TestFactory,
) where
OverseerFactory: FnOnce(mpsc::UnboundedReceiver<AllMessages>) -> Overseer,
Overseer: Future<Output = ()>,
TestFactory: FnOnce(TestSubsystemSender) -> Test,
Test: Future<Output = ()>,
{
let (tx, rx) = polkadot_node_subsystem_test_helpers::sender_receiver();
let overseer = overseer_factory(rx);
let test = test_factory(tx);
futures::pin_mut!(overseer, test);
let _ = futures::executor::block_on(future::join(overseer, test));
}
}
mod select_candidates {
use super::{
super::*, build_occupied_core, common::test_harness, default_bitvec, occupied_core,
scheduled_core, MOCK_GROUP_SIZE,
};
use ::test_helpers::{dummy_candidate_descriptor, dummy_hash};
use futures::channel::mpsc;
use polkadot_node_subsystem::messages::{
AllMessages, RuntimeApiMessage,
RuntimeApiRequest::{
AvailabilityCores, PersistedValidationData as PersistedValidationDataReq,
},
};
use polkadot_node_subsystem_test_helpers::TestSubsystemSender;
use polkadot_node_subsystem_util::runtime::ProspectiveParachainsMode;
use polkadot_primitives::{
BlockNumber, CandidateCommitments, CommittedCandidateReceipt, PersistedValidationData,
};
use rstest::rstest;
const BLOCK_UNDER_PRODUCTION: BlockNumber = 128;
fn dummy_candidate_template() -> CandidateReceipt {
let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();
let mut descriptor_template = dummy_candidate_descriptor(dummy_hash());
descriptor_template.persisted_validation_data_hash = empty_hash;
CandidateReceipt {
descriptor: descriptor_template,
commitments_hash: CandidateCommitments::default().hash(),
}
}
fn make_candidates(
core_count: usize,
expected_backed_indices: Vec<usize>,
) -> (Vec<CandidateHash>, Vec<BackedCandidate>) {
let candidate_template = dummy_candidate_template();
let candidates: Vec<_> = std::iter::repeat(candidate_template)
.take(core_count)
.enumerate()
.map(|(idx, mut candidate)| {
candidate.descriptor.para_id = idx.into();
candidate
})
.collect();
let expected_backed = expected_backed_indices
.iter()
.map(|&idx| candidates[idx].clone())
.map(|c| {
BackedCandidate::new(
CommittedCandidateReceipt {
descriptor: c.descriptor.clone(),
commitments: Default::default(),
},
Vec::new(),
default_bitvec(MOCK_GROUP_SIZE),
None,
)
})
.collect();
let candidate_hashes = candidates.into_iter().map(|c| c.hash()).collect();
(candidate_hashes, expected_backed)
}
// For testing only one core assigned to a parachain, we return this set of availability cores:
//
// [
// 0: Free,
// 1: Scheduled(default),
// 2: Occupied(no next_up set),
// 3: Occupied(next_up_on_available set but not available),
// 4: Occupied(next_up_on_available set and available),
// 5: Occupied(next_up_on_time_out set but not timeout),
// 6: Occupied(next_up_on_time_out set and timeout but available),
// 7: Occupied(next_up_on_time_out set and timeout and not available),
// 8: Occupied(both next_up set, available),
// 9: Occupied(both next_up set, not available, no timeout),
// 10: Occupied(both next_up set, not available, timeout),
// 11: Occupied(next_up_on_available and available, but different successor para_id)
// ]
fn mock_availability_cores_one_per_para() -> Vec<CoreState> {
use std::ops::Not;
use CoreState::{Free, Scheduled};
vec![
// 0: Free,
Free,
// 1: Scheduled(default),
Scheduled(scheduled_core(1)),
// 2: Occupied(no next_up set),
occupied_core(2),
// 3: Occupied(next_up_on_available set but not available),
build_occupied_core(3, |core| {
core.next_up_on_available = Some(scheduled_core(3));
}),
// 4: Occupied(next_up_on_available set and available),
build_occupied_core(4, |core| {
core.next_up_on_available = Some(scheduled_core(4));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(41));
}),
// 5: Occupied(next_up_on_time_out set but not timeout),
build_occupied_core(5, |core| {
core.next_up_on_time_out = Some(scheduled_core(5));
}),
// 6: Occupied(next_up_on_time_out set and timeout but available),
build_occupied_core(6, |core| {
core.next_up_on_time_out = Some(scheduled_core(6));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.availability = core.availability.clone().not();
}),
// 7: Occupied(next_up_on_time_out set and timeout and not available),
build_occupied_core(7, |core| {
core.next_up_on_time_out = Some(scheduled_core(7));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(71));
}),
// 8: Occupied(both next_up set, available),
build_occupied_core(8, |core| {
core.next_up_on_available = Some(scheduled_core(8));
core.next_up_on_time_out = Some(scheduled_core(8));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(81));
}),
// 9: Occupied(both next_up set, not available, no timeout),
build_occupied_core(9, |core| {
core.next_up_on_available = Some(scheduled_core(9));
core.next_up_on_time_out = Some(scheduled_core(9));
}),
// 10: Occupied(both next_up set, not available, timeout),
build_occupied_core(10, |core| {
core.next_up_on_available = Some(scheduled_core(10));
core.next_up_on_time_out = Some(scheduled_core(10));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(101));
}),
// 11: Occupied(next_up_on_available and available, but different successor para_id)
build_occupied_core(11, |core| {
core.next_up_on_available = Some(scheduled_core(12));
core.availability = core.availability.clone().not();
}),
]
}
// For test purposes with multiple possible cores assigned to a para, we always return this set
// of availability cores:
fn mock_availability_cores_multiple_per_para() -> Vec<CoreState> {
use std::ops::Not;
use CoreState::{Free, Scheduled};
vec![
// 0: Free,
Free,
// 1: Scheduled(default),
Scheduled(scheduled_core(1)),
// 2: Occupied(no next_up set),
occupied_core(2),
// 3: Occupied(next_up_on_available set but not available),
build_occupied_core(3, |core| {
core.next_up_on_available = Some(scheduled_core(3));
}),
// 4: Occupied(next_up_on_available set and available),
build_occupied_core(4, |core| {
core.next_up_on_available = Some(scheduled_core(4));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(41));
}),
// 5: Occupied(next_up_on_time_out set but not timeout),
build_occupied_core(5, |core| {
core.next_up_on_time_out = Some(scheduled_core(5));
}),
// 6: Occupied(next_up_on_time_out set and timeout but available),
build_occupied_core(6, |core| {
core.next_up_on_time_out = Some(scheduled_core(6));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.availability = core.availability.clone().not();
}),
// 7: Occupied(next_up_on_time_out set and timeout and not available),
build_occupied_core(7, |core| {
core.next_up_on_time_out = Some(scheduled_core(7));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(71));
}),
// 8: Occupied(both next_up set, available),
build_occupied_core(8, |core| {
core.next_up_on_available = Some(scheduled_core(8));
core.next_up_on_time_out = Some(scheduled_core(8));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(81));
}),
// 9: Occupied(both next_up set, not available, no timeout),
build_occupied_core(9, |core| {
core.next_up_on_available = Some(scheduled_core(9));
core.next_up_on_time_out = Some(scheduled_core(9));
}),
// 10: Occupied(both next_up set, not available, timeout),
build_occupied_core(10, |core| {
core.next_up_on_available = Some(scheduled_core(10));
core.next_up_on_time_out = Some(scheduled_core(10));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(101));
}),
// 11: Occupied(next_up_on_available and available, but different successor para_id)
build_occupied_core(11, |core| {
core.next_up_on_available = Some(scheduled_core(12));
core.availability = core.availability.clone().not();
}),
// 12-14: Occupied(next_up_on_available and available, same para_id).
build_occupied_core(12, |core| {
core.next_up_on_available = Some(scheduled_core(12));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(121));
}),
build_occupied_core(12, |core| {
core.next_up_on_available = Some(scheduled_core(12));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(122));
}),
build_occupied_core(12, |core| {
core.next_up_on_available = Some(scheduled_core(12));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(123));
}),
// 15: Scheduled on same para_id as 12-14.
Scheduled(scheduled_core(12)),
// 16: Occupied(13, no next_up set, not available)
build_occupied_core(13, |core| {
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(131));
}),
// 17: Occupied(13, no next_up set, available)
build_occupied_core(13, |core| {
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(132));
}),
// 18: Occupied(13, next_up_on_available set to 13 but not available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(13));
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(133));
}),
// 19: Occupied(13, next_up_on_available set to 13 and available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(13));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(134));
}),
// 20: Occupied(13, next_up_on_time_out set to 13 but not timeout)
build_occupied_core(13, |core| {
core.next_up_on_time_out = Some(scheduled_core(13));
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(135));
}),
// 21: Occupied(13, next_up_on_available set to 14 and available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(14));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(136));
}),
// 22: Occupied(13, next_up_on_available set to 14 but not available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(14));
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(137));
}),
// 23: Occupied(13, both next_up set to 14, available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(14));
core.next_up_on_time_out = Some(scheduled_core(14));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(138));
}),
// 24: Occupied(13, both next_up set to 14, not available, timeout)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(14));
core.next_up_on_time_out = Some(scheduled_core(14));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(1399));
}),
// 25: Occupied(13, next_up_on_available and available, but successor para_id 15)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(15));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(139));
}),
// 26: Occupied(15, next_up_on_available and available, but successor para_id 13)
build_occupied_core(15, |core| {
core.next_up_on_available = Some(scheduled_core(13));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(151));
}),
// 27: Occupied(15, both next_up, both available and timed out)
build_occupied_core(15, |core| {
core.next_up_on_available = Some(scheduled_core(15));
core.availability = core.availability.clone().not();
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(152));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
}),
// 28: Occupied(13, both next_up set to 13, not available)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(13));
core.next_up_on_time_out = Some(scheduled_core(13));
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(1398));
}),
// 29: Occupied(13, both next_up set to 13, not available, timeout)
build_occupied_core(13, |core| {
core.next_up_on_available = Some(scheduled_core(13));
core.next_up_on_time_out = Some(scheduled_core(13));
core.time_out_at = BLOCK_UNDER_PRODUCTION;
core.candidate_hash = CandidateHash(Hash::from_low_u64_be(1397));
}),
]
}
async fn mock_overseer(
mut receiver: mpsc::UnboundedReceiver<AllMessages>,
mock_availability_cores: Vec<CoreState>,
mut expected: Vec<BackedCandidate>,
mut expected_ancestors: HashMap<Vec<CandidateHash>, Ancestors>,
prospective_parachains_mode: ProspectiveParachainsMode,
) {
use ChainApiMessage::BlockNumber;
use RuntimeApiMessage::Request;
let mut backed_iter = expected.clone().into_iter();
expected.sort_by_key(|c| c.candidate().descriptor.para_id);
let mut candidates_iter = expected
.iter()
.map(|candidate| (candidate.hash(), candidate.descriptor().relay_parent));
while let Some(from_job) = receiver.next().await {
match from_job {
AllMessages::ChainApi(BlockNumber(_relay_parent, tx)) =>
tx.send(Ok(Some(BLOCK_UNDER_PRODUCTION - 1))).unwrap(),
AllMessages::RuntimeApi(Request(
_parent_hash,
PersistedValidationDataReq(_para_id, _assumption, tx),
)) => tx.send(Ok(Some(Default::default()))).unwrap(),
AllMessages::RuntimeApi(Request(_parent_hash, AvailabilityCores(tx))) =>
tx.send(Ok(mock_availability_cores.clone())).unwrap(),
AllMessages::CandidateBacking(CandidateBackingMessage::GetBackedCandidates(
hashes,
sender,
)) => {
let response: Vec<BackedCandidate> =
backed_iter.by_ref().take(hashes.len()).collect();
let expected_hashes: Vec<(CandidateHash, Hash)> = response
.iter()
.map(|candidate| (candidate.hash(), candidate.descriptor().relay_parent))
.collect();
assert_eq!(expected_hashes, hashes);
let _ = sender.send(response);
},
AllMessages::ProspectiveParachains(
ProspectiveParachainsMessage::GetBackableCandidates(
_,
_para_id,
count,
actual_ancestors,
tx,
),
) => match prospective_parachains_mode {
ProspectiveParachainsMode::Enabled { .. } => {
assert!(count > 0);
let candidates =
(&mut candidates_iter).take(count as usize).collect::<Vec<_>>();
assert_eq!(candidates.len(), count as usize);
if !expected_ancestors.is_empty() {
if let Some(expected_required_ancestors) = expected_ancestors.remove(
&(candidates
.clone()
.into_iter()
.take(actual_ancestors.len())
.map(|(c_hash, _)| c_hash)
.collect::<Vec<_>>()),
) {
assert_eq!(expected_required_ancestors, actual_ancestors);
} else {
assert_eq!(actual_ancestors.len(), 0);
}
}
let _ = tx.send(candidates);
},
ProspectiveParachainsMode::Disabled =>
panic!("unexpected prospective parachains request"),
},
_ => panic!("Unexpected message: {:?}", from_job),
}
}
if let ProspectiveParachainsMode::Enabled { .. } = prospective_parachains_mode {
assert_eq!(candidates_iter.next(), None);
}
assert_eq!(expected_ancestors.len(), 0);
}
#[rstest]
#[case(ProspectiveParachainsMode::Disabled)]
#[case(ProspectiveParachainsMode::Enabled {max_candidate_depth: 0, allowed_ancestry_len: 0})]
fn can_succeed(#[case] prospective_parachains_mode: ProspectiveParachainsMode) {
test_harness(
|r| {
mock_overseer(
r,
Vec::new(),
Vec::new(),
HashMap::new(),
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
select_candidates(
&[],
&[],
&[],
prospective_parachains_mode,
false,
Default::default(),
&mut tx,
)
.await
.unwrap();
},
)
}
// Test candidate selection when prospective parachains mode is disabled.
// This tests that only the appropriate candidates get selected when prospective parachains mode
// is disabled. To accomplish this, we supply a candidate list containing one candidate per
// possible core; the candidate selection algorithm must filter them to the appropriate set
#[rstest]
// why those particular indices? see the comments on mock_availability_cores_*() functions.
#[case(mock_availability_cores_one_per_para(), vec![1, 4, 7, 8, 10], true)]
#[case(mock_availability_cores_one_per_para(), vec![1, 4, 7, 8, 10], false)]
#[case(mock_availability_cores_multiple_per_para(), vec![1, 4, 7, 8, 10, 12, 13, 14, 15], true)]
#[case(mock_availability_cores_multiple_per_para(), vec![1, 4, 7, 8, 10, 12, 13, 14, 15], false)]
fn test_in_subsystem_selection(
#[case] mock_cores: Vec<CoreState>,
#[case] expected_candidates: Vec<usize>,
#[case] elastic_scaling_mvp: bool,
) {
let candidate_template = dummy_candidate_template();
let candidates: Vec<_> = std::iter::repeat(candidate_template)
.take(mock_cores.len())
.enumerate()
.map(|(idx, mut candidate)| {
candidate.descriptor.para_id = idx.into();
candidate
})
.cycle()
.take(mock_cores.len() * 3)
.enumerate()
.map(|(idx, mut candidate)| {
if idx < mock_cores.len() {
// first go-around: use candidates which should work
candidate
} else if idx < mock_cores.len() * 2 {
// for the second repetition of the candidates, give them the wrong hash
candidate.descriptor.persisted_validation_data_hash = Default::default();
candidate
} else {
// third go-around: right hash, wrong para_id
candidate.descriptor.para_id = idx.into();
candidate
}
})
.collect();
let expected_candidates: Vec<_> =
expected_candidates.into_iter().map(|idx| candidates[idx].clone()).collect();
let prospective_parachains_mode = ProspectiveParachainsMode::Disabled;
let expected_backed = expected_candidates
.iter()
.map(|c| {
BackedCandidate::new(
CommittedCandidateReceipt {
descriptor: c.descriptor().clone(),
commitments: Default::default(),
},
Vec::new(),
default_bitvec(MOCK_GROUP_SIZE),
None,
)
})
.collect();
let mock_cores_clone = mock_cores.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_backed,
HashMap::new(),
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result: Vec<BackedCandidate> = select_candidates(
&mock_cores,
&[],
&candidates,
prospective_parachains_mode,
elastic_scaling_mvp,
Default::default(),
&mut tx,
)
.await
.unwrap();
result.into_iter().for_each(|c| {
assert!(
expected_candidates.iter().any(|c2| c.candidate().corresponds_to(c2)),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
#[rstest]
#[case(ProspectiveParachainsMode::Disabled)]
#[case(ProspectiveParachainsMode::Enabled {max_candidate_depth: 0, allowed_ancestry_len: 0})]
fn selects_max_one_code_upgrade(
#[case] prospective_parachains_mode: ProspectiveParachainsMode,
) {
let mock_cores = mock_availability_cores_one_per_para();
let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();
// why those particular indices? see the comments on mock_availability_cores()
// the first candidate with code is included out of [1, 4, 7, 8, 10, 12].
let cores = [1, 4, 7, 8, 10, 12];
let cores_with_code = [1, 4, 8];
let expected_cores = [1, 7, 10, 12];
let committed_receipts: Vec<_> = (0..=mock_cores.len())
.map(|i| {
let mut descriptor = dummy_candidate_descriptor(dummy_hash());
descriptor.para_id = i.into();
descriptor.persisted_validation_data_hash = empty_hash;
CommittedCandidateReceipt {
descriptor,
commitments: CandidateCommitments {
new_validation_code: if cores_with_code.contains(&i) {
Some(vec![].into())
} else {
None
},
..Default::default()
},
}
})
.collect();
// Input to select_candidates
let candidates: Vec<_> = committed_receipts.iter().map(|r| r.to_plain()).collect();
// Build possible outputs from select_candidates
let backed_candidates: Vec<_> = committed_receipts
.iter()
.map(|committed_receipt| {
BackedCandidate::new(
committed_receipt.clone(),
Vec::new(),
default_bitvec(MOCK_GROUP_SIZE),
None,
)
})
.collect();
// First, provisioner will request backable candidates for each scheduled core.
// Then, some of them get filtered due to new validation code rule.
let expected_backed: Vec<_> =
cores.iter().map(|&idx| backed_candidates[idx].clone()).collect();
let expected_backed_filtered: Vec<_> =
expected_cores.iter().map(|&idx| candidates[idx].clone()).collect();
let mock_cores_clone = mock_cores.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_backed,
HashMap::new(),
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
&[],
&candidates,
prospective_parachains_mode,
false,
Default::default(),
&mut tx,
)
.await
.unwrap();
assert_eq!(result.len(), 4);
result.into_iter().for_each(|c| {
assert!(
expected_backed_filtered.iter().any(|c2| c.candidate().corresponds_to(c2)),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
#[rstest]
#[case(true)]
#[case(false)]
fn request_from_prospective_parachains_one_core_per_para(#[case] elastic_scaling_mvp: bool) {
let mock_cores = mock_availability_cores_one_per_para();
// why those particular indices? see the comments on mock_availability_cores()
let expected_candidates: Vec<_> = vec![1, 4, 7, 8, 10, 12];
let (candidates, expected_candidates) =
make_candidates(mock_cores.len() + 1, expected_candidates);
// Expect prospective parachains subsystem requests.
let prospective_parachains_mode =
ProspectiveParachainsMode::Enabled { max_candidate_depth: 0, allowed_ancestry_len: 0 };
let mut required_ancestors: HashMap<Vec<CandidateHash>, Ancestors> = HashMap::new();
required_ancestors.insert(
vec![candidates[4]],
vec![CandidateHash(Hash::from_low_u64_be(41))].into_iter().collect(),
);
required_ancestors.insert(
vec![candidates[8]],
vec![CandidateHash(Hash::from_low_u64_be(81))].into_iter().collect(),
);
let mock_cores_clone = mock_cores.clone();
let expected_candidates_clone = expected_candidates.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_candidates_clone,
required_ancestors,
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
&[],
&[],
prospective_parachains_mode,
elastic_scaling_mvp,
Default::default(),
&mut tx,
)
.await
.unwrap();
assert_eq!(result.len(), expected_candidates.len());
result.into_iter().for_each(|c| {
assert!(
expected_candidates
.iter()
.any(|c2| c.candidate().corresponds_to(&c2.receipt())),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
#[test]
fn request_from_prospective_parachains_multiple_cores_per_para_elastic_scaling_mvp() {
let mock_cores = mock_availability_cores_multiple_per_para();
// why those particular indices? see the comments on mock_availability_cores()
let expected_candidates: Vec<_> =
vec![1, 4, 7, 8, 10, 12, 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15];
// Expect prospective parachains subsystem requests.
let prospective_parachains_mode =
ProspectiveParachainsMode::Enabled { max_candidate_depth: 0, allowed_ancestry_len: 0 };
let (candidates, expected_candidates) =
make_candidates(mock_cores.len(), expected_candidates);
let mut required_ancestors: HashMap<Vec<CandidateHash>, Ancestors> = HashMap::new();
required_ancestors.insert(
vec![candidates[4]],
vec![CandidateHash(Hash::from_low_u64_be(41))].into_iter().collect(),
);
required_ancestors.insert(
vec![candidates[8]],
vec![CandidateHash(Hash::from_low_u64_be(81))].into_iter().collect(),
);
required_ancestors.insert(
[12, 12, 12].iter().map(|&idx| candidates[idx]).collect::<Vec<_>>(),
vec![
CandidateHash(Hash::from_low_u64_be(121)),
CandidateHash(Hash::from_low_u64_be(122)),
CandidateHash(Hash::from_low_u64_be(123)),
]
.into_iter()
.collect(),
);
required_ancestors.insert(
[13, 13, 13].iter().map(|&idx| candidates[idx]).collect::<Vec<_>>(),
(131..=139)
.map(|num| CandidateHash(Hash::from_low_u64_be(num)))
.chain(std::iter::once(CandidateHash(Hash::from_low_u64_be(1398))))
.collect(),
);
required_ancestors.insert(
[15, 15].iter().map(|&idx| candidates[idx]).collect::<Vec<_>>(),
vec![
CandidateHash(Hash::from_low_u64_be(151)),
CandidateHash(Hash::from_low_u64_be(152)),
]
.into_iter()
.collect(),
);
let mock_cores_clone = mock_cores.clone();
let expected_candidates_clone = expected_candidates.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_candidates,
required_ancestors,
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
&[],
&[],
prospective_parachains_mode,
true,
Default::default(),
&mut tx,
)
.await
.unwrap();
assert_eq!(result.len(), expected_candidates_clone.len());
result.into_iter().for_each(|c| {
assert!(
expected_candidates_clone
.iter()
.any(|c2| c.candidate().corresponds_to(&c2.receipt())),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
#[test]
fn request_from_prospective_parachains_multiple_cores_per_para_elastic_scaling_mvp_disabled() {
let mock_cores = mock_availability_cores_multiple_per_para();
// why those particular indices? see the comments on mock_availability_cores()
let expected_candidates: Vec<_> = vec![1, 4, 7, 8, 10];
// Expect prospective parachains subsystem requests.
let prospective_parachains_mode =
ProspectiveParachainsMode::Enabled { max_candidate_depth: 0, allowed_ancestry_len: 0 };
let (candidates, expected_candidates) =
make_candidates(mock_cores.len(), expected_candidates);
let mut required_ancestors: HashMap<Vec<CandidateHash>, Ancestors> = HashMap::new();
required_ancestors.insert(
vec![candidates[4]],
vec![CandidateHash(Hash::from_low_u64_be(41))].into_iter().collect(),
);
required_ancestors.insert(
vec![candidates[8]],
vec![CandidateHash(Hash::from_low_u64_be(81))].into_iter().collect(),
);
let mock_cores_clone = mock_cores.clone();
let expected_candidates_clone = expected_candidates.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_candidates,
required_ancestors,
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
&[],
&[],
prospective_parachains_mode,
false,
Default::default(),
&mut tx,
)
.await
.unwrap();
assert_eq!(result.len(), expected_candidates_clone.len());
result.into_iter().for_each(|c| {
assert!(
expected_candidates_clone
.iter()
.any(|c2| c.candidate().corresponds_to(&c2.receipt())),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
#[test]
fn request_receipts_based_on_relay_parent() {
let mock_cores = mock_availability_cores_one_per_para();
let candidate_template = dummy_candidate_template();
let candidates: Vec<_> = std::iter::repeat(candidate_template)
.take(mock_cores.len() + 1)
.enumerate()
.map(|(idx, mut candidate)| {
candidate.descriptor.para_id = idx.into();
candidate.descriptor.relay_parent = Hash::repeat_byte(idx as u8);
candidate
})
.collect();
// why those particular indices? see the comments on mock_availability_cores()
let expected_candidates: Vec<_> =
[1, 4, 7, 8, 10, 12].iter().map(|&idx| candidates[idx].clone()).collect();
// Expect prospective parachains subsystem requests.
let prospective_parachains_mode =
ProspectiveParachainsMode::Enabled { max_candidate_depth: 0, allowed_ancestry_len: 0 };
let expected_backed = expected_candidates
.iter()
.map(|c| {
BackedCandidate::new(
CommittedCandidateReceipt {
descriptor: c.descriptor().clone(),
commitments: Default::default(),
},
Vec::new(),
default_bitvec(MOCK_GROUP_SIZE),
None,
)
})
.collect();
let mock_cores_clone = mock_cores.clone();
test_harness(
|r| {
mock_overseer(
r,
mock_cores_clone,
expected_backed,
HashMap::new(),
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
&[],
&[],
prospective_parachains_mode,
false,
Default::default(),
&mut tx,
)
.await
.unwrap();
result.into_iter().for_each(|c| {
assert!(
expected_candidates.iter().any(|c2| c.candidate().corresponds_to(c2)),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}
}