// 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 . //! 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 sp_runtime::traits::Header as HeaderT; use primitives::v1::{ BackedCandidate, PARACHAINS_INHERENT_IDENTIFIER, InherentData as ParachainsInherentData, }; use frame_support::{ decl_error, decl_module, decl_storage, ensure, dispatch::DispatchResultWithPostInfo, weights::{DispatchClass, Weight}, traits::Get, inherent::{InherentIdentifier, InherentData, MakeFatalError, ProvideInherent}, }; use frame_system::ensure_none; use crate::{ disputes::DisputesHandler, inclusion, scheduler::{self, FreedReason}, shared, ump, }; const LOG_TARGET: &str = "runtime::inclusion-inherent"; // In the future, we should benchmark these consts; these are all untested assumptions for now. const BACKED_CANDIDATE_WEIGHT: Weight = 100_000; const INCLUSION_INHERENT_CLAIMED_WEIGHT: Weight = 1_000_000_000; // we assume that 75% of an paras inherent's weight is used processing backed candidates const MINIMAL_INCLUSION_INHERENT_WEIGHT: Weight = INCLUSION_INHERENT_CLAIMED_WEIGHT / 4; pub trait Config: inclusion::Config + scheduler::Config {} decl_storage! { trait Store for Module as ParaInherent { /// Whether the paras 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 { /// Inclusion inherent called more than once per block. TooManyInclusionInherents, /// The hash of the submitted parent header doesn't correspond to the saved block hash of /// the parent. InvalidParentHeader, /// Potentially invalid candidate. CandidateCouldBeInvalid, } } decl_module! { /// The paras inherent module. pub struct Module for enum Call where origin: ::Origin { type Error = Error; 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"); } } /// Enter the paras inherent. This will process bitfields and backed candidates. #[weight = ( MINIMAL_INCLUSION_INHERENT_WEIGHT + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT, DispatchClass::Mandatory, )] pub fn enter( origin, data: ParachainsInherentData, ) -> DispatchResultWithPostInfo { let ParachainsInherentData { bitfields: signed_bitfields, backed_candidates, parent_header, disputes, } = data; ensure_none(origin)?; ensure!(!::exists(), Error::::TooManyInclusionInherents); // Check that the submitted parent header indeed corresponds to the previous block hash. let parent_hash = >::parent_hash(); ensure!( parent_header.hash().as_ref() == parent_hash.as_ref(), Error::::InvalidParentHeader, ); // Handle disputes logic. let current_session = >::session_index(); let freed_disputed: Vec<(_, FreedReason)> = { let fresh_disputes = T::DisputesHandler::provide_multi_dispute_data(disputes)?; if T::DisputesHandler::is_frozen() { // The relay chain we are currently on is invalid. Proceed no further on parachains. Included::set(Some(())); return Ok(Some( MINIMAL_INCLUSION_INHERENT_WEIGHT ).into()); } let any_current_session_disputes = fresh_disputes.iter() .any(|(s, _)| s == ¤t_session); if any_current_session_disputes { let current_session_disputes: Vec<_> = fresh_disputes.iter() .filter(|(s, _)| s == ¤t_session) .map(|(_, c)| *c) .collect(); >::collect_disputed(current_session_disputes) .into_iter() .map(|core| (core, FreedReason::Concluded)) .collect() } else { Vec::new() } }; // Process new availability bitfields, yielding any availability cores whose // work has now concluded. let expected_bits = >::availability_cores().len(); let freed_concluded = >::process_bitfields( expected_bits, signed_bitfields, >::core_para, )?; // Inform the disputes module of all included candidates. let now = >::block_number(); for (_, candidate_hash) in &freed_concluded { T::DisputesHandler::note_included(current_session, *candidate_hash, now); } // Handle timeouts for any availability core work. let availability_pred = >::availability_timeout_predicate(); let freed_timeout = if let Some(pred) = availability_pred { >::collect_pending(pred) } else { Vec::new() }; // Schedule paras again, given freed cores, and reasons for freeing. let mut freed = freed_disputed.into_iter() .chain(freed_concluded.into_iter().map(|(c, _hash)| (c, FreedReason::Concluded))) .chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut))) .collect::>(); freed.sort_unstable_by_key(|pair| pair.0); // sort by core index >::clear(); >::schedule( freed, >::block_number(), ); let backed_candidates = limit_backed_candidates::(backed_candidates); let backed_candidates_len = backed_candidates.len() as Weight; // Refuse to back any candidates that are disputed or invalid. for candidate in &backed_candidates { ensure!( !T::DisputesHandler::could_be_invalid( current_session, candidate.candidate.hash(), ), Error::::CandidateCouldBeInvalid, ); } // Process backed candidates according to scheduled cores. let parent_storage_root = parent_header.state_root().clone(); let occupied = >::process_candidates( parent_storage_root, backed_candidates, >::scheduled(), >::group_validators, )?; // Note which of the scheduled cores were actually occupied by a backed candidate. >::occupied(&occupied); // Give some time slice to dispatch pending upward messages. >::process_pending_upward_messages(); // And track that we've finished processing the inherent for this block. Included::set(Some(())); Ok(Some( MINIMAL_INCLUSION_INHERENT_WEIGHT + (backed_candidates_len * BACKED_CANDIDATE_WEIGHT) ).into()) } } } /// Limit the number of backed candidates processed in order to stay within block weight limits. /// /// Use a configured assumption about the weight required to process a backed candidate and the /// current block weight as of the execution of this function to ensure that we don't overload /// the block with candidate processing. /// /// If the backed candidates exceed the available block weight remaining, then skips all of them. /// This is somewhat less desirable than attempting to fit some of them, but is more fair in the /// even that we can't trust the provisioner to provide a fair / random ordering of candidates. fn limit_backed_candidates( mut backed_candidates: Vec>, ) -> Vec> { const MAX_CODE_UPGRADES: usize = 1; // Ignore any candidates beyond one that contain code upgrades. // // This is an artificial limitation that does not appear in the guide as it is a practical // concern around execution. { let mut code_upgrades = 0; backed_candidates.retain(|c| { if c.candidate.commitments.new_validation_code.is_some() { if code_upgrades >= MAX_CODE_UPGRADES { return false } code_upgrades +=1; } true }); } // the weight of the paras inherent is already included in the current block weight, // so our operation is simple: if the block is currently overloaded, make this intrinsic smaller if frame_system::Pallet::::block_weight().total() > ::BlockWeights::get().max_block { Vec::new() } else { backed_candidates } } impl ProvideInherent for Module { type Call = Call; type Error = MakeFatalError<()>; const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER; fn create_inherent(data: &InherentData) -> Option { let mut inherent_data: ParachainsInherentData = match data.get_data(&Self::INHERENT_IDENTIFIER) { Ok(Some(d)) => d, Ok(None) => return None, Err(_) => { log::warn!( target: LOG_TARGET, "ParachainsInherentData failed to decode", ); return None; } }; // filter out any unneeded dispute statements T::DisputesHandler::filter_multi_dispute_data(&mut inherent_data.disputes); // Sanity check: session changes can invalidate an inherent, and we _really_ don't want that to happen. // See github.com/paritytech/polkadot/issues/1327 let inherent_data = match Self::enter( frame_system::RawOrigin::None.into(), inherent_data.clone(), ) { Ok(_) => inherent_data, Err(err) => { log::warn!( target: LOG_TARGET, "dropping signed_bitfields and backed_candidates because they produced \ an invalid paras inherent: {:?}", err, ); ParachainsInherentData { bitfields: Vec::new(), backed_candidates: Vec::new(), disputes: Vec::new(), parent_header: inherent_data.parent_header, } } }; Some(Call::enter(inherent_data)) } fn is_inherent(call: &Self::Call) -> bool { matches!(call, Call::enter(..)) } } #[cfg(test)] mod tests { use super::*; use crate::mock::{ new_test_ext, System, MockGenesisConfig, Test }; mod limit_backed_candidates { use super::*; #[test] fn does_not_truncate_on_empty_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let backed_candidates = vec![BackedCandidate::default()]; System::set_block_consumed_resources(0, 0); assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); }); } #[test] fn does_not_truncate_on_exactly_full_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let backed_candidates = vec![BackedCandidate::default()]; let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight, 0); assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); }); } #[test] fn truncates_on_over_full_block() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let backed_candidates = vec![BackedCandidate::default()]; let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight + 1, 0); assert_eq!(limit_backed_candidates::(backed_candidates).len(), 0); }); } #[test] fn all_backed_candidates_get_truncated() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let backed_candidates = vec![BackedCandidate::default(); 10]; let max_block_weight = ::BlockWeights::get().max_block; // if the consumed resources are precisely equal to the max block weight, we do not truncate. System::set_block_consumed_resources(max_block_weight + 1, 0); assert_eq!(limit_backed_candidates::(backed_candidates).len(), 0); }); } #[test] fn ignores_subsequent_code_upgrades() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let mut backed = BackedCandidate::default(); backed.candidate.commitments.new_validation_code = Some(Vec::new().into()); let backed_candidates = (0..3).map(|_| backed.clone()).collect(); assert_eq!(limit_backed_candidates::(backed_candidates).len(), 1); }); } } mod paras_inherent_weight { use super::*; use crate::mock::{ new_test_ext, System, MockGenesisConfig, Test }; use primitives::v1::Header; use frame_support::traits::UnfilteredDispatchable; fn default_header() -> Header { Header { parent_hash: Default::default(), number: 0, state_root: Default::default(), extrinsics_root: Default::default(), digest: Default::default(), } } /// We expect the weight of the paras inherent not to change when no truncation occurs: /// its weight is dynamically computed from the size of the backed candidates list, and is /// already incorporated into the current block weight when it is selected by the provisioner. #[test] fn weight_does_not_change_on_happy_path() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let header = default_header(); System::set_block_number(1); System::set_parent_hash(header.hash()); // number of bitfields doesn't affect the paras inherent weight, so we can mock it with an empty one let signed_bitfields = Vec::new(); // backed candidates must not be empty, so we can demonstrate that the weight has not changed let backed_candidates = vec![BackedCandidate::default(); 10]; // the expected weight can always be computed by this formula let expected_weight = MINIMAL_INCLUSION_INHERENT_WEIGHT + (backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT); // we've used half the block weight; there's plenty of margin let max_block_weight = ::BlockWeights::get().max_block; let used_block_weight = max_block_weight / 2; System::set_block_consumed_resources(used_block_weight, 0); // execute the paras inherent let post_info = Call::::enter(ParachainsInherentData { bitfields: signed_bitfields, backed_candidates, disputes: Vec::new(), parent_header: default_header(), }) .dispatch_bypass_filter(None.into()).unwrap_err().post_info; // we don't directly check the block's weight post-call. Instead, we check that the // call has returned the appropriate post-dispatch weight for refund, and trust // Substrate to do the right thing with that information. // // In this case, the weight system can update the actual weight with the same amount, // or return `None` to indicate that the pre-computed weight should not change. // Either option is acceptable for our purposes. if let Some(actual_weight) = post_info.actual_weight { assert_eq!(actual_weight, expected_weight); } }); } /// We expect the weight of the paras inherent to change when truncation occurs: its /// weight was initially dynamically computed from the size of the backed candidates list, /// but was reduced by truncation. #[test] fn weight_changes_when_backed_candidates_are_truncated() { new_test_ext(MockGenesisConfig::default()).execute_with(|| { let header = default_header(); System::set_block_number(1); System::set_parent_hash(header.hash()); // number of bitfields doesn't affect the paras inherent weight, so we can mock it with an empty one let signed_bitfields = Vec::new(); // backed candidates must not be empty, so we can demonstrate that the weight has not changed let backed_candidates = vec![BackedCandidate::default(); 10]; // the expected weight with no blocks is just the minimum weight let expected_weight = MINIMAL_INCLUSION_INHERENT_WEIGHT; // oops, looks like this mandatory call pushed the block weight over the limit let max_block_weight = ::BlockWeights::get().max_block; let used_block_weight = max_block_weight + 1; System::set_block_consumed_resources(used_block_weight, 0); // execute the paras inherent let post_info = Call::::enter(ParachainsInherentData { bitfields: signed_bitfields, backed_candidates, disputes: Vec::new(), parent_header: header, }) .dispatch_bypass_filter(None.into()).unwrap(); // we don't directly check the block's weight post-call. Instead, we check that the // call has returned the appropriate post-dispatch weight for refund, and trust // Substrate to do the right thing with that information. assert_eq!( post_info.actual_weight.unwrap(), expected_weight, ); }); } } }