remove provisioner checks (#4254)

* chore/provisioner: move metrics to a separate module

* avoid the duplicate names

* reduce all checks

* fixup tests

* Update node/core/provisioner/src/lib.rs

Co-authored-by: Zeke Mostov <z.mostov@gmail.com>

* chore: fmt

* chore: spellcheck

* doc

* remove the enum anti-pattern

* guide update - remove all the responsibilities

* add another trivial check

* Update node/core/provisioner/src/metrics.rs

Co-authored-by: Andronik Ordian <write@reusable.software>

* Update roadmap/implementers-guide/src/node/utility/provisioner.md

Co-authored-by: Andronik Ordian <write@reusable.software>

* Update node/core/provisioner/src/metrics.rs

Co-authored-by: Andronik Ordian <write@reusable.software>

Co-authored-by: Zeke Mostov <z.mostov@gmail.com>
Co-authored-by: Andronik Ordian <write@reusable.software>
This commit is contained in:
Bernhard Schuster
2021-11-19 19:15:59 +01:00
committed by GitHub
parent eee4bb2577
commit d5d916a915
8 changed files with 321 additions and 886 deletions
+146 -469
View File
@@ -1,481 +1,158 @@
// Copyright 2020 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 bitvec::bitvec;
use polkadot_primitives::v1::{OccupiedCore, ScheduledCore};
use futures::{channel::mpsc, future};
pub fn occupied_core(para_id: u32) -> CoreState {
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![bitvec::order::Lsb0, u8; 0; 32],
candidate_descriptor: Default::default(),
candidate_hash: Default::default(),
})
fn default_bitvec(n_cores: usize) -> bitvec::vec::BitVec<bitvec::order::Lsb0, u8> {
bitvec::vec::BitVec::repeat(false, n_cores)
}
pub fn build_occupied_core<Builder>(para_id: u32, builder: Builder) -> CoreState
where
Builder: FnOnce(&mut OccupiedCore),
use crate::collect_backed_candidates;
use polkadot_node_subsystem::messages::AllMessages;
use polkadot_node_subsystem_test_helpers::TestSubsystemSender;
use polkadot_primitives::v1::{
BlockNumber, CandidateCommitments, CandidateDescriptor, CommittedCandidateReceipt, Hash,
PersistedValidationData,
};
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 mut core = match occupied_core(para_id) {
CoreState::Occupied(core) => core,
_ => unreachable!(),
};
let (tx, rx) = polkadot_node_subsystem_test_helpers::sender_receiver();
let overseer = overseer_factory(rx);
let test = test_factory(tx);
builder(&mut core);
futures::pin_mut!(overseer, test);
CoreState::Occupied(core)
let _ = futures::executor::block_on(future::join(overseer, test));
}
pub fn default_bitvec(n_cores: usize) -> CoreAvailability {
bitvec![bitvec::order::Lsb0, u8; 0; n_cores]
}
pub fn scheduled_core(id: u32) -> ScheduledCore {
ScheduledCore { para_id: id.into(), ..Default::default() }
}
mod select_availability_bitfields {
use super::{super::*, default_bitvec, occupied_core};
use futures::executor::block_on;
use polkadot_primitives::v1::{SigningContext, ValidatorId, ValidatorIndex};
use sp_application_crypto::AppKey;
use sp_keystore::{testing::KeyStore, CryptoStore, SyncCryptoStorePtr};
use std::sync::Arc;
async fn signed_bitfield(
keystore: &SyncCryptoStorePtr,
field: CoreAvailability,
validator_idx: ValidatorIndex,
) -> SignedAvailabilityBitfield {
let public = CryptoStore::sr25519_generate_new(&**keystore, ValidatorId::ID, None)
.await
.expect("generated sr25519 key");
SignedAvailabilityBitfield::sign(
&keystore,
field.into(),
&<SigningContext<Hash>>::default(),
validator_idx,
&public.into(),
)
.await
.ok()
.flatten()
.expect("Should be signed")
}
#[test]
fn not_more_than_one_per_validator() {
let keystore: SyncCryptoStorePtr = Arc::new(KeyStore::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![
block_on(signed_bitfield(&keystore, bitvec.clone(), ValidatorIndex(0))),
block_on(signed_bitfield(&keystore, bitvec.clone(), ValidatorIndex(1))),
block_on(signed_bitfield(&keystore, bitvec, ValidatorIndex(1))),
];
let mut selected_bitfields = select_availability_bitfields(&cores, &bitfields);
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: SyncCryptoStorePtr = Arc::new(KeyStore::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(Default::default()), occupied_core(2)];
let bitfields = vec![
block_on(signed_bitfield(&keystore, bitvec0, ValidatorIndex(0))),
block_on(signed_bitfield(&keystore, bitvec1, ValidatorIndex(1))),
block_on(signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(2))),
];
let selected_bitfields = select_availability_bitfields(&cores, &bitfields);
// 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: SyncCryptoStorePtr = Arc::new(KeyStore::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![
block_on(signed_bitfield(&keystore, bitvec, ValidatorIndex(1))),
block_on(signed_bitfield(&keystore, bitvec1.clone(), ValidatorIndex(1))),
];
let selected_bitfields = select_availability_bitfields(&cores, &bitfields);
assert_eq!(selected_bitfields.len(), 1);
assert_eq!(selected_bitfields[0].payload().0, bitvec1.clone());
}
#[test]
fn more_complex_bitfields() {
let keystore: SyncCryptoStorePtr = Arc::new(KeyStore::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![
block_on(signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(3))),
block_on(signed_bitfield(&keystore, bitvec3.clone(), ValidatorIndex(3))),
block_on(signed_bitfield(&keystore, bitvec0.clone(), ValidatorIndex(0))),
block_on(signed_bitfield(&keystore, bitvec2.clone(), ValidatorIndex(2))),
block_on(signed_bitfield(&keystore, bitvec1.clone(), ValidatorIndex(1))),
];
let selected_bitfields = select_availability_bitfields(&cores, &bitfields);
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);
}
}
mod select_candidates {
use super::{super::*, build_occupied_core, default_bitvec, occupied_core, scheduled_core};
use polkadot_node_subsystem::messages::{
AllMessages, RuntimeApiMessage,
RuntimeApiRequest::{
AvailabilityCores, PersistedValidationData as PersistedValidationDataReq,
},
};
use polkadot_node_subsystem_test_helpers::TestSubsystemSender;
use polkadot_primitives::v1::{
BlockNumber, CandidateCommitments, CandidateDescriptor, CommittedCandidateReceipt,
PersistedValidationData,
};
const BLOCK_UNDER_PRODUCTION: BlockNumber = 128;
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));
}
// For test purposes, we always 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() -> 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();
}),
// 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;
}),
// 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();
}),
// 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;
}),
// 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();
}),
]
}
async fn mock_overseer(
mut receiver: mpsc::UnboundedReceiver<AllMessages>,
expected: Vec<BackedCandidate>,
) {
use ChainApiMessage::BlockNumber;
use RuntimeApiMessage::Request;
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())).unwrap(),
AllMessages::CandidateBacking(CandidateBackingMessage::GetBackedCandidates(
_,
_,
sender,
)) => {
let _ = sender.send(expected.clone());
},
_ => panic!("Unexpected message: {:?}", from_job),
}
async fn mock_overseer(
mut receiver: mpsc::UnboundedReceiver<AllMessages>,
expected: Vec<BackedCandidate>,
) {
while let Some(from_job) = receiver.next().await {
match from_job {
AllMessages::CandidateBacking(CandidateBackingMessage::GetBackedCandidates(
_,
_,
sender,
)) => {
let _ = sender.send(expected.clone());
},
_ => panic!("Unexpected message: {:?}", from_job),
}
}
#[test]
fn can_succeed() {
test_harness(
|r| mock_overseer(r, Vec::new()),
|mut tx: TestSubsystemSender| async move {
select_candidates(&[], &[], &[], Default::default(), &mut tx).await.unwrap();
},
)
}
// this tests that only the appropriate candidates get selected.
// 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
#[test]
fn selects_correct_candidates() {
let mock_cores = mock_availability_cores();
let n_cores = mock_cores.len();
let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();
let candidate_template = CandidateReceipt {
descriptor: CandidateDescriptor {
persisted_validation_data_hash: empty_hash,
..Default::default()
},
commitments_hash: CandidateCommitments::default().hash(),
};
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();
// why those particular indices? see the comments on mock_availability_cores()
let expected_candidates: Vec<_> =
[1, 4, 7, 8, 10].iter().map(|&idx| candidates[idx].clone()).collect();
let expected_backed = expected_candidates
.iter()
.map(|c| BackedCandidate {
candidate: CommittedCandidateReceipt {
descriptor: c.descriptor.clone(),
..Default::default()
},
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
})
.collect();
test_harness(
|r| mock_overseer(r, expected_backed),
|mut tx: TestSubsystemSender| async move {
let result =
select_candidates(&mock_cores, &[], &candidates, 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,
)
});
},
)
}
#[test]
fn selects_max_one_code_upgrade() {
let mock_cores = mock_availability_cores();
let n_cores = mock_cores.len();
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].
let cores = [1, 7, 10];
let cores_with_code = [1, 4, 8];
let committed_receipts: Vec<_> = (0..mock_cores.len())
.map(|i| CommittedCandidateReceipt {
descriptor: CandidateDescriptor {
para_id: i.into(),
persisted_validation_data_hash: empty_hash,
..Default::default()
},
commitments: CandidateCommitments {
new_validation_code: if cores_with_code.contains(&i) {
Some(vec![].into())
} else {
None
},
..Default::default()
},
..Default::default()
})
.collect();
let candidates: Vec<_> = committed_receipts.iter().map(|r| r.to_plain()).collect();
let expected_candidates: Vec<_> =
cores.iter().map(|&idx| candidates[idx].clone()).collect();
let expected_backed: Vec<_> = cores
.iter()
.map(|&idx| BackedCandidate {
candidate: committed_receipts[idx].clone(),
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
})
.collect();
test_harness(
|r| mock_overseer(r, expected_backed),
|mut tx: TestSubsystemSender| async move {
let result =
select_candidates(&mock_cores, &[], &candidates, 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,
)
});
},
)
}
}
#[test]
fn can_succeed() {
test_harness(
|r| mock_overseer(r, Vec::new()),
|mut tx: TestSubsystemSender| async move {
collect_backed_candidates(Vec::new(), Default::default(), &mut tx)
.await
.unwrap();
},
)
}
// this tests that only the appropriate candidates get selected.
// 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
#[test]
fn selects_correct_candidates() {
let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();
let candidate_template = CandidateReceipt {
descriptor: CandidateDescriptor {
persisted_validation_data_hash: empty_hash,
..Default::default()
},
commitments_hash: CandidateCommitments::default().hash(),
};
let n_cores = 5;
let candidate_receipts: Vec<_> = std::iter::repeat(candidate_template)
.take(n_cores)
.enumerate()
.map(|(idx, mut candidate)| {
candidate.descriptor.para_id = idx.into();
candidate
})
.cycle()
.take(n_cores * 4)
.enumerate()
.map(|(idx, mut candidate_receipt)| {
if idx < n_cores {
// first go-around: use candidates which should work
candidate_receipt
} else if idx < n_cores * 2 {
// for the second repetition of the candidates, give them the wrong hash
candidate_receipt.descriptor.persisted_validation_data_hash = Default::default();
candidate_receipt
} else if idx < n_cores * 3 {
// third go-around: right hash, wrong para_id
candidate_receipt.descriptor.para_id = idx.into();
candidate_receipt
} else {
// fourth go-around: wrong relay parent, this is the only thing that is checked
candidate_receipt.descriptor.relay_parent = Hash::repeat_byte(0xFF);
candidate_receipt
}
})
.collect();
// candidates now contains 1/3 valid canidates, and 2/3 invalid
// but we don't check them in them here, so they should be passed alright
let expected_candidate_receipts =
candidate_receipts.iter().take(n_cores * 3).cloned().collect::<Vec<_>>();
let expected_backed = expected_candidate_receipts
.iter()
.map(|candidate_receipt| BackedCandidate {
candidate: CommittedCandidateReceipt {
descriptor: candidate_receipt.descriptor.clone(),
..Default::default()
},
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
})
.collect();
test_harness(
|r| mock_overseer(r, expected_backed),
|mut tx: TestSubsystemSender| async move {
let result = collect_backed_candidates(candidate_receipts, Default::default(), &mut tx)
.await
.unwrap();
result.into_iter().for_each(|c| {
assert!(
expected_candidate_receipts.iter().any(|c2| c.candidate.corresponds_to(c2)),
"Failed to find candidate: {:?}",
c,
)
});
},
)
}