Inclusion Module (#1242)

* add availability bitfield types to primitives

* begin inclusion module

* use GitHub issue link for limitation

* fix some compiler errors

* integrate validators into initializer

* add generic signing context

* make signing-context more generic

* fix issues with inclusion module

* add TODO

* guide: add validators and session index to inclusion

* guide: add session index to change notification

* implement session change logic

* add BackedCandidate type

* guide: refine inclusion pipeline

* guide: rename group_on to group_validators

* guide: add check about collator for parathread

* guide: add last_code_upgrade to paras and use in inclusion

* implement Paras::last_code_upgrade

* implement most checks in process_candidates

* make candidate receipt structs more generic

* make BackedCandidate struct more generic

* use hash param, not block number

* check that candidate is in context of the parent block

* include inclusion module in initializer

* implement enact-candidate

* check that only occupied cores have bits set

* finish implementing bitfield processing

* restructure consistency checks on candidates

* make some more primitives generic

* signature checking logic for backed candidates

* finish implementing process_candidates

* implement collect_pending

* add some trait implementations to primitives

* implement InclusionInherent and squash warnings

* test bitfield signing checks

* rename parachain head to para_head

* fix note_new_head bug in paras

* test bitfield enactment in inclusion

* helpers for candidate checks

* add test for most candidate checks

* add test for backing setting storage

* test session change logic

* remove extraneous type parameter

* remove some allow(unused)s

* extract threshold computation to const fn

* remove some more allow(unused)s

* improve doc

* add debug assertion

* fix primitive test compilation

* tag unanimous variant as unused
This commit is contained in:
Robert Habermeier
2020-06-18 19:38:07 -04:00
committed by GitHub
parent 7accc6e499
commit 879892d3f9
12 changed files with 1969 additions and 74 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,120 @@
// 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/>.
//! Provides glue code over the scheduler and inclusion modules, and accepting
//! one inherent per block that can include new para candidates and bitfields.
//!
//! Unlike other modules in this crate, it does not need to be initialized by the initializer,
//! as it has no initialization logic and its finalization logic depends only on the details of
//! this module.
use sp_std::prelude::*;
use primitives::{
parachain::{BackedCandidate, SignedAvailabilityBitfields},
};
use frame_support::{
decl_storage, decl_module, decl_error, ensure,
dispatch::DispatchResult,
weights::{DispatchClass, Weight},
traits::Get,
};
use system::ensure_none;
use crate::{inclusion, scheduler::{self, FreedReason}};
pub trait Trait: inclusion::Trait + scheduler::Trait { }
decl_storage! {
trait Store for Module<T: Trait> as ParaInclusionInherent {
/// Whether the inclusion inherent was included within this block.
///
/// The `Option<()>` is effectively a bool, but it never hits storage in the `None` variant
/// due to the guarantees of FRAME's storage APIs.
///
/// If this is `None` at the end of the block, we panic and render the block invalid.
Included: Option<()>;
}
}
decl_error! {
pub enum Error for Module<T: Trait> {
/// Inclusion inherent called more than once per block.
TooManyInclusionInherents,
}
}
decl_module! {
/// The inclusion inherent module.
pub struct Module<T: Trait> for enum Call where origin: <T as system::Trait>::Origin {
type Error = Error<T>;
fn on_initialize() -> Weight {
T::DbWeight::get().reads_writes(1, 1) // in on_finalize.
}
fn on_finalize() {
if Included::take().is_none() {
panic!("Bitfields and heads must be included every block");
}
}
/// Include backed candidates and bitfields.
#[weight = (1_000_000_000, DispatchClass::Mandatory)]
pub fn inclusion(
origin,
signed_bitfields: SignedAvailabilityBitfields,
backed_candidates: Vec<BackedCandidate<T::Hash>>,
) -> DispatchResult {
ensure_none(origin)?;
ensure!(!<Included>::exists(), Error::<T>::TooManyInclusionInherents);
// Process new availability bitfields, yielding any availability cores whose
// work has now concluded.
let freed_concluded = <inclusion::Module<T>>::process_bitfields(
signed_bitfields,
<scheduler::Module<T>>::core_para,
)?;
// Handle timeouts for any availability core work.
let availability_pred = <scheduler::Module<T>>::availability_timeout_predicate();
let freed_timeout = if let Some(pred) = availability_pred {
<inclusion::Module<T>>::collect_pending(pred)
} else {
Vec::new()
};
// Schedule paras again, given freed cores, and reasons for freeing.
let freed = freed_concluded.into_iter().map(|c| (c, FreedReason::Concluded))
.chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut)));
<scheduler::Module<T>>::schedule(freed.collect());
// Process backed candidates according to scheduled cores.
let occupied = <inclusion::Module<T>>::process_candidates(
backed_candidates,
<scheduler::Module<T>>::scheduled(),
<scheduler::Module<T>>::group_validators,
)?;
// Note which of the scheduled cores were actually occupied by a backed candidate.
<scheduler::Module<T>>::occupied(&occupied);
// And track that we've finished processing the inherent for this block.
Included::set(Some(()));
Ok(())
}
}
}
+32 -9
View File
@@ -27,7 +27,7 @@ use primitives::{
use frame_support::{
decl_storage, decl_module, decl_error, traits::Randomness,
};
use crate::{configuration::{self, HostConfiguration}, paras, scheduler};
use crate::{configuration::{self, HostConfiguration}, paras, scheduler, inclusion};
/// Information about a session change that has just occurred.
#[derive(Default, Clone)]
@@ -42,9 +42,13 @@ pub struct SessionChangeNotification<BlockNumber> {
pub new_config: HostConfiguration<BlockNumber>,
/// A secure random seed for the session, gathered from BABE.
pub random_seed: [u8; 32],
/// New session index.
pub session_index: sp_staking::SessionIndex,
}
pub trait Trait: system::Trait + configuration::Trait + paras::Trait + scheduler::Trait {
pub trait Trait:
system::Trait + configuration::Trait + paras::Trait + scheduler::Trait + inclusion::Trait
{
/// A randomness beacon.
type Randomness: Randomness<Self::Hash>;
}
@@ -81,7 +85,8 @@ decl_module! {
// - Validity
let total_weight = configuration::Module::<T>::initializer_initialize(now) +
paras::Module::<T>::initializer_initialize(now) +
scheduler::Module::<T>::initializer_initialize(now);
scheduler::Module::<T>::initializer_initialize(now) +
inclusion::Module::<T>::initializer_initialize(now);
HasInitialized::set(Some(()));
@@ -91,6 +96,7 @@ decl_module! {
fn on_finalize() {
// reverse initialization order.
inclusion::Module::<T>::initializer_finalize();
scheduler::Module::<T>::initializer_finalize();
paras::Module::<T>::initializer_finalize();
configuration::Module::<T>::initializer_finalize();
@@ -101,16 +107,25 @@ decl_module! {
impl<T: Trait> Module<T> {
/// Should be called when a new session occurs. Forwards the session notification to all
/// wrapped modules.
/// wrapped modules. If `queued` is `None`, the `validators` are considered queued.
///
/// Panics if the modules have already been initialized.
fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, queued: I)
fn on_new_session<'a, I: 'a>(
_changed: bool,
session_index: sp_staking::SessionIndex,
validators: I,
queued: Option<I>,
)
where I: Iterator<Item=(&'a T::AccountId, ValidatorId)>
{
assert!(HasInitialized::get().is_none());
let validators: Vec<_> = validators.map(|(_, v)| v).collect();
let queued: Vec<_> = queued.map(|(_, v)| v).collect();
let queued: Vec<_> = if let Some(queued) = queued {
queued.map(|(_, v)| v).collect()
} else {
validators.clone()
};
let prev_config = <configuration::Module<T>>::config();
@@ -134,10 +149,12 @@ impl<T: Trait> Module<T> {
prev_config,
new_config,
random_seed,
session_index,
};
paras::Module::<T>::initializer_on_new_session(&notification);
scheduler::Module::<T>::initializer_on_new_session(&notification);
inclusion::Module::<T>::initializer_on_new_session(&notification);
}
}
@@ -145,7 +162,7 @@ impl<T: Trait> sp_runtime::BoundToRuntimeAppPublic for Module<T> {
type Public = ValidatorId;
}
impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
impl<T: session::Trait + Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
type Key = ValidatorId;
fn on_genesis_session<'a, I: 'a>(_validators: I)
@@ -157,7 +174,8 @@ impl<T: Trait> session::OneSessionHandler<T::AccountId> for Module<T> {
fn on_new_session<'a, I: 'a>(changed: bool, validators: I, queued: I)
where I: Iterator<Item=(&'a T::AccountId, Self::Key)>
{
<Module<T>>::on_new_session(changed, validators, queued);
let session_index = <session::Module<T>>::current_index();
<Module<T>>::on_new_session(changed, session_index, validators, Some(queued));
}
fn on_disabled(_i: usize) { }
@@ -175,7 +193,12 @@ mod tests {
fn panics_if_session_changes_after_on_initialize() {
new_test_ext(Default::default()).execute_with(|| {
Initializer::on_initialize(1);
Initializer::on_new_session(false, Vec::new().into_iter(), Vec::new().into_iter());
Initializer::on_new_session(
false,
1,
Vec::new().into_iter(),
Some(Vec::new().into_iter()),
);
});
}
+1
View File
@@ -22,6 +22,7 @@
mod configuration;
mod inclusion;
mod inclusion_inherent;
mod initializer;
mod paras;
mod scheduler;
+5
View File
@@ -99,6 +99,8 @@ impl crate::paras::Trait for Test { }
impl crate::scheduler::Trait for Test { }
impl crate::inclusion::Trait for Test { }
pub type System = system::Module<Test>;
/// Mocked initializer.
@@ -113,6 +115,9 @@ pub type Paras = crate::paras::Module<Test>;
/// Mocked scheduler.
pub type Scheduler = crate::scheduler::Module<Test>;
/// Mocked inclusion module.
pub type Inclusion = crate::inclusion::Module<Test>;
/// Create a new set of test externalities.
pub fn new_test_ext(state: GenesisConfig) -> TestExternalities {
let mut t = state.system.build_storage::<Test>().unwrap();
+51 -7
View File
@@ -174,7 +174,7 @@ decl_storage! {
/// All parathreads.
Parathreads: map hasher(twox_64_concat) ParaId => Option<()>;
/// The head-data of every registered para.
Heads get(fn parachain_head): map hasher(twox_64_concat) ParaId => Option<HeadData>;
Heads get(fn para_head): map hasher(twox_64_concat) ParaId => Option<HeadData>;
/// The validation code of every live para.
CurrentCode get(fn current_code): map hasher(twox_64_concat) ParaId => Option<ValidationCode>;
/// Actual past code, indicated by the para id as well as the block number at which it became outdated.
@@ -368,7 +368,7 @@ impl<T: Trait> Module<T> {
<Self as Store>::PastCode::remove(&(para_id, pruned_repl_at));
}
meta.most_recent_change().is_none() && Self::parachain_head(&para_id).is_none()
meta.most_recent_change().is_none() && Self::para_head(&para_id).is_none()
});
// This parachain has been removed and now the vestigial code
@@ -428,7 +428,6 @@ impl<T: Trait> Module<T> {
/// with number >= `expected_at`
///
/// If there is already a scheduled code upgrade for the para, this is a no-op.
#[allow(unused)]
pub(crate) fn schedule_code_upgrade(
id: ParaId,
new_code: ValidationCode,
@@ -448,15 +447,14 @@ impl<T: Trait> Module<T> {
/// Note that a para has progressed to a new head, where the new head was executed in the context
/// of a relay-chain block with given number. This will apply pending code upgrades based
/// on the block number provided.
#[allow(unused)]
pub(crate) fn note_new_head(
id: ParaId,
new_head: HeadData,
execution_context: T::BlockNumber,
) -> Weight {
if let Some(expected_at) = <Self as Store>::FutureCodeUpgrades::get(&id) {
Heads::insert(&id, new_head);
Heads::insert(&id, new_head);
if let Some(expected_at) = <Self as Store>::FutureCodeUpgrades::get(&id) {
if expected_at <= execution_context {
<Self as Store>::FutureCodeUpgrades::remove(&id);
@@ -481,7 +479,7 @@ impl<T: Trait> Module<T> {
T::DbWeight::get().reads_writes(1, 1 + 0)
}
} else {
T::DbWeight::get().reads_writes(1, 0)
T::DbWeight::get().reads_writes(1, 1)
}
}
@@ -526,6 +524,18 @@ impl<T: Trait> Module<T> {
pub(crate) fn is_parathread(id: ParaId) -> bool {
Parathreads::get(&id).is_some()
}
/// The block number of the last scheduled upgrade of the requested para. Includes future upgrades
/// if the flag is set. This is the `expected_at` number, not the `activated_at` number.
pub(crate) fn last_code_upgrade(id: ParaId, include_future: bool) -> Option<T::BlockNumber> {
if include_future {
if let Some(at) = Self::future_code_upgrade_at(id) {
return Some(at);
}
}
Self::past_code_meta(&id).most_recent_change()
}
}
#[cfg(test)]
@@ -683,6 +693,40 @@ mod tests {
});
}
#[test]
fn note_new_head_sets_head() {
let acceptance_period = 10;
let paras = vec![
(0u32.into(), ParaGenesisArgs {
parachain: true,
genesis_head: Default::default(),
validation_code: Default::default(),
}),
];
let genesis_config = MockGenesisConfig {
paras: GenesisConfig { paras, ..Default::default() },
configuration: crate::configuration::GenesisConfig {
config: HostConfiguration {
acceptance_period,
..Default::default()
},
..Default::default()
},
..Default::default()
};
new_test_ext(genesis_config).execute_with(|| {
let id_a = ParaId::from(0u32);
assert_eq!(Paras::para_head(&id_a), Some(Default::default()));
Paras::note_new_head(id_a, vec![1, 2, 3].into(), 0);
assert_eq!(Paras::para_head(&id_a), Some(vec![1, 2, 3].into()));
});
}
#[test]
fn note_past_code_sets_up_pruning_correctly() {
let acceptance_period = 10;
+15 -10
View File
@@ -57,11 +57,23 @@ use crate::{configuration, paras, initializer::SessionChangeNotification};
#[cfg_attr(test, derive(Debug))]
pub struct CoreIndex(u32);
impl From<u32> for CoreIndex {
fn from(i: u32) -> CoreIndex {
CoreIndex(i)
}
}
/// The unique (during session) index of a validator group.
#[derive(Encode, Decode, Default, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub struct GroupIndex(u32);
impl From<u32> for GroupIndex {
fn from(i: u32) -> GroupIndex {
GroupIndex(i)
}
}
/// A claim on authoring the next block for a given parathread.
#[derive(Clone, Encode, Decode, Default)]
#[cfg_attr(test, derive(PartialEq, Debug))]
@@ -130,7 +142,7 @@ pub enum AssignmentKind {
}
/// How a free core is scheduled to be assigned.
#[derive(Encode, Decode)]
#[derive(Clone, Encode, Decode)]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub struct CoreAssignment {
/// The core that is assigned.
@@ -145,7 +157,6 @@ pub struct CoreAssignment {
impl CoreAssignment {
/// Get the ID of a collator who is required to collate this block.
#[allow(unused)]
pub(crate) fn required_collator(&self) -> Option<&CollatorId> {
match self.kind {
AssignmentKind::Parachain => None,
@@ -153,7 +164,6 @@ impl CoreAssignment {
}
}
#[allow(unused)]
fn to_core_occupied(&self) -> CoreOccupied {
match self.kind {
AssignmentKind::Parachain => CoreOccupied::Parachain,
@@ -168,7 +178,6 @@ impl CoreAssignment {
}
/// Reasons a core might be freed
#[allow(unused)]
pub enum FreedReason {
/// The core's work concluded and the parablock assigned to it is considered available.
Concluded,
@@ -525,7 +534,6 @@ impl<T: Trait> Module<T> {
///
/// Complexity: O(n) in the number of scheduled cores, which is capped at the number of total cores.
/// This is efficient in the case that most scheduled cores are occupied.
#[allow(unused)]
pub(crate) fn occupied(now_occupied: &[CoreIndex]) {
if now_occupied.is_empty() { return }
@@ -557,7 +565,6 @@ impl<T: Trait> Module<T> {
/// Get the para (chain or thread) ID assigned to a particular core or index, if any. Core indices
/// out of bounds will return `None`, as will indices of unassigned cores.
#[allow(unused)]
pub(crate) fn core_para(core_index: CoreIndex) -> Option<ParaId> {
let cores = AvailabilityCores::get();
match cores.get(core_index.0 as usize).and_then(|c| c.as_ref()) {
@@ -571,7 +578,6 @@ impl<T: Trait> Module<T> {
}
/// Get the validators in the given group, if the group index is valid for this session.
#[allow(unused)]
pub(crate) fn group_validators(group_index: GroupIndex) -> Option<Vec<ValidatorIndex>> {
ValidatorGroups::get().get(group_index.0 as usize).map(|g| g.clone())
}
@@ -615,10 +621,9 @@ impl<T: Trait> Module<T> {
/// timeouts, i.e. only within `max(config.chain_availability_period, config.thread_availability_period)`
/// of the last rotation would this return `Some`.
///
/// This really should not be a box, but is working around a compiler limitation described here:
/// https://users.rust-lang.org/t/cannot-unify-associated-type-in-impl-fn-with-concrete-type/44129
/// This really should not be a box, but is working around a compiler limitation filed here:
/// https://github.com/rust-lang/rust/issues/73226
/// which prevents us from testing the code if using `impl Trait`.
#[allow(unused)]
pub(crate) fn availability_timeout_predicate() -> Option<Box<dyn Fn(CoreIndex, T::BlockNumber) -> bool>> {
let now = <system::Module<T>>::block_number();
let config = <configuration::Module<T>>::config();