// 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. /// # Overview /// /// A set of utilities for node-side code to emulate the logic the runtime uses for checking /// parachain blocks in order to build prospective parachains that are produced ahead of the /// relay chain. These utilities allow the node-side to predict, with high accuracy, what /// the relay-chain will accept in the near future. /// /// This module has 2 key data types: [`Constraints`] and [`Fragment`]s. [`Constraints`] /// exhaustively define the set of valid inputs and outputs to parachain execution. A /// [`Fragment`] indicates a parachain block, anchored to the relay-chain at a particular /// relay-chain block, known as the relay-parent. /// /// ## Fragment Validity /// /// Every relay-parent is implicitly associated with a unique set of [`Constraints`] that /// describe the properties that must be true for a block to be included in a direct child of /// that block, assuming there is no intermediate parachain block pending availability. /// /// However, the key factor that makes asynchronously-grown prospective chains /// possible is the fact that the relay-chain accepts candidate blocks based on whether they /// are valid under the constraints of the present moment, not based on whether they were /// valid at the time of construction. /// /// As such, [`Fragment`]s are often, but not always constructed in such a way that they are /// invalid at first and become valid later on, as the relay chain grows. /// /// # Usage /// /// It's expected that the users of this module will be building up trees of /// [`Fragment`]s and consistently pruning and adding to the tree. /// /// ## Operating Constraints /// /// The *operating constraints* of a `Fragment` are the constraints with which that fragment /// was intended to comply. The operating constraints are defined as the base constraints /// of the relay-parent of the fragment modified by the cumulative modifications of all /// fragments between the relay-parent and the current fragment. /// /// What the operating constraints are, in practice, is a prediction about the state of the /// relay-chain in the future. The relay-chain is aware of some current state, and we want to /// make an intelligent prediction about what might be accepted in the future based on /// prior fragments that also exist off-chain. /// /// ## Fragment Trees /// /// As the relay-chain grows, some predictions come true and others come false. /// And new predictions get made. These three changes correspond distinctly to the /// 3 primary operations on fragment trees. /// /// A fragment tree is a mental model for thinking about a forking series of predictions /// about a single parachain. There may be one or more fragment trees per parachain. /// /// In expectation, most parachains will have a plausibly-unique authorship method which means /// that they should really be much closer to fragment-chains, maybe with an occasional fork. /// /// Avoiding fragment-tree blowup is beyond the scope of this module. /// /// ### Pruning Fragment Trees /// /// When the relay-chain advances, we want to compare the new constraints of that relay-parent /// to the roots of the fragment trees we have. There are 3 cases: /// /// 1. The root fragment is still valid under the new constraints. In this case, we do nothing. /// This is the "prediction still uncertain" case. /// /// 2. The root fragment is invalid under the new constraints because it has been subsumed by /// the relay-chain. In this case, we can discard the root and split & re-root the fragment /// tree under its descendents and compare to the new constraints again. This is the /// "prediction came true" case. /// /// 3. The root fragment is invalid under the new constraints because a competing parachain /// block has been included or it would never be accepted for some other reason. In this /// case we can discard the entire fragment tree. This is the "prediction came false" case. /// /// This is all a bit of a simplification because it assumes that the relay-chain advances /// without forks and is finalized instantly. In practice, the set of fragment-trees needs to /// be observable from the perspective of a few different possible forks of the relay-chain and /// not pruned too eagerly. /// /// Note that the fragments themselves don't need to change and the only thing we care about /// is whether the predictions they represent are still valid. /// /// ### Extending Fragment Trees /// /// As predictions fade into the past, new ones should be stacked on top. /// /// Every new relay-chain block is an opportunity to make a new prediction about the future. /// Higher-level logic should select the leaves of the fragment-trees to build upon or whether /// to create a new fragment-tree. /// /// ### Code Upgrades /// /// Code upgrades are the main place where this emulation fails. The on-chain PVF upgrade /// scheduling logic is very path-dependent and intricate so we just assume that code upgrades /// can't be initiated and applied within a single fragment-tree. Fragment-trees aren't deep, /// in practice and code upgrades are fairly rare. So what's likely to happen around code /// upgrades is that the entire fragment-tree has to get discarded at some point. /// /// That means a few blocks of execution time lost, which is not a big deal for code upgrades /// in practice at most once every few weeks. use polkadot_primitives::{ async_backing::Constraints as PrimitiveConstraints, BlockNumber, CandidateCommitments, CollatorId, CollatorSignature, Hash, HeadData, Id as ParaId, PersistedValidationData, UpgradeRestriction, ValidationCodeHash, }; use std::{ borrow::{Borrow, Cow}, collections::HashMap, }; /// Constraints on inbound HRMP channels. #[derive(Debug, Clone, PartialEq)] pub struct InboundHrmpLimitations { /// An exhaustive set of all valid watermarks, sorted ascending pub valid_watermarks: Vec, } /// Constraints on outbound HRMP channels. #[derive(Debug, Clone, PartialEq)] pub struct OutboundHrmpChannelLimitations { /// The maximum bytes that can be written to the channel. pub bytes_remaining: usize, /// The maximum messages that can be written to the channel. pub messages_remaining: usize, } /// Constraints on the actions that can be taken by a new parachain /// block. These limitations are implicitly associated with some particular /// parachain, which should be apparent from usage. #[derive(Debug, Clone, PartialEq)] pub struct Constraints { /// The minimum relay-parent number accepted under these constraints. pub min_relay_parent_number: BlockNumber, /// The maximum Proof-of-Validity size allowed, in bytes. pub max_pov_size: usize, /// The maximum new validation code size allowed, in bytes. pub max_code_size: usize, /// The amount of UMP messages remaining. pub ump_remaining: usize, /// The amount of UMP bytes remaining. pub ump_remaining_bytes: usize, /// The maximum number of UMP messages allowed per candidate. pub max_ump_num_per_candidate: usize, /// Remaining DMP queue. Only includes sent-at block numbers. pub dmp_remaining_messages: Vec, /// The limitations of all registered inbound HRMP channels. pub hrmp_inbound: InboundHrmpLimitations, /// The limitations of all registered outbound HRMP channels. pub hrmp_channels_out: HashMap, /// The maximum number of HRMP messages allowed per candidate. pub max_hrmp_num_per_candidate: usize, /// The required parent head-data of the parachain. pub required_parent: HeadData, /// The expected validation-code-hash of this parachain. pub validation_code_hash: ValidationCodeHash, /// The code upgrade restriction signal as-of this parachain. pub upgrade_restriction: Option, /// The future validation code hash, if any, and at what relay-parent /// number the upgrade would be minimally applied. pub future_validation_code: Option<(BlockNumber, ValidationCodeHash)>, } impl From for Constraints { fn from(c: PrimitiveConstraints) -> Self { Constraints { min_relay_parent_number: c.min_relay_parent_number, max_pov_size: c.max_pov_size as _, max_code_size: c.max_code_size as _, ump_remaining: c.ump_remaining as _, ump_remaining_bytes: c.ump_remaining_bytes as _, max_ump_num_per_candidate: c.max_ump_num_per_candidate as _, dmp_remaining_messages: c.dmp_remaining_messages, hrmp_inbound: InboundHrmpLimitations { valid_watermarks: c.hrmp_inbound.valid_watermarks, }, hrmp_channels_out: c .hrmp_channels_out .into_iter() .map(|(para_id, limits)| { ( para_id, OutboundHrmpChannelLimitations { bytes_remaining: limits.bytes_remaining as _, messages_remaining: limits.messages_remaining as _, }, ) }) .collect(), max_hrmp_num_per_candidate: c.max_hrmp_num_per_candidate as _, required_parent: c.required_parent, validation_code_hash: c.validation_code_hash, upgrade_restriction: c.upgrade_restriction, future_validation_code: c.future_validation_code, } } } /// Kinds of errors that can occur when modifying constraints. #[derive(Debug, Clone, PartialEq)] pub enum ModificationError { /// The HRMP watermark is not allowed. DisallowedHrmpWatermark(BlockNumber), /// No such HRMP outbound channel. NoSuchHrmpChannel(ParaId), /// Too many messages submitted to HRMP channel. HrmpMessagesOverflow { /// The ID of the recipient. para_id: ParaId, /// The amount of remaining messages in the capacity of the channel. messages_remaining: usize, /// The amount of messages submitted to the channel. messages_submitted: usize, }, /// Too many bytes submitted to HRMP channel. HrmpBytesOverflow { /// The ID of the recipient. para_id: ParaId, /// The amount of remaining bytes in the capacity of the channel. bytes_remaining: usize, /// The amount of bytes submitted to the channel. bytes_submitted: usize, }, /// Too many messages submitted to UMP. UmpMessagesOverflow { /// The amount of remaining messages in the capacity of UMP. messages_remaining: usize, /// The amount of messages submitted to UMP. messages_submitted: usize, }, /// Too many bytes submitted to UMP. UmpBytesOverflow { /// The amount of remaining bytes in the capacity of UMP. bytes_remaining: usize, /// The amount of bytes submitted to UMP. bytes_submitted: usize, }, /// Too many messages processed from DMP. DmpMessagesUnderflow { /// The amount of messages waiting to be processed from DMP. messages_remaining: usize, /// The amount of messages processed. messages_processed: usize, }, /// No validation code upgrade to apply. AppliedNonexistentCodeUpgrade, } impl Constraints { /// Check modifications against constraints. pub fn check_modifications( &self, modifications: &ConstraintModifications, ) -> Result<(), ModificationError> { if let Some(HrmpWatermarkUpdate::Trunk(hrmp_watermark)) = modifications.hrmp_watermark { // head updates are always valid. if self.hrmp_inbound.valid_watermarks.iter().all(|w| w != &hrmp_watermark) { return Err(ModificationError::DisallowedHrmpWatermark(hrmp_watermark)) } } for (id, outbound_hrmp_mod) in &modifications.outbound_hrmp { if let Some(outbound) = self.hrmp_channels_out.get(&id) { outbound.bytes_remaining.checked_sub(outbound_hrmp_mod.bytes_submitted).ok_or( ModificationError::HrmpBytesOverflow { para_id: *id, bytes_remaining: outbound.bytes_remaining, bytes_submitted: outbound_hrmp_mod.bytes_submitted, }, )?; outbound .messages_remaining .checked_sub(outbound_hrmp_mod.messages_submitted) .ok_or(ModificationError::HrmpMessagesOverflow { para_id: *id, messages_remaining: outbound.messages_remaining, messages_submitted: outbound_hrmp_mod.messages_submitted, })?; } else { return Err(ModificationError::NoSuchHrmpChannel(*id)) } } self.ump_remaining.checked_sub(modifications.ump_messages_sent).ok_or( ModificationError::UmpMessagesOverflow { messages_remaining: self.ump_remaining, messages_submitted: modifications.ump_messages_sent, }, )?; self.ump_remaining_bytes.checked_sub(modifications.ump_bytes_sent).ok_or( ModificationError::UmpBytesOverflow { bytes_remaining: self.ump_remaining_bytes, bytes_submitted: modifications.ump_bytes_sent, }, )?; self.dmp_remaining_messages .len() .checked_sub(modifications.dmp_messages_processed) .ok_or(ModificationError::DmpMessagesUnderflow { messages_remaining: self.dmp_remaining_messages.len(), messages_processed: modifications.dmp_messages_processed, })?; if self.future_validation_code.is_none() && modifications.code_upgrade_applied { return Err(ModificationError::AppliedNonexistentCodeUpgrade) } Ok(()) } /// Apply modifications to these constraints. If this succeeds, it passes /// all sanity-checks. pub fn apply_modifications( &self, modifications: &ConstraintModifications, ) -> Result { let mut new = self.clone(); if let Some(required_parent) = modifications.required_parent.as_ref() { new.required_parent = required_parent.clone(); } if let Some(ref hrmp_watermark) = modifications.hrmp_watermark { match new.hrmp_inbound.valid_watermarks.binary_search(&hrmp_watermark.watermark()) { Ok(pos) => { // Exact match, so this is OK in all cases. let _ = new.hrmp_inbound.valid_watermarks.drain(..pos + 1); }, Err(pos) => match hrmp_watermark { HrmpWatermarkUpdate::Head(_) => { // Updates to Head are always OK. let _ = new.hrmp_inbound.valid_watermarks.drain(..pos); }, HrmpWatermarkUpdate::Trunk(n) => { // Trunk update landing on disallowed watermark is not OK. return Err(ModificationError::DisallowedHrmpWatermark(*n)) }, }, } } for (id, outbound_hrmp_mod) in &modifications.outbound_hrmp { if let Some(outbound) = new.hrmp_channels_out.get_mut(&id) { outbound.bytes_remaining = outbound .bytes_remaining .checked_sub(outbound_hrmp_mod.bytes_submitted) .ok_or(ModificationError::HrmpBytesOverflow { para_id: *id, bytes_remaining: outbound.bytes_remaining, bytes_submitted: outbound_hrmp_mod.bytes_submitted, })?; outbound.messages_remaining = outbound .messages_remaining .checked_sub(outbound_hrmp_mod.messages_submitted) .ok_or(ModificationError::HrmpMessagesOverflow { para_id: *id, messages_remaining: outbound.messages_remaining, messages_submitted: outbound_hrmp_mod.messages_submitted, })?; } else { return Err(ModificationError::NoSuchHrmpChannel(*id)) } } new.ump_remaining = new.ump_remaining.checked_sub(modifications.ump_messages_sent).ok_or( ModificationError::UmpMessagesOverflow { messages_remaining: new.ump_remaining, messages_submitted: modifications.ump_messages_sent, }, )?; new.ump_remaining_bytes = new .ump_remaining_bytes .checked_sub(modifications.ump_bytes_sent) .ok_or(ModificationError::UmpBytesOverflow { bytes_remaining: new.ump_remaining_bytes, bytes_submitted: modifications.ump_bytes_sent, })?; if modifications.dmp_messages_processed > new.dmp_remaining_messages.len() { return Err(ModificationError::DmpMessagesUnderflow { messages_remaining: new.dmp_remaining_messages.len(), messages_processed: modifications.dmp_messages_processed, }) } else { new.dmp_remaining_messages = new.dmp_remaining_messages[modifications.dmp_messages_processed..].to_vec(); } if modifications.code_upgrade_applied { new.validation_code_hash = new .future_validation_code .take() .ok_or(ModificationError::AppliedNonexistentCodeUpgrade)? .1; } Ok(new) } } /// Information about a relay-chain block. #[derive(Debug, Clone, PartialEq)] pub struct RelayChainBlockInfo { /// The hash of the relay-chain block. pub hash: Hash, /// The number of the relay-chain block. pub number: BlockNumber, /// The storage-root of the relay-chain block. pub storage_root: Hash, } /// An update to outbound HRMP channels. #[derive(Debug, Clone, PartialEq, Default)] pub struct OutboundHrmpChannelModification { /// The number of bytes submitted to the channel. pub bytes_submitted: usize, /// The number of messages submitted to the channel. pub messages_submitted: usize, } /// An update to the HRMP Watermark. #[derive(Debug, Clone, PartialEq)] pub enum HrmpWatermarkUpdate { /// This is an update placing the watermark at the head of the chain, /// which is always legal. Head(BlockNumber), /// This is an update placing the watermark behind the head of the /// chain, which is only legal if it lands on a block where messages /// were queued. Trunk(BlockNumber), } impl HrmpWatermarkUpdate { fn watermark(&self) -> BlockNumber { match *self { HrmpWatermarkUpdate::Head(n) | HrmpWatermarkUpdate::Trunk(n) => n, } } } /// Modifications to constraints as a result of prospective candidates. #[derive(Debug, Clone, PartialEq)] pub struct ConstraintModifications { /// The required parent head to build upon. pub required_parent: Option, /// The new HRMP watermark pub hrmp_watermark: Option, /// Outbound HRMP channel modifications. pub outbound_hrmp: HashMap, /// The amount of UMP messages sent. pub ump_messages_sent: usize, /// The amount of UMP bytes sent. pub ump_bytes_sent: usize, /// The amount of DMP messages processed. pub dmp_messages_processed: usize, /// Whether a pending code upgrade has been applied. pub code_upgrade_applied: bool, } impl ConstraintModifications { /// The 'identity' modifications: these can be applied to /// any constraints and yield the exact same result. pub fn identity() -> Self { ConstraintModifications { required_parent: None, hrmp_watermark: None, outbound_hrmp: HashMap::new(), ump_messages_sent: 0, ump_bytes_sent: 0, dmp_messages_processed: 0, code_upgrade_applied: false, } } /// Stack other modifications on top of these. /// /// This does no sanity-checking, so if `other` is garbage relative /// to `self`, then the new value will be garbage as well. /// /// This is an addition which is not commutative. pub fn stack(&mut self, other: &Self) { if let Some(ref new_parent) = other.required_parent { self.required_parent = Some(new_parent.clone()); } if let Some(ref new_hrmp_watermark) = other.hrmp_watermark { self.hrmp_watermark = Some(new_hrmp_watermark.clone()); } for (id, mods) in &other.outbound_hrmp { let record = self.outbound_hrmp.entry(*id).or_default(); record.messages_submitted += mods.messages_submitted; record.bytes_submitted += mods.bytes_submitted; } self.ump_messages_sent += other.ump_messages_sent; self.ump_bytes_sent += other.ump_bytes_sent; self.dmp_messages_processed += other.dmp_messages_processed; self.code_upgrade_applied |= other.code_upgrade_applied; } } /// The prospective candidate. /// /// This comprises the key information that represent a candidate /// without pinning it to a particular session. For example, everything /// to do with the collator's signature and commitments are represented /// here. But the erasure-root is not. This means that prospective candidates /// are not correlated to any session in particular. #[derive(Debug, Clone, PartialEq)] pub struct ProspectiveCandidate<'a> { /// The commitments to the output of the execution. pub commitments: Cow<'a, CandidateCommitments>, /// The collator that created the candidate. pub collator: CollatorId, /// The signature of the collator on the payload. pub collator_signature: CollatorSignature, /// The persisted validation data used to create the candidate. pub persisted_validation_data: PersistedValidationData, /// The hash of the PoV. pub pov_hash: Hash, /// The validation code hash used by the candidate. pub validation_code_hash: ValidationCodeHash, } impl<'a> ProspectiveCandidate<'a> { fn into_owned(self) -> ProspectiveCandidate<'static> { ProspectiveCandidate { commitments: Cow::Owned(self.commitments.into_owned()), ..self } } /// Partially clone the prospective candidate, but borrow the /// parts which are potentially heavy. pub fn partial_clone(&self) -> ProspectiveCandidate { ProspectiveCandidate { commitments: Cow::Borrowed(self.commitments.borrow()), collator: self.collator.clone(), collator_signature: self.collator_signature.clone(), persisted_validation_data: self.persisted_validation_data.clone(), pov_hash: self.pov_hash, validation_code_hash: self.validation_code_hash, } } } #[cfg(test)] impl ProspectiveCandidate<'static> { fn commitments_mut(&mut self) -> &mut CandidateCommitments { self.commitments.to_mut() } } /// Kinds of errors with the validity of a fragment. #[derive(Debug, Clone, PartialEq)] pub enum FragmentValidityError { /// The validation code of the candidate doesn't match the /// operating constraints. /// /// Expected, Got ValidationCodeMismatch(ValidationCodeHash, ValidationCodeHash), /// The persisted-validation-data doesn't match. /// /// Expected, Got PersistedValidationDataMismatch(PersistedValidationData, PersistedValidationData), /// The outputs of the candidate are invalid under the operating /// constraints. OutputsInvalid(ModificationError), /// New validation code size too big. /// /// Max allowed, new. CodeSizeTooLarge(usize, usize), /// Relay parent too old. /// /// Min allowed, current. RelayParentTooOld(BlockNumber, BlockNumber), /// Para is required to process at least one DMP message from the queue. DmpAdvancementRule, /// Too many messages upward messages submitted. UmpMessagesPerCandidateOverflow { /// The amount of messages a single candidate can submit. messages_allowed: usize, /// The amount of messages sent to all HRMP channels. messages_submitted: usize, }, /// Too many messages submitted to all HRMP channels. HrmpMessagesPerCandidateOverflow { /// The amount of messages a single candidate can submit. messages_allowed: usize, /// The amount of messages sent to all HRMP channels. messages_submitted: usize, }, /// Code upgrade not allowed. CodeUpgradeRestricted, /// HRMP messages are not ascending or are duplicate. /// /// The `usize` is the index into the outbound HRMP messages of /// the candidate. HrmpMessagesDescendingOrDuplicate(usize), } /// A parachain fragment, representing another prospective parachain block. /// /// This is a type which guarantees that the candidate is valid under the /// operating constraints. #[derive(Debug, Clone, PartialEq)] pub struct Fragment<'a> { /// The new relay-parent. relay_parent: RelayChainBlockInfo, /// The constraints this fragment is operating under. operating_constraints: Constraints, /// The core information about the prospective candidate. candidate: ProspectiveCandidate<'a>, /// Modifications to the constraints based on the outputs of /// the candidate. modifications: ConstraintModifications, } impl<'a> Fragment<'a> { /// Create a new fragment. /// /// This fails if the fragment isn't in line with the operating /// constraints. That is, either its inputs or its outputs fail /// checks against the constraints. /// /// This doesn't check that the collator signature is valid or /// whether the PoV is small enough. pub fn new( relay_parent: RelayChainBlockInfo, operating_constraints: Constraints, candidate: ProspectiveCandidate<'a>, ) -> Result { let modifications = { let commitments = &candidate.commitments; ConstraintModifications { required_parent: Some(commitments.head_data.clone()), hrmp_watermark: Some({ if commitments.hrmp_watermark == relay_parent.number { HrmpWatermarkUpdate::Head(commitments.hrmp_watermark) } else { HrmpWatermarkUpdate::Trunk(commitments.hrmp_watermark) } }), outbound_hrmp: { let mut outbound_hrmp = HashMap::<_, OutboundHrmpChannelModification>::new(); let mut last_recipient = None::; for (i, message) in commitments.horizontal_messages.iter().enumerate() { if let Some(last) = last_recipient { if last >= message.recipient { return Err( FragmentValidityError::HrmpMessagesDescendingOrDuplicate(i), ) } } last_recipient = Some(message.recipient); let record = outbound_hrmp.entry(message.recipient).or_default(); record.bytes_submitted += message.data.len(); record.messages_submitted += 1; } outbound_hrmp }, ump_messages_sent: commitments.upward_messages.len(), ump_bytes_sent: commitments.upward_messages.iter().map(|msg| msg.len()).sum(), dmp_messages_processed: commitments.processed_downward_messages as _, code_upgrade_applied: operating_constraints .future_validation_code .map_or(false, |(at, _)| relay_parent.number >= at), } }; validate_against_constraints( &operating_constraints, &relay_parent, &candidate, &modifications, )?; Ok(Fragment { relay_parent, operating_constraints, candidate, modifications }) } /// Access the relay parent information. pub fn relay_parent(&self) -> &RelayChainBlockInfo { &self.relay_parent } /// Access the operating constraints pub fn operating_constraints(&self) -> &Constraints { &self.operating_constraints } /// Access the underlying prospective candidate. pub fn candidate(&self) -> &ProspectiveCandidate<'a> { &self.candidate } /// Modifications to constraints based on the outputs of the candidate. pub fn constraint_modifications(&self) -> &ConstraintModifications { &self.modifications } /// Convert the fragment into an owned variant. pub fn into_owned(self) -> Fragment<'static> { Fragment { candidate: self.candidate.into_owned(), ..self } } /// Validate this fragment against some set of constraints /// instead of the operating constraints. pub fn validate_against_constraints( &self, constraints: &Constraints, ) -> Result<(), FragmentValidityError> { validate_against_constraints( constraints, &self.relay_parent, &self.candidate, &self.modifications, ) } } fn validate_against_constraints( constraints: &Constraints, relay_parent: &RelayChainBlockInfo, candidate: &ProspectiveCandidate, modifications: &ConstraintModifications, ) -> Result<(), FragmentValidityError> { let expected_pvd = PersistedValidationData { parent_head: constraints.required_parent.clone(), relay_parent_number: relay_parent.number, relay_parent_storage_root: relay_parent.storage_root, max_pov_size: constraints.max_pov_size as u32, }; if expected_pvd != candidate.persisted_validation_data { return Err(FragmentValidityError::PersistedValidationDataMismatch( expected_pvd, candidate.persisted_validation_data.clone(), )) } if constraints.validation_code_hash != candidate.validation_code_hash { return Err(FragmentValidityError::ValidationCodeMismatch( constraints.validation_code_hash, candidate.validation_code_hash, )) } if relay_parent.number < constraints.min_relay_parent_number { return Err(FragmentValidityError::RelayParentTooOld( constraints.min_relay_parent_number, relay_parent.number, )) } if candidate.commitments.new_validation_code.is_some() { match constraints.upgrade_restriction { None => {}, Some(UpgradeRestriction::Present) => return Err(FragmentValidityError::CodeUpgradeRestricted), } } let announced_code_size = candidate .commitments .new_validation_code .as_ref() .map_or(0, |code| code.0.len()); if announced_code_size > constraints.max_code_size { return Err(FragmentValidityError::CodeSizeTooLarge( constraints.max_code_size, announced_code_size, )) } if modifications.dmp_messages_processed == 0 { if constraints .dmp_remaining_messages .get(0) .map_or(false, |&msg_sent_at| msg_sent_at <= relay_parent.number) { return Err(FragmentValidityError::DmpAdvancementRule) } } if candidate.commitments.horizontal_messages.len() > constraints.max_hrmp_num_per_candidate { return Err(FragmentValidityError::HrmpMessagesPerCandidateOverflow { messages_allowed: constraints.max_hrmp_num_per_candidate, messages_submitted: candidate.commitments.horizontal_messages.len(), }) } if candidate.commitments.upward_messages.len() > constraints.max_ump_num_per_candidate { return Err(FragmentValidityError::UmpMessagesPerCandidateOverflow { messages_allowed: constraints.max_ump_num_per_candidate, messages_submitted: candidate.commitments.upward_messages.len(), }) } constraints .check_modifications(&modifications) .map_err(FragmentValidityError::OutputsInvalid) } #[cfg(test)] mod tests { use super::*; use polkadot_primitives::{ CollatorPair, HorizontalMessages, OutboundHrmpMessage, ValidationCode, }; use sp_application_crypto::Pair; #[test] fn stack_modifications() { let para_a = ParaId::from(1u32); let para_b = ParaId::from(2u32); let para_c = ParaId::from(3u32); let a = ConstraintModifications { required_parent: None, hrmp_watermark: None, outbound_hrmp: { let mut map = HashMap::new(); map.insert( para_a, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5 }, ); map.insert( para_b, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5 }, ); map }, ump_messages_sent: 6, ump_bytes_sent: 1000, dmp_messages_processed: 5, code_upgrade_applied: true, }; let b = ConstraintModifications { required_parent: None, hrmp_watermark: None, outbound_hrmp: { let mut map = HashMap::new(); map.insert( para_b, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5 }, ); map.insert( para_c, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5 }, ); map }, ump_messages_sent: 6, ump_bytes_sent: 1000, dmp_messages_processed: 5, code_upgrade_applied: true, }; let mut c = a.clone(); c.stack(&b); assert_eq!( c, ConstraintModifications { required_parent: None, hrmp_watermark: None, outbound_hrmp: { let mut map = HashMap::new(); map.insert( para_a, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5, }, ); map.insert( para_b, OutboundHrmpChannelModification { bytes_submitted: 200, messages_submitted: 10, }, ); map.insert( para_c, OutboundHrmpChannelModification { bytes_submitted: 100, messages_submitted: 5, }, ); map }, ump_messages_sent: 12, ump_bytes_sent: 2000, dmp_messages_processed: 10, code_upgrade_applied: true, }, ); let mut d = ConstraintModifications::identity(); d.stack(&a); d.stack(&b); assert_eq!(c, d); } fn make_constraints() -> Constraints { let para_a = ParaId::from(1u32); let para_b = ParaId::from(2u32); let para_c = ParaId::from(3u32); Constraints { min_relay_parent_number: 5, max_pov_size: 1000, max_code_size: 1000, ump_remaining: 10, ump_remaining_bytes: 1024, max_ump_num_per_candidate: 5, dmp_remaining_messages: Vec::new(), hrmp_inbound: InboundHrmpLimitations { valid_watermarks: vec![6, 8] }, hrmp_channels_out: { let mut map = HashMap::new(); map.insert( para_a, OutboundHrmpChannelLimitations { messages_remaining: 5, bytes_remaining: 512 }, ); map.insert( para_b, OutboundHrmpChannelLimitations { messages_remaining: 10, bytes_remaining: 1024, }, ); map.insert( para_c, OutboundHrmpChannelLimitations { messages_remaining: 1, bytes_remaining: 128 }, ); map }, max_hrmp_num_per_candidate: 5, required_parent: HeadData::from(vec![1, 2, 3]), validation_code_hash: ValidationCode(vec![4, 5, 6]).hash(), upgrade_restriction: None, future_validation_code: None, } } #[test] fn constraints_disallowed_trunk_watermark() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); modifications.hrmp_watermark = Some(HrmpWatermarkUpdate::Trunk(7)); assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::DisallowedHrmpWatermark(7)), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::DisallowedHrmpWatermark(7)), ); } #[test] fn constraints_always_allow_head_watermark() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); modifications.hrmp_watermark = Some(HrmpWatermarkUpdate::Head(7)); assert!(constraints.check_modifications(&modifications).is_ok()); let new_constraints = constraints.apply_modifications(&modifications).unwrap(); assert_eq!(new_constraints.hrmp_inbound.valid_watermarks, vec![8]); } #[test] fn constraints_no_such_hrmp_channel() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); let bad_para = ParaId::from(100u32); modifications.outbound_hrmp.insert( bad_para, OutboundHrmpChannelModification { bytes_submitted: 0, messages_submitted: 0 }, ); assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::NoSuchHrmpChannel(bad_para)), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::NoSuchHrmpChannel(bad_para)), ); } #[test] fn constraints_hrmp_messages_overflow() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); let para_a = ParaId::from(1u32); modifications.outbound_hrmp.insert( para_a, OutboundHrmpChannelModification { bytes_submitted: 0, messages_submitted: 6 }, ); assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::HrmpMessagesOverflow { para_id: para_a, messages_remaining: 5, messages_submitted: 6, }), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::HrmpMessagesOverflow { para_id: para_a, messages_remaining: 5, messages_submitted: 6, }), ); } #[test] fn constraints_hrmp_bytes_overflow() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); let para_a = ParaId::from(1u32); modifications.outbound_hrmp.insert( para_a, OutboundHrmpChannelModification { bytes_submitted: 513, messages_submitted: 1 }, ); assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::HrmpBytesOverflow { para_id: para_a, bytes_remaining: 512, bytes_submitted: 513, }), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::HrmpBytesOverflow { para_id: para_a, bytes_remaining: 512, bytes_submitted: 513, }), ); } #[test] fn constraints_ump_messages_overflow() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); modifications.ump_messages_sent = 11; assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::UmpMessagesOverflow { messages_remaining: 10, messages_submitted: 11, }), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::UmpMessagesOverflow { messages_remaining: 10, messages_submitted: 11, }), ); } #[test] fn constraints_ump_bytes_overflow() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); modifications.ump_bytes_sent = 1025; assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::UmpBytesOverflow { bytes_remaining: 1024, bytes_submitted: 1025, }), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::UmpBytesOverflow { bytes_remaining: 1024, bytes_submitted: 1025, }), ); } #[test] fn constraints_dmp_messages() { let mut constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); assert!(constraints.check_modifications(&modifications).is_ok()); assert!(constraints.apply_modifications(&modifications).is_ok()); modifications.dmp_messages_processed = 6; assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::DmpMessagesUnderflow { messages_remaining: 0, messages_processed: 6, }), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::DmpMessagesUnderflow { messages_remaining: 0, messages_processed: 6, }), ); constraints.dmp_remaining_messages = vec![1, 4, 8, 10]; modifications.dmp_messages_processed = 2; assert!(constraints.check_modifications(&modifications).is_ok()); let constraints = constraints .apply_modifications(&modifications) .expect("modifications are valid"); assert_eq!(&constraints.dmp_remaining_messages, &[8, 10]); } #[test] fn constraints_nonexistent_code_upgrade() { let constraints = make_constraints(); let mut modifications = ConstraintModifications::identity(); modifications.code_upgrade_applied = true; assert_eq!( constraints.check_modifications(&modifications), Err(ModificationError::AppliedNonexistentCodeUpgrade), ); assert_eq!( constraints.apply_modifications(&modifications), Err(ModificationError::AppliedNonexistentCodeUpgrade), ); } fn make_candidate( constraints: &Constraints, relay_parent: &RelayChainBlockInfo, ) -> ProspectiveCandidate<'static> { let collator_pair = CollatorPair::generate().0; let collator = collator_pair.public(); let sig = collator_pair.sign(b"blabla".as_slice()); ProspectiveCandidate { commitments: Cow::Owned(CandidateCommitments { upward_messages: Default::default(), horizontal_messages: Default::default(), new_validation_code: None, head_data: HeadData::from(vec![1, 2, 3, 4, 5]), processed_downward_messages: 0, hrmp_watermark: relay_parent.number, }), collator, collator_signature: sig, persisted_validation_data: PersistedValidationData { parent_head: constraints.required_parent.clone(), relay_parent_number: relay_parent.number, relay_parent_storage_root: relay_parent.storage_root, max_pov_size: constraints.max_pov_size as u32, }, pov_hash: Hash::repeat_byte(1), validation_code_hash: constraints.validation_code_hash, } } #[test] fn fragment_validation_code_mismatch() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); let expected_code = constraints.validation_code_hash; let got_code = ValidationCode(vec![9, 9, 9]).hash(); candidate.validation_code_hash = got_code; assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::ValidationCodeMismatch(expected_code, got_code,)), ) } #[test] fn fragment_pvd_mismatch() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let relay_parent_b = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0b), storage_root: Hash::repeat_byte(0xee), }; let constraints = make_constraints(); let candidate = make_candidate(&constraints, &relay_parent); let expected_pvd = PersistedValidationData { parent_head: constraints.required_parent.clone(), relay_parent_number: relay_parent_b.number, relay_parent_storage_root: relay_parent_b.storage_root, max_pov_size: constraints.max_pov_size as u32, }; let got_pvd = candidate.persisted_validation_data.clone(); assert_eq!( Fragment::new(relay_parent_b, constraints, candidate), Err(FragmentValidityError::PersistedValidationDataMismatch(expected_pvd, got_pvd,)), ); } #[test] fn fragment_code_size_too_large() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); let max_code_size = constraints.max_code_size; candidate.commitments_mut().new_validation_code = Some(vec![0; max_code_size + 1].into()); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::CodeSizeTooLarge(max_code_size, max_code_size + 1,)), ); } #[test] fn fragment_relay_parent_too_old() { let relay_parent = RelayChainBlockInfo { number: 3, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let candidate = make_candidate(&constraints, &relay_parent); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::RelayParentTooOld(5, 3,)), ); } #[test] fn fragment_hrmp_messages_overflow() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); let max_hrmp = constraints.max_hrmp_num_per_candidate; candidate .commitments_mut() .horizontal_messages .try_extend((0..max_hrmp + 1).map(|i| OutboundHrmpMessage { recipient: ParaId::from(i as u32), data: vec![1, 2, 3], })) .unwrap(); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::HrmpMessagesPerCandidateOverflow { messages_allowed: max_hrmp, messages_submitted: max_hrmp + 1, }), ); } #[test] fn fragment_dmp_advancement_rule() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let mut constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); // Empty dmp queue is ok. assert!(Fragment::new(relay_parent.clone(), constraints.clone(), candidate.clone()).is_ok()); // Unprocessed message that was sent later is ok. constraints.dmp_remaining_messages = vec![relay_parent.number + 1]; assert!(Fragment::new(relay_parent.clone(), constraints.clone(), candidate.clone()).is_ok()); for block_number in 0..=relay_parent.number { constraints.dmp_remaining_messages = vec![block_number]; assert_eq!( Fragment::new(relay_parent.clone(), constraints.clone(), candidate.clone()), Err(FragmentValidityError::DmpAdvancementRule), ); } candidate.commitments.to_mut().processed_downward_messages = 1; assert!(Fragment::new(relay_parent, constraints, candidate).is_ok()); } #[test] fn fragment_ump_messages_overflow() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); let max_ump = constraints.max_ump_num_per_candidate; candidate .commitments .to_mut() .upward_messages .try_extend((0..max_ump + 1).map(|i| vec![i as u8])) .unwrap(); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::UmpMessagesPerCandidateOverflow { messages_allowed: max_ump, messages_submitted: max_ump + 1, }), ); } #[test] fn fragment_code_upgrade_restricted() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let mut constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); constraints.upgrade_restriction = Some(UpgradeRestriction::Present); candidate.commitments_mut().new_validation_code = Some(ValidationCode(vec![1, 2, 3])); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::CodeUpgradeRestricted), ); } #[test] fn fragment_hrmp_messages_descending_or_duplicate() { let relay_parent = RelayChainBlockInfo { number: 6, hash: Hash::repeat_byte(0x0a), storage_root: Hash::repeat_byte(0xff), }; let constraints = make_constraints(); let mut candidate = make_candidate(&constraints, &relay_parent); candidate.commitments_mut().horizontal_messages = HorizontalMessages::truncate_from(vec![ OutboundHrmpMessage { recipient: ParaId::from(0 as u32), data: vec![1, 2, 3] }, OutboundHrmpMessage { recipient: ParaId::from(0 as u32), data: vec![4, 5, 6] }, ]); assert_eq!( Fragment::new(relay_parent.clone(), constraints.clone(), candidate.clone()), Err(FragmentValidityError::HrmpMessagesDescendingOrDuplicate(1)), ); candidate.commitments_mut().horizontal_messages = HorizontalMessages::truncate_from(vec![ OutboundHrmpMessage { recipient: ParaId::from(1 as u32), data: vec![1, 2, 3] }, OutboundHrmpMessage { recipient: ParaId::from(0 as u32), data: vec![4, 5, 6] }, ]); assert_eq!( Fragment::new(relay_parent, constraints, candidate), Err(FragmentValidityError::HrmpMessagesDescendingOrDuplicate(1)), ); } }