From 7cc81f5fc851f864df8bf40ff37f3b0cca9411e7 Mon Sep 17 00:00:00 2001 From: Zeke Mostov <32168567+emostov@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:10:52 +0200 Subject: [PATCH 01/27] Weights for enacting candidates --- primitives/src/v1/mod.rs | 2 ++ runtime/parachains/src/dmp.rs | 4 +++ runtime/parachains/src/hrmp.rs | 18 ++++++++++ runtime/parachains/src/inclusion.rs | 44 ++++++++++++++++++------ runtime/parachains/src/paras.rs | 10 ++++++ runtime/parachains/src/paras_inherent.rs | 20 ++++++++--- runtime/parachains/src/ump.rs | 12 +++++++ 7 files changed, 94 insertions(+), 16 deletions(-) diff --git a/primitives/src/v1/mod.rs b/primitives/src/v1/mod.rs index a4b598dd6c6a..ddafb06a4b4b 100644 --- a/primitives/src/v1/mod.rs +++ b/primitives/src/v1/mod.rs @@ -513,8 +513,10 @@ impl PersistedValidationData { #[cfg_attr(feature = "std", derive(Debug, Default, Hash, MallocSizeOf))] pub struct CandidateCommitments { /// Messages destined to be interpreted by the Relay chain itself. + // TODO bound pub upward_messages: Vec, /// Horizontal messages sent by the parachain. + // TODO bound pub horizontal_messages: Vec>, /// New validation code. pub new_validation_code: Option, diff --git a/runtime/parachains/src/dmp.rs b/runtime/parachains/src/dmp.rs index 08def77f9c35..0ac3620cbf06 100644 --- a/runtime/parachains/src/dmp.rs +++ b/runtime/parachains/src/dmp.rs @@ -203,6 +203,10 @@ impl Pallet { T::DbWeight::get().reads_writes(1, 1) } + pub(crate) fn prune_dmq_weight() -> Weight { + T::DbWeight::get().reads_writes(1, 1) + } + /// Returns the Head of Message Queue Chain for the given para or `None` if there is none /// associated with it. #[cfg(test)] diff --git a/runtime/parachains/src/hrmp.rs b/runtime/parachains/src/hrmp.rs index 87ba4ad861b8..b76af85d8911 100644 --- a/runtime/parachains/src/hrmp.rs +++ b/runtime/parachains/src/hrmp.rs @@ -908,6 +908,18 @@ impl Pallet { weight } + /// Worst case weight for prune hrmp weight. + pub(crate) fn prune_hrmp_weight( + hrmp_max_parachain_inbound_channels: u32, + hrmp_max_parathread_inbound_channels: u32, + ) -> Weight { + let max_pruneable_channels: u64 = hrmp_max_parachain_inbound_channels + .max(hrmp_max_parathread_inbound_channels).into(); + + T::DbWeight::get() + .reads_writes(1 + 2 * max_pruneable_channels, 1 + 1 + 2 * max_pruneable_channels) + } + /// Process the outbound HRMP messages by putting them into the appropriate recipient queues. /// /// Returns the amount of weight consumed. @@ -979,6 +991,12 @@ impl Pallet { weight } + /// Worst case weight for queue outbound hrmp. + pub(crate) fn queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate: u32) -> Weight { + let read_writes = (2 * hrmp_max_message_num_per_candidate).into(); + T::DbWeight::get().reads_writes(read_writes, read_writes) + } + /// Initiate opening a channel from a parachain to a given recipient with given channel /// parameters. /// diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index dd865bc8572b..8cb8a133b327 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -257,15 +257,17 @@ impl Pallet { /// Process a set of incoming bitfields. /// - /// Returns a `Vec` of `CandidateHash`es and their respective `AvailabilityCore`s that became available, - /// and cores free. + /// Returns a `Vec` of `CandidateHash`es and their respective `AvailabilityCore`s that became + /// available, cores free, and enactment weight. pub(crate) fn process_bitfields( expected_bits: usize, unchecked_bitfields: UncheckedSignedAvailabilityBitfields, core_lookup: impl Fn(CoreIndex) -> Option, - ) -> Result, DispatchError> { + ) -> Result<(Vec<(CoreIndex, CandidateHash)>, Weight) + , DispatchError> { let validators = shared::Pallet::::active_validator_keys(); let session_index = shared::Pallet::::session_index(); + let mut enacted_candidate_weight = 0; let mut assigned_paras_record = (0..expected_bits) .map(|bit_index| core_lookup(CoreIndex::from(bit_index as u32))) @@ -383,7 +385,7 @@ impl Pallet { descriptor: pending_availability.descriptor, commitments, }; - Self::enact_candidate( + enacted_candidate_weight += Self::enact_candidate( pending_availability.relay_parent_number, receipt, pending_availability.backers, @@ -398,7 +400,7 @@ impl Pallet { } } - Ok(freed_cores) + Ok((freed_cores, enacted_candidate_weight)) } /// Process candidates that have been backed. Provide the relay storage root, a set of candidates @@ -717,6 +719,8 @@ impl Pallet { let plain = receipt.to_plain(); let commitments = receipt.commitments; let config = >::config(); + // initial weight is config read. + let mut weight = T::DbWeight::get().reads_writes(1, 0); T::RewardValidators::reward_backing( backers @@ -734,8 +738,6 @@ impl Pallet { .map(|(i, _)| ValidatorIndex(i as _)), ); - // initial weight is config read. - let mut weight = T::DbWeight::get().reads_writes(1, 0); if let Some(new_code) = commitments.new_validation_code { weight += >::schedule_code_upgrade( receipt.descriptor.para_id, @@ -778,6 +780,26 @@ impl Pallet { ) } + /// Worst case weight for `enact_candidate`. `enact_candidate` should return actual weight used. + pub(crate) fn enact_candidate_weight( + hrmp_max_message_num_per_candidate: u32, + max_upward_message_num_per_candidate: u32, + hrmp_max_parachain_inbound_channels: u32, + hrmp_max_parathread_inbound_channels: u32, + ) -> Weight { + T::DbWeight::get().reads(1) // initial weight is config read. + // enact the messaging facet of the candidate. + + >::schedule_code_upgrade_weight() + + >::prune_dmq_weight() + + >::receive_upward_messages_weight(max_upward_message_num_per_candidate) + + >::prune_hrmp_weight( + hrmp_max_parachain_inbound_channels, + hrmp_max_parathread_inbound_channels + ) + + >::queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate) + + >::note_new_head_weight() + } + /// Cleans up all paras pending availability that the predicate returns true for. /// /// The predicate accepts the index of the core and the block number the core has been occupied @@ -1451,8 +1473,8 @@ mod tests { expected_bits(), vec![signed.into()], &core_lookup, - ), - Ok(vec![]) + ).unwrap().0, + vec![] ); } @@ -1556,8 +1578,8 @@ mod tests { expected_bits(), vec![signed.into()], &core_lookup, - ), - Ok(vec![]), + ).unwrap().0, + vec![], ); } }); diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index d7bc9e7a7619..dcef24b8b9df 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -1002,6 +1002,11 @@ impl Pallet { }) } + /// Worst case weight for `schedule_code_upgrade`. + pub(crate) fn schedule_code_upgrade_weight() -> Weight { + T::DbWeight::get().reads_writes(2 + 1, 3 + 2) + } + /// 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 relay-parent block number provided. @@ -1040,6 +1045,11 @@ impl Pallet { } } + /// Worst case weight for `note_new_head`. + pub(crate) fn note_new_head_weight() -> Weight { + T::DbWeight::get().reads_writes(2 + 3, 3 + 1 + 3) + } + /// Fetches the validation code hash for the validation code to be used when validating a block /// in the context of the given relay-chain height. A second block number parameter may be used /// to tell the lookup to proceed as if an intermediate parablock has been with the given diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index c866a077ccb2..f729e3333cce 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -25,7 +25,7 @@ use crate::{ disputes::DisputesHandler, inclusion, scheduler::{self, FreedReason}, - shared, ump, + shared, ump, configuration }; use frame_support::{ inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, @@ -58,7 +58,7 @@ pub mod pallet { #[pallet::config] #[pallet::disable_frame_system_supertrait_check] - pub trait Config: inclusion::Config + scheduler::Config {} + pub trait Config: inclusion::Config + scheduler::Config + configuration::Config {} #[pallet::error] pub enum Error { @@ -153,7 +153,17 @@ pub mod pallet { impl Pallet { /// Enter the paras inherent. This will process bitfields and backed candidates. #[pallet::weight(( - MINIMAL_INCLUSION_INHERENT_WEIGHT + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT, + { + let c = >::config(); + MINIMAL_INCLUSION_INHERENT_WEIGHT + + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT + + >::enact_candidate_weight( + c.hrmp_max_message_num_per_candidate, + c.max_upward_message_num_per_candidate, + c.hrmp_max_parachain_inbound_channels, + c.hrmp_max_parathread_inbound_channels, + ) + }, DispatchClass::Mandatory, ))] pub fn enter( @@ -219,7 +229,7 @@ pub mod pallet { // Process new availability bitfields, yielding any availability cores whose // work has now concluded. let expected_bits = >::availability_cores().len(); - let freed_concluded = >::process_bitfields( + let (freed_concluded, enacted_weight) = >::process_bitfields( expected_bits, signed_bitfields, >::core_para, @@ -296,7 +306,7 @@ pub mod pallet { Included::::set(Some(())); Ok(Some( - MINIMAL_INCLUSION_INHERENT_WEIGHT + + enacted_weight + MINIMAL_INCLUSION_INHERENT_WEIGHT + (backed_candidates_len * BACKED_CANDIDATE_WEIGHT), ) .into()) diff --git a/runtime/parachains/src/ump.rs b/runtime/parachains/src/ump.rs index 47111e357db9..414fd1c35bf1 100644 --- a/runtime/parachains/src/ump.rs +++ b/runtime/parachains/src/ump.rs @@ -451,6 +451,18 @@ impl Pallet { weight } + /// Worst case weight for `receive_upward_messages`. + pub(crate) fn receive_upward_messages_weight( + max_upward_message_num_per_candidate: u32, + ) -> Weight { + use sp_runtime::traits::Zero; + if !max_upward_message_num_per_candidate.is_zero() { + T::DbWeight::get().reads_writes(3, 3) + } else { + 0 + } + } + /// Devote some time into dispatching pending upward messages. pub(crate) fn process_pending_upward_messages() -> Weight { let mut weight_used = 0; From eabde2405c7fe1d59706aca71dbd0e94b8fba29b Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:23:18 +0200 Subject: [PATCH 02/27] Multiply enactments by cores --- runtime/parachains/src/paras_inherent.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index f729e3333cce..03bb2d3863b4 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -155,6 +155,9 @@ pub mod pallet { #[pallet::weight(( { let c = >::config(); + let num_cores = c.max_validators // TODO check if better way to do this + .unwrap_or_default() + / c.max_validators_per_core.unwrap_or_default(); MINIMAL_INCLUSION_INHERENT_WEIGHT + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT + >::enact_candidate_weight( @@ -162,7 +165,7 @@ pub mod pallet { c.max_upward_message_num_per_candidate, c.hrmp_max_parachain_inbound_channels, c.hrmp_max_parathread_inbound_channels, - ) + ) * num_cores as u64 }, DispatchClass::Mandatory, ))] From 6b6b037e403d5f1ae6a915a42c719e8f7b58daf8 Mon Sep 17 00:00:00 2001 From: Zeke Mostov <32168567+emostov@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:25:48 +0200 Subject: [PATCH 03/27] Apply suggestions from code review --- primitives/src/v1/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/primitives/src/v1/mod.rs b/primitives/src/v1/mod.rs index ddafb06a4b4b..a4b598dd6c6a 100644 --- a/primitives/src/v1/mod.rs +++ b/primitives/src/v1/mod.rs @@ -513,10 +513,8 @@ impl PersistedValidationData { #[cfg_attr(feature = "std", derive(Debug, Default, Hash, MallocSizeOf))] pub struct CandidateCommitments { /// Messages destined to be interpreted by the Relay chain itself. - // TODO bound pub upward_messages: Vec, /// Horizontal messages sent by the parachain. - // TODO bound pub horizontal_messages: Vec>, /// New validation code. pub new_validation_code: Option, From 4187793e10aee72db04be84d8cdd3de44e8ce812 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 18:38:46 +0200 Subject: [PATCH 04/27] Some improvements --- runtime/parachains/src/hrmp.rs | 12 +++++++----- runtime/parachains/src/inclusion.rs | 13 ++++++++----- runtime/parachains/src/paras.rs | 8 ++++---- runtime/parachains/src/paras_inherent.rs | 21 ++++++++++++--------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/runtime/parachains/src/hrmp.rs b/runtime/parachains/src/hrmp.rs index b76af85d8911..dc1ae5861abf 100644 --- a/runtime/parachains/src/hrmp.rs +++ b/runtime/parachains/src/hrmp.rs @@ -914,10 +914,11 @@ impl Pallet { hrmp_max_parathread_inbound_channels: u32, ) -> Weight { let max_pruneable_channels: u64 = hrmp_max_parachain_inbound_channels - .max(hrmp_max_parathread_inbound_channels).into(); + .max(hrmp_max_parathread_inbound_channels) + .into(); T::DbWeight::get() - .reads_writes(1 + 2 * max_pruneable_channels, 1 + 1 + 2 * max_pruneable_channels) + .reads_writes(1 + 2 * max_pruneable_channels, 2 + 2 * max_pruneable_channels) } /// Process the outbound HRMP messages by putting them into the appropriate recipient queues. @@ -985,7 +986,7 @@ impl Pallet { } ::HrmpChannelDigests::insert(&channel_id.recipient, recipient_digest); - weight += T::DbWeight::get().reads_writes(2, 2); + weight += T::DbWeight::get().reads_writes(3, 2); } weight @@ -993,8 +994,9 @@ impl Pallet { /// Worst case weight for queue outbound hrmp. pub(crate) fn queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate: u32) -> Weight { - let read_writes = (2 * hrmp_max_message_num_per_candidate).into(); - T::DbWeight::get().reads_writes(read_writes, read_writes) + let reads = (3 * hrmp_max_message_num_per_candidate).into(); + let writes = (2 * hrmp_max_message_num_per_candidate).into(); + T::DbWeight::get().reads_writes(reads, writes) } /// Initiate opening a channel from a parachain to a given recipient with given channel diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index 8cb8a133b327..90b0176912d2 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -263,8 +263,7 @@ impl Pallet { expected_bits: usize, unchecked_bitfields: UncheckedSignedAvailabilityBitfields, core_lookup: impl Fn(CoreIndex) -> Option, - ) -> Result<(Vec<(CoreIndex, CandidateHash)>, Weight) - , DispatchError> { + ) -> Result<(Vec<(CoreIndex, CandidateHash)>, Weight), DispatchError> { let validators = shared::Pallet::::active_validator_keys(); let session_index = shared::Pallet::::session_index(); let mut enacted_candidate_weight = 0; @@ -780,7 +779,7 @@ impl Pallet { ) } - /// Worst case weight for `enact_candidate`. `enact_candidate` should return actual weight used. + /// Worst case weight for `enact_candidate`. pub(crate) fn enact_candidate_weight( hrmp_max_message_num_per_candidate: u32, max_upward_message_num_per_candidate: u32, @@ -1473,7 +1472,9 @@ mod tests { expected_bits(), vec![signed.into()], &core_lookup, - ).unwrap().0, + ) + .unwrap() + .0, vec![] ); } @@ -1578,7 +1579,9 @@ mod tests { expected_bits(), vec![signed.into()], &core_lookup, - ).unwrap().0, + ) + .unwrap() + .0, vec![], ); } diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index dcef24b8b9df..a9be3e570ee8 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -964,7 +964,7 @@ impl Pallet { ) -> Weight { ::FutureCodeUpgrades::mutate(&id, |up| { if up.is_some() { - T::DbWeight::get().reads_writes(1, 0) + T::DbWeight::get().reads_writes(1, 1) } else { let expected_at = relay_parent_number + cfg.validation_upgrade_delay; let next_possible_upgrade_at = @@ -997,14 +997,14 @@ impl Pallet { let (reads, writes) = Self::increase_code_ref(&new_code_hash, &new_code); FutureCodeHash::::insert(&id, new_code_hash); - T::DbWeight::get().reads_writes(2 + reads, 3 + writes) + T::DbWeight::get().reads_writes(3 + reads, 3 + writes) } }) } /// Worst case weight for `schedule_code_upgrade`. pub(crate) fn schedule_code_upgrade_weight() -> Weight { - T::DbWeight::get().reads_writes(2 + 1, 3 + 2) + T::DbWeight::get().reads_writes(4, 5) } /// Note that a para has progressed to a new head, where the new head was executed in the context @@ -1047,7 +1047,7 @@ impl Pallet { /// Worst case weight for `note_new_head`. pub(crate) fn note_new_head_weight() -> Weight { - T::DbWeight::get().reads_writes(2 + 3, 3 + 1 + 3) + T::DbWeight::get().reads_writes(6, 6) } /// Fetches the validation code hash for the validation code to be used when validating a block diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 03bb2d3863b4..424632b17b41 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -22,10 +22,11 @@ //! this module. use crate::{ + configuration, disputes::DisputesHandler, inclusion, scheduler::{self, FreedReason}, - shared, ump, configuration + shared, ump, }; use frame_support::{ inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, @@ -155,17 +156,18 @@ pub mod pallet { #[pallet::weight(( { let c = >::config(); - let num_cores = c.max_validators // TODO check if better way to do this - .unwrap_or_default() - / c.max_validators_per_core.unwrap_or_default(); - MINIMAL_INCLUSION_INHERENT_WEIGHT - + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT - + >::enact_candidate_weight( + let enact_candidate_weight = >::enact_candidate_weight( c.hrmp_max_message_num_per_candidate, c.max_upward_message_num_per_candidate, c.hrmp_max_parachain_inbound_channels, c.hrmp_max_parathread_inbound_channels, - ) * num_cores as u64 + ) + // NOTE: this will need to updated if the max number of cores changes. + * 40u64; + + MINIMAL_INCLUSION_INHERENT_WEIGHT + + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT + + enact_candidate_weight }, DispatchClass::Mandatory, ))] @@ -309,7 +311,8 @@ pub mod pallet { Included::::set(Some(())); Ok(Some( - enacted_weight + MINIMAL_INCLUSION_INHERENT_WEIGHT + + enacted_weight + + MINIMAL_INCLUSION_INHERENT_WEIGHT + (backed_candidates_len * BACKED_CANDIDATE_WEIGHT), ) .into()) From 8724e9acb827fc5e861d294842e94c5ecbb6d92a Mon Sep 17 00:00:00 2001 From: Zeke Mostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 18:44:07 +0200 Subject: [PATCH 05/27] Apply suggestions from code review --- runtime/parachains/src/inclusion.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index 90b0176912d2..310190e623c3 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -786,8 +786,7 @@ impl Pallet { hrmp_max_parachain_inbound_channels: u32, hrmp_max_parathread_inbound_channels: u32, ) -> Weight { - T::DbWeight::get().reads(1) // initial weight is config read. - // enact the messaging facet of the candidate. + T::DbWeight::get().reads(1) + >::schedule_code_upgrade_weight() + >::prune_dmq_weight() + >::receive_upward_messages_weight(max_upward_message_num_per_candidate) From 5ecc2bba662a59ea20a916050d24675cedce3c7e Mon Sep 17 00:00:00 2001 From: Zeke Mostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 18:58:28 +0200 Subject: [PATCH 06/27] Apply suggestions from code review --- runtime/parachains/src/hrmp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/parachains/src/hrmp.rs b/runtime/parachains/src/hrmp.rs index dc1ae5861abf..977102874ba6 100644 --- a/runtime/parachains/src/hrmp.rs +++ b/runtime/parachains/src/hrmp.rs @@ -908,7 +908,7 @@ impl Pallet { weight } - /// Worst case weight for prune hrmp weight. + /// Worst case weight for `prune_hrmp`. pub(crate) fn prune_hrmp_weight( hrmp_max_parachain_inbound_channels: u32, hrmp_max_parathread_inbound_channels: u32, From eeb158e1ad4cb06b0e3d16400cd51ae1d76b875a Mon Sep 17 00:00:00 2001 From: Zeke Mostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:05:38 +0200 Subject: [PATCH 07/27] Apply suggestions from code review --- runtime/parachains/src/hrmp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/parachains/src/hrmp.rs b/runtime/parachains/src/hrmp.rs index 977102874ba6..9d45209429cf 100644 --- a/runtime/parachains/src/hrmp.rs +++ b/runtime/parachains/src/hrmp.rs @@ -992,7 +992,7 @@ impl Pallet { weight } - /// Worst case weight for queue outbound hrmp. + /// Worst case weight for `queue_outbound_hrmp`. pub(crate) fn queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate: u32) -> Weight { let reads = (3 * hrmp_max_message_num_per_candidate).into(); let writes = (2 * hrmp_max_message_num_per_candidate).into(); From 973ceeb10fc394226a6baf2c4210bab79922de5f Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:06:35 +0200 Subject: [PATCH 08/27] Update schedule code upgrade --- runtime/parachains/src/paras.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index a9be3e570ee8..497e9a9b071f 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -997,14 +997,14 @@ impl Pallet { let (reads, writes) = Self::increase_code_ref(&new_code_hash, &new_code); FutureCodeHash::::insert(&id, new_code_hash); - T::DbWeight::get().reads_writes(3 + reads, 3 + writes) + T::DbWeight::get().reads_writes(3 + reads, 5 + writes) } }) } /// Worst case weight for `schedule_code_upgrade`. pub(crate) fn schedule_code_upgrade_weight() -> Weight { - T::DbWeight::get().reads_writes(4, 5) + T::DbWeight::get().reads_writes(4, 8) } /// Note that a para has progressed to a new head, where the new head was executed in the context From 9b325b31fe9f546c7a110fdd1af454dcf4548b6b Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Fri, 29 Oct 2021 19:16:39 +0200 Subject: [PATCH 09/27] fmt --- runtime/parachains/src/inclusion.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index 310190e623c3..5f2279d8a8e0 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -786,16 +786,15 @@ impl Pallet { hrmp_max_parachain_inbound_channels: u32, hrmp_max_parathread_inbound_channels: u32, ) -> Weight { - T::DbWeight::get().reads(1) - + >::schedule_code_upgrade_weight() - + >::prune_dmq_weight() - + >::receive_upward_messages_weight(max_upward_message_num_per_candidate) - + >::prune_hrmp_weight( - hrmp_max_parachain_inbound_channels, - hrmp_max_parathread_inbound_channels - ) - + >::queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate) - + >::note_new_head_weight() + T::DbWeight::get().reads(1) + + >::schedule_code_upgrade_weight() + + >::prune_dmq_weight() + + >::receive_upward_messages_weight(max_upward_message_num_per_candidate) + + >::prune_hrmp_weight( + hrmp_max_parachain_inbound_channels, + hrmp_max_parathread_inbound_channels, + ) + >::queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate) + + >::note_new_head_weight() } /// Cleans up all paras pending availability that the predicate returns true for. From b8a12f20095f2553d473b2731b275928a4f17155 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Sun, 31 Oct 2021 10:57:42 +0100 Subject: [PATCH 10/27] Add MAX_EXPECTED_CORES_FOR_WEIGHT_CALC --- runtime/parachains/src/paras_inherent.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 424632b17b41..77ced0add8fe 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -48,6 +48,9 @@ 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; +// The upper bound of cores used for calculating the worst case weight of enacting candidates. This +// value must be updated anytime a production configuration increases the upper bound of cores. +const MAX_EXPECTED_CORES_FOR_WEIGHT_CALC: Weight = 100; #[frame_support::pallet] pub mod pallet { @@ -163,7 +166,7 @@ pub mod pallet { c.hrmp_max_parathread_inbound_channels, ) // NOTE: this will need to updated if the max number of cores changes. - * 40u64; + * MAX_EXPECTED_CORES_FOR_WEIGHT_CALC; MINIMAL_INCLUSION_INHERENT_WEIGHT + data.backed_candidates.len() as Weight * BACKED_CANDIDATE_WEIGHT From 244ac54f3389517d647816d30cbe412f7f46b411 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 17 Nov 2021 13:03:32 +0100 Subject: [PATCH 11/27] Update to refund with weight based on enact candidates --- runtime/parachains/src/inclusion.rs | 44 ++++++---- runtime/parachains/src/paras_inherent.rs | 103 ++++++++++++++++------- 2 files changed, 102 insertions(+), 45 deletions(-) diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index 7e8220405204..cc93432f6336 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -294,7 +294,7 @@ impl Pallet { validators: &[ValidatorId], signed_bitfields: UncheckedSignedAvailabilityBitfields, core_lookup: F, - ) -> Vec<(CoreIndex, CandidateHash)> + ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) where F: Fn(CoreIndex) -> Option, { @@ -343,6 +343,7 @@ impl Pallet { >::insert(&validator_index, record); } + let mut enacted_candidate_weight = 0; let threshold = availability_threshold(validators.len()); let mut freed_cores = Vec::with_capacity(expected_bits); @@ -370,7 +371,8 @@ impl Pallet { descriptor: pending_availability.descriptor, commitments, }; - let _weight = Self::enact_candidate( + + let weight = Self::enact_candidate( pending_availability.relay_parent_number, receipt, pending_availability.backers, @@ -378,6 +380,8 @@ impl Pallet { pending_availability.core, pending_availability.backing_group, ); + + enacted_candidate_weight = enacted_candidate_weight.saturating_add(weight); } freed_cores.push((pending_availability.core, pending_availability.hash)); @@ -386,19 +390,19 @@ impl Pallet { } } - Ok((freed_cores, enacted_candidate_weight)) + (freed_cores, enacted_candidate_weight) } /// Process a set of incoming bitfields. /// /// Returns a `Vec` of `CandidateHash`es and their respective `AvailabilityCore`s that became available, - /// and cores free. + /// and cores free. Additionally returns the weight consumed by enacted candidates. pub(crate) fn process_bitfields( expected_bits: usize, signed_bitfields: UncheckedSignedAvailabilityBitfields, disputed_bitfield: DisputedBitfield, core_lookup: impl Fn(CoreIndex) -> Option, - ) -> Vec<(CoreIndex, CandidateHash)> { + ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) { let validators = shared::Pallet::::active_validator_keys(); let session_index = shared::Pallet::::session_index(); let parent_hash = frame_system::Pallet::::parent_hash(); @@ -412,14 +416,15 @@ impl Pallet { &validators[..], ); - let freed_cores = Self::update_pending_availability_and_get_freed_cores::<_, true>( - expected_bits, - &validators[..], - checked_bitfields, - core_lookup, - ); + let (freed_cores, enacted_candidate_weight) = + Self::update_pending_availability_and_get_freed_cores::<_, true>( + expected_bits, + &validators[..], + checked_bitfields, + core_lookup, + ); - freed_cores + (freed_cores, enacted_candidate_weight) } /// Process candidates that have been backed. Provide the relay storage root, a set of candidates @@ -1412,7 +1417,8 @@ pub(crate) mod tests { vec![signed.into()], DisputedBitfield::zeros(expected_bits()), &core_lookup, - ), + ) + .0, vec![] ); } @@ -1434,7 +1440,8 @@ pub(crate) mod tests { vec![signed.into()], DisputedBitfield::zeros(expected_bits()), &core_lookup, - ), + ) + .0, vec![] ); } @@ -1472,6 +1479,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); assert_eq!( @@ -1528,6 +1536,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); assert_eq!( @@ -1559,6 +1568,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); } @@ -1579,6 +1589,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); } @@ -1622,6 +1633,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); >::remove(chain_a); @@ -1665,6 +1677,7 @@ pub(crate) mod tests { DisputedBitfield::zeros(expected_bits()), &core_lookup, ) + .0 .is_empty()); } }); @@ -1811,7 +1824,8 @@ pub(crate) mod tests { signed_bitfields, DisputedBitfield::zeros(expected_bits()), &core_lookup, - ), + ) + .0, vec![(CoreIndex(0), candidate_a.hash())] ); diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 541215df1ad3..6892449fd00a 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -27,6 +27,7 @@ use crate::{ inclusion, initializer, scheduler::{self, CoreAssignment, FreedReason}, shared, ump, + configuration::HostConfiguration }; use bitvec::prelude::BitVec; use frame_support::{ @@ -112,12 +113,14 @@ impl WeightInfo for TestWeightInfo { } } +// Note that this does a storage read. fn paras_inherent_total_weight( backed_candidates: &[BackedCandidate<::Hash>], bitfields: &[UncheckedSignedAvailabilityBitfield], disputes: &[DisputeStatementSet], + config: HostConfiguration, ) -> Weight { - backed_candidates_weight::(backed_candidates) + backed_candidates_weight::(backed_candidates, config) .saturating_add(signed_bitfields_weight::(bitfields.len())) .saturating_add(dispute_statements_weight::(disputes)) } @@ -138,7 +141,8 @@ fn signed_bitfields_weight(bitfields_len: usize) -> Weight { .saturating_mul(bitfields_len as Weight) } -fn backed_candidate_weight( +// Inner function of `backed_candidate_weight_fn`. +fn backed_candidate_weight_inner( candidate: &BackedCandidate, ) -> Weight { if candidate.candidate.commitments.new_validation_code.is_some() { @@ -150,12 +154,44 @@ fn backed_candidate_weight( } } +fn enact_candidates_weight( + candidate_count: usize, + config: &HostConfiguration, +) -> Weight { + >::enact_candidate_weight( + config.hrmp_max_parathread_inbound_channels, + config.hrmp_max_parachain_inbound_channels, + config.max_upward_message_num_per_candidate, + config.hrmp_max_message_num_per_candidate, + ) + .saturating_mul(candidate_count as Weight) +} + +// Returns a function that calculates the max weight of a candidate. +fn backed_candidate_weight_fn( + config: HostConfiguration, +) -> impl Fn(&BackedCandidate) -> Weight { + move |candidate: &BackedCandidate| -> Weight { + backed_candidate_weight_inner::(candidate).saturating_add( + >::enact_candidate_weight( + config.hrmp_max_parathread_inbound_channels, + config.hrmp_max_parachain_inbound_channels, + config.max_upward_message_num_per_candidate, + config.hrmp_max_message_num_per_candidate, + ), + ) + } +} + +// Calculate the max weight of the given candidates. fn backed_candidates_weight( candidates: &[BackedCandidate], + config: HostConfiguration, ) -> Weight { + let backed_candidate_weight = backed_candidate_weight_fn::(config); candidates .iter() - .map(|c| backed_candidate_weight::(c)) + .map(|c| backed_candidate_weight(c)) .fold(0, |acc, x| acc.saturating_add(x)) } @@ -313,22 +349,13 @@ pub mod pallet { #[pallet::call] impl Pallet { /// Enter the paras inherent. This will process bitfields and backed candidates. - #[pallet::weight(({ - let enact_candidate_weight = >::enact_candidate_weight( - c.hrmp_max_parathread_inbound_channels, - c.hrmp_max_parachain_inbound_channels, - c.max_upward_message_num_per_candidate, - c.hrmp_max_message_num_per_candidate, - ) - // NOTE: this will need to updated if the max number of cores changes. - * MAX_EXPECTED_CORES_FOR_WEIGHT_CALC; - - enact_candidate_weight + paras_inherent_total_weight::( - data.backed_candidates.as_slice(), - data.bitfields.as_slice(), - data.disputes.as_slice(), - ) - }, + #[pallet::weight(( + paras_inherent_total_weight::( + data.backed_candidates.as_slice(), + data.bitfields.as_slice(), + data.disputes.as_slice(), + >::config(), + ), DispatchClass::Mandatory, ))] pub fn enter( @@ -354,7 +381,8 @@ pub mod pallet { Error::::InvalidParentHeader, ); - let mut candidate_weight = backed_candidates_weight::(&backed_candidates); + let config = >::config(); + let mut candidate_weight = backed_candidates_weight::(&backed_candidates, config.clone()); let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); let disputes_weight = dispute_statements_weight::(&disputes); @@ -449,12 +477,13 @@ pub mod pallet { // Process new availability bitfields, yielding any availability cores whose // work has now concluded. - let (freed_concluded, enacted_weight) = >::process_bitfields( - expected_bits, - signed_bitfields, - disputed_bitfield, - >::core_para, - ); + let (freed_concluded, actual_enact_candidates_weight) = + >::process_bitfields( + expected_bits, + signed_bitfields, + disputed_bitfield, + >::core_para, + ); // Inform the disputes module of all included candidates. let now = >::block_number(); @@ -479,6 +508,7 @@ pub mod pallet { // Process backed candidates according to scheduled cores. let parent_storage_root = parent_header.state_root().clone(); + let backed_candidates_count = backed_candidates.len(); let inclusion::ProcessedCandidates::<::Hash> { core_indices: occupied, candidate_receipt_with_backing_validator_indices, @@ -504,7 +534,17 @@ pub mod pallet { // this is max config.ump_service_total_weight let _ump_weight = >::process_pending_upward_messages(); - Ok(Some(total_weight).into()) + let actual_total_weight = { + let max_enact_candidates_weight = + enact_candidates_weight::(backed_candidates_count, &config); + total_weight + // subtract the max enact candidate weight, + .saturating_sub(max_enact_candidates_weight) + // and add back the actual enact candidate weight + .saturating_add(actual_enact_candidates_weight) + }; + + Ok(Some(actual_total_weight).into()) } } } @@ -608,7 +648,7 @@ impl Pallet { &validator_public[..], ); - let freed_concluded = + let (freed_concluded, _enact_candidate_weight) = >::update_pending_availability_and_get_freed_cores::< _, false, @@ -654,6 +694,7 @@ impl Pallet { &mut disputes, max_block_weight, &mut rng, + >::config(), ); Some(ParachainsInherentData:: { @@ -739,11 +780,12 @@ fn apply_weight_limit( disputes: &mut MultiDisputeStatementSet, max_block_weight: Weight, rng: &mut rand_chacha::ChaChaRng, + config: HostConfiguration ) -> Weight { // include as many disputes as possible, always let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); - let total_candidates_weight = backed_candidates_weight::(candidates.as_slice()); + let total_candidates_weight = backed_candidates_weight::(candidates.as_slice(), config.clone()); let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); @@ -757,11 +799,12 @@ fn apply_weight_limit( // There is weight remaining to be consumed by a subset of candidates // which are going to be picked now. if let Some(remaining_weight) = remaining_weight.checked_sub(total_bitfields_weight) { + let backed_candidate_weight = backed_candidate_weight_fn::(config); let (acc_candidate_weight, indices) = random_sel::::Hash>, _>( rng, candidates.clone(), - |c| backed_candidate_weight::(c), + |c| backed_candidate_weight(c), remaining_weight, ); let mut idx = 0_usize; From 8f6113a7197e754c73dc2b22f94555ec8fa8c8c4 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 17 Nov 2021 13:12:11 +0100 Subject: [PATCH 12/27] fmt --- runtime/parachains/src/paras_inherent.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 6892449fd00a..d752cd4e6fb2 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -23,11 +23,11 @@ use crate::{ configuration, + configuration::HostConfiguration, disputes::DisputesHandler, inclusion, initializer, scheduler::{self, CoreAssignment, FreedReason}, shared, ump, - configuration::HostConfiguration }; use bitvec::prelude::BitVec; use frame_support::{ @@ -382,7 +382,8 @@ pub mod pallet { ); let config = >::config(); - let mut candidate_weight = backed_candidates_weight::(&backed_candidates, config.clone()); + let mut candidate_weight = + backed_candidates_weight::(&backed_candidates, config.clone()); let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); let disputes_weight = dispute_statements_weight::(&disputes); @@ -780,12 +781,13 @@ fn apply_weight_limit( disputes: &mut MultiDisputeStatementSet, max_block_weight: Weight, rng: &mut rand_chacha::ChaChaRng, - config: HostConfiguration + config: HostConfiguration, ) -> Weight { // include as many disputes as possible, always let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); - let total_candidates_weight = backed_candidates_weight::(candidates.as_slice(), config.clone()); + let total_candidates_weight = + backed_candidates_weight::(candidates.as_slice(), config.clone()); let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); From fc713d3e96f4eedb84c63fbfae38b9e94a0d2a61 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 17 Nov 2021 13:28:26 +0100 Subject: [PATCH 13/27] Use saturating arith in enact_candidates_weight --- runtime/parachains/src/inclusion.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index cc93432f6336..8b8b865e309e 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -811,15 +811,21 @@ impl Pallet { hrmp_max_parachain_inbound_channels: u32, hrmp_max_parathread_inbound_channels: u32, ) -> Weight { - T::DbWeight::get().reads(1) + - >::schedule_code_upgrade_weight() + - >::prune_dmq_weight() + - >::receive_upward_messages_weight(max_upward_message_num_per_candidate) + - >::prune_hrmp_weight( + T::DbWeight::get() + .reads(1) + .saturating_add(>::schedule_code_upgrade_weight()) + .saturating_add(>::prune_dmq_weight()) + .saturating_add(>::receive_upward_messages_weight( + max_upward_message_num_per_candidate, + )) + .saturating_add(>::prune_hrmp_weight( hrmp_max_parachain_inbound_channels, hrmp_max_parathread_inbound_channels, - ) + >::queue_outbound_hrmp_weight(hrmp_max_message_num_per_candidate) + - >::note_new_head_weight() + )) + .saturating_add(>::queue_outbound_hrmp_weight( + hrmp_max_message_num_per_candidate, + )) + .saturating_add(>::note_new_head_weight()) } /// Cleans up all paras pending availability that the predicate returns true for. From 8443b88c6886d4997b0c9c04b55af2ceb3c42c41 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 24 Nov 2021 12:57:51 -0800 Subject: [PATCH 14/27] add code comment' --- runtime/parachains/src/paras_inherent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 3eab1159d5db..c3bbea28e18e 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -156,6 +156,7 @@ fn backed_candidate_weight_inner( } } +// Calculate the worst case weight for enacting the given number of candidates. fn enact_candidates_weight( candidate_count: usize, config: &HostConfiguration, @@ -502,7 +503,6 @@ impl Pallet { Vec::new() }; - // Create a bit index from the set of core indices where each index corresponds to // a core index that was freed due to a dispute. let disputed_bitfield = create_disputed_bitfield( From 7b1bc7a61127f9bbfb0ef08b836851a8eb358a31 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Sat, 27 Nov 2021 19:12:31 -0800 Subject: [PATCH 15/27] Account for enact_candidate based on availability bitfields --- runtime/parachains/src/paras_inherent.rs | 90 ++++++++++++------------ 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index c3bbea28e18e..4503f1c10f64 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -49,7 +49,7 @@ use primitives::v1::{ use rand::{seq::SliceRandom, SeedableRng}; use scale_info::TypeInfo; -use sp_runtime::traits::{Header as HeaderT, One}; +use sp_runtime::traits::{Header as HeaderT, One, Zero}; use sp_std::{ cmp::Ordering, collections::{btree_map::BTreeMap, btree_set::BTreeSet}, @@ -115,16 +115,37 @@ impl WeightInfo for TestWeightInfo { } } +// Count the ones of the first bitfield in a slice. Careful, since this only counts the ones of +// the first bitfield, it may be misleading as to how many ones the other bitfields contain. It +// should only be used in circumstances where an estimate is ok. +fn bitfields_count_ones(bitfields: &[UncheckedSignedAvailabilityBitfield]) -> usize { + if bitfields.len().is_zero() { + 0 + } else { + bitfields + .get(0) + .expect("checked for empty bitfield. qed.") + .unchecked_payload() + .0 + .count_ones() + } +} + // Note that this does a storage read. fn paras_inherent_total_weight( backed_candidates: &[BackedCandidate<::Hash>], bitfields: &[UncheckedSignedAvailabilityBitfield], disputes: &[DisputeStatementSet], - config: HostConfiguration, + config: &HostConfiguration, ) -> Weight { - backed_candidates_weight::(backed_candidates, config) + backed_candidates_weight::(backed_candidates) .saturating_add(signed_bitfields_weight::(bitfields.len())) .saturating_add(dispute_statements_weight::(disputes)) + // in order to get a constant time calculation we get the votes in the first bitfield + // and assume the worst case for enacting that many candidates. This may not always be + // perfect but should be close enough most of the time for getting an estimate when queuing + // extrinsics for block authoring. + .saturating_add(enact_candidates_weight::(bitfields_count_ones(bitfields), config)) } fn dispute_statements_weight(disputes: &[DisputeStatementSet]) -> Weight { @@ -143,19 +164,6 @@ fn signed_bitfields_weight(bitfields_len: usize) -> Weight { .saturating_mul(bitfields_len as Weight) } -// Inner function of `backed_candidate_weight_fn`. -fn backed_candidate_weight_inner( - candidate: &BackedCandidate, -) -> Weight { - if candidate.candidate.commitments.new_validation_code.is_some() { - <::WeightInfo as WeightInfo>::enter_backed_candidate_code_upgrade() - } else { - <::WeightInfo as WeightInfo>::enter_backed_candidates_variable( - candidate.validity_votes.len() as u32, - ) - } -} - // Calculate the worst case weight for enacting the given number of candidates. fn enact_candidates_weight( candidate_count: usize, @@ -170,18 +178,14 @@ fn enact_candidates_weight( .saturating_mul(candidate_count as Weight) } -// Returns a function that calculates the max weight of a candidate. -fn backed_candidate_weight_fn( - config: HostConfiguration, -) -> impl Fn(&BackedCandidate) -> Weight { - move |candidate: &BackedCandidate| -> Weight { - backed_candidate_weight_inner::(candidate).saturating_add( - >::enact_candidate_weight( - config.hrmp_max_parathread_inbound_channels, - config.hrmp_max_parachain_inbound_channels, - config.max_upward_message_num_per_candidate, - config.hrmp_max_message_num_per_candidate, - ), +fn backed_candidate_weight( + candidate: &BackedCandidate, +) -> Weight { + if candidate.candidate.commitments.new_validation_code.is_some() { + <::WeightInfo as WeightInfo>::enter_backed_candidate_code_upgrade() + } else { + <::WeightInfo as WeightInfo>::enter_backed_candidates_variable( + candidate.validity_votes.len() as u32, ) } } @@ -189,12 +193,10 @@ fn backed_candidate_weight_fn( // Calculate the max weight of the given candidates. fn backed_candidates_weight( candidates: &[BackedCandidate], - config: HostConfiguration, ) -> Weight { - let backed_candidate_weight = backed_candidate_weight_fn::(config); candidates .iter() - .map(|c| backed_candidate_weight(c)) + .map(|c| backed_candidate_weight::(c)) .fold(0, |acc, x| acc.saturating_add(x)) } @@ -373,13 +375,13 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// Enter the paras inherent. This will process bitfields and backed candidates. + /// Enter the paras inherent. This will process disputes, bitfields and backed candidates. #[pallet::weight(( paras_inherent_total_weight::( data.backed_candidates.as_slice(), data.bitfields.as_slice(), data.disputes.as_slice(), - >::config(), + &>::config(), ), DispatchClass::Mandatory, ))] @@ -425,11 +427,14 @@ impl Pallet { ); let now = >::block_number(); - let config = >::config(); - let mut candidate_weight = backed_candidates_weight::(&backed_candidates, config.clone()); + let mut candidate_weight = backed_candidates_weight::(&backed_candidates); let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); let disputes_weight = dispute_statements_weight::(&disputes); + let max_enact_candidates_weight = enact_candidates_weight::( + bitfields_count_ones(&signed_bitfields), + &>::config(), + ); let max_block_weight = ::BlockWeights::get().max_block; @@ -551,8 +556,6 @@ impl Pallet { &scheduled[..], ); - let backed_candidates_count = backed_candidates.len(); - // Process backed candidates according to scheduled cores. let parent_storage_root = parent_header.state_root().clone(); let inclusion::ProcessedCandidates::<::Hash> { @@ -582,8 +585,6 @@ impl Pallet { let _ump_weight = >::process_pending_upward_messages(); let actual_total_weight = { - let max_enact_candidates_weight = - enact_candidates_weight::(backed_candidates_count, &config); total_weight // subtract the max enact candidate weight, .saturating_sub(max_enact_candidates_weight) @@ -767,7 +768,6 @@ impl Pallet { &mut disputes, max_block_weight, &mut rng, - >::config(), ); Some(ParachainsInherentData:: { @@ -873,13 +873,11 @@ fn apply_weight_limit( disputes: &mut MultiDisputeStatementSet, max_block_weight: Weight, rng: &mut rand_chacha::ChaChaRng, - config: HostConfiguration, ) -> Weight { // include as many disputes as possible, always let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); - let total_candidates_weight = - backed_candidates_weight::(candidates.as_slice(), config.clone()); + let total_candidates_weight = backed_candidates_weight::(candidates.as_slice()); let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); @@ -903,13 +901,12 @@ fn apply_weight_limit( // There is weight remaining to be consumed by a subset of candidates // which are going to be picked now. if let Some(remaining_weight) = remaining_weight.checked_sub(total_bitfields_weight) { - let backed_candidate_weight = backed_candidate_weight_fn::(config); let (acc_candidate_weight, indices) = random_sel::::Hash>, _>( rng, candidates.clone(), preferred_indices, - |c| backed_candidate_weight(c), + |c| backed_candidate_weight::(c), remaining_weight, ); candidates.indexed_retain(|idx, _backed_candidate| indices.binary_search(&idx).is_ok()); @@ -927,6 +924,9 @@ fn apply_weight_limit( rng, bitfields.clone(), vec![], + // bitfields can lead to enacting a candidate; however we don't have a good way of + // accounting for that when tracking individual bitfield weight, thus the weight here likely + // ends up being an underestimate. |_| <::WeightInfo as WeightInfo>::enter_bitfields(), remaining_weight, ); From 7e9dcca869b6112d32a1a75ff6da5ebaa990644f Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Sat, 27 Nov 2021 19:20:38 -0800 Subject: [PATCH 16/27] Formatting --- runtime/parachains/src/paras_inherent.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 4503f1c10f64..9cef99661b68 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -584,13 +584,11 @@ impl Pallet { // this is max config.ump_service_total_weight let _ump_weight = >::process_pending_upward_messages(); - let actual_total_weight = { - total_weight - // subtract the max enact candidate weight, - .saturating_sub(max_enact_candidates_weight) - // and add back the actual enact candidate weight - .saturating_add(actual_enact_candidates_weight) - }; + let actual_total_weight = total_weight + // subtract the max enact candidate weight, + .saturating_sub(max_enact_candidates_weight) + // and add back the actual enact candidate weight + .saturating_add(actual_enact_candidates_weight); Ok(Some(actual_total_weight).into()) } From b9e81bf1e458c2a2d9b562860db2426fd7991c7f Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Sat, 27 Nov 2021 19:38:37 -0800 Subject: [PATCH 17/27] Account for enact_candidate weight when calculating total weight --- runtime/parachains/src/paras_inherent.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 9cef99661b68..3c7330a1abfa 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -442,7 +442,8 @@ impl Pallet { let total_weight = { if candidate_weight .saturating_add(bitfields_weight) - .saturating_add(disputes_weight) > + .saturating_add(disputes_weight) + .saturating_add(max_enact_candidates_weight) > max_block_weight { // if the total weight is over the max block weight, first try clearing backed @@ -451,6 +452,7 @@ impl Pallet { candidate_weight = 0; signed_bitfields.clear(); bitfields_weight = 0; + max_enact_candidates_weight = 0; } if disputes_weight > max_block_weight { @@ -467,6 +469,7 @@ impl Pallet { candidate_weight .saturating_add(bitfields_weight) .saturating_add(disputes_weight) + .saturating_add(max_enact_candidates_weight) } }; From 75c8b969f0268fd4e629261b4e97980a528534e9 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Mon, 29 Nov 2021 17:41:26 -0800 Subject: [PATCH 18/27] Or together or bitfields --- runtime/parachains/src/paras_inherent.rs | 52 ++++++++++++------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 3c7330a1abfa..5a4eb81c7b40 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -31,7 +31,7 @@ use crate::{ scheduler::{self, CoreAssignment, FreedReason}, shared, ump, }; -use bitvec::prelude::BitVec; +use bitvec::{order::Lsb0 as BitOrderLsb0, prelude::BitVec}; use frame_support::{ inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, pallet_prelude::*, @@ -49,7 +49,7 @@ use primitives::v1::{ use rand::{seq::SliceRandom, SeedableRng}; use scale_info::TypeInfo; -use sp_runtime::traits::{Header as HeaderT, One, Zero}; +use sp_runtime::traits::{Header as HeaderT, One}; use sp_std::{ cmp::Ordering, collections::{btree_map::BTreeMap, btree_set::BTreeSet}, @@ -61,6 +61,7 @@ use sp_std::{ mod benchmarking; const LOG_TARGET: &str = "runtime::inclusion-inherent"; +const MAX_UNCHECKED_BITFIELD_ITERATIONS: usize = 1_000; pub trait WeightInfo { /// Variant over `v`, the count of dispute statements in a dispute statement set. This gives the @@ -115,20 +116,21 @@ impl WeightInfo for TestWeightInfo { } } -// Count the ones of the first bitfield in a slice. Careful, since this only counts the ones of -// the first bitfield, it may be misleading as to how many ones the other bitfields contain. It -// should only be used in circumstances where an estimate is ok. -fn bitfields_count_ones(bitfields: &[UncheckedSignedAvailabilityBitfield]) -> usize { - if bitfields.len().is_zero() { - 0 - } else { - bitfields - .get(0) - .expect("checked for empty bitfield. qed.") - .unchecked_payload() - .0 - .count_ones() - } +// OR together all bitfields and count the ones from the result. +// Note that this will only OR together the first `MAX_UNCHECKED_BITFIELD_ITERATIONS` to avoid +// excessive iteration. +fn bitfields_count_ones( + bitfields: &[UncheckedSignedAvailabilityBitfield], + expected_bits: usize, +) -> usize { + bitfields + .iter() + .take(MAX_UNCHECKED_BITFIELD_ITERATIONS) + .fold( + BitVec::::repeat(false, expected_bits), + |acc: BitVec, cur| acc | cur.unchecked_payload().0.clone(), + ) + .count_ones() } // Note that this does a storage read. @@ -136,16 +138,16 @@ fn paras_inherent_total_weight( backed_candidates: &[BackedCandidate<::Hash>], bitfields: &[UncheckedSignedAvailabilityBitfield], disputes: &[DisputeStatementSet], + expected_bits: usize, config: &HostConfiguration, ) -> Weight { backed_candidates_weight::(backed_candidates) .saturating_add(signed_bitfields_weight::(bitfields.len())) .saturating_add(dispute_statements_weight::(disputes)) - // in order to get a constant time calculation we get the votes in the first bitfield - // and assume the worst case for enacting that many candidates. This may not always be - // perfect but should be close enough most of the time for getting an estimate when queuing - // extrinsics for block authoring. - .saturating_add(enact_candidates_weight::(bitfields_count_ones(bitfields), config)) + .saturating_add(enact_candidates_weight::( + bitfields_count_ones(bitfields, expected_bits), + config, + )) } fn dispute_statements_weight(disputes: &[DisputeStatementSet]) -> Weight { @@ -381,6 +383,7 @@ pub mod pallet { data.backed_candidates.as_slice(), data.bitfields.as_slice(), data.disputes.as_slice(), + >::availability_cores().len(), &>::config(), ), DispatchClass::Mandatory, @@ -427,12 +430,13 @@ impl Pallet { ); let now = >::block_number(); + let expected_bits = >::availability_cores().len(); let mut candidate_weight = backed_candidates_weight::(&backed_candidates); let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); let disputes_weight = dispute_statements_weight::(&disputes); - let max_enact_candidates_weight = enact_candidates_weight::( - bitfields_count_ones(&signed_bitfields), + let mut max_enact_candidates_weight = enact_candidates_weight::( + bitfields_count_ones(&signed_bitfields, expected_bits), &>::config(), ); @@ -473,8 +477,6 @@ impl Pallet { } }; - let expected_bits = >::availability_cores().len(); - // Handle disputes logic. let current_session = >::session_index(); let disputed_bitfield = { From 615e78830a3d399ced22772817b822b9c8bfd601 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 15:31:57 -0800 Subject: [PATCH 19/27] Add cheap bitfields checks when counting ones --- runtime/parachains/src/paras_inherent.rs | 117 ++++++++++++++++------- 1 file changed, 82 insertions(+), 35 deletions(-) diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 5a4eb81c7b40..65b54f83e4fa 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -122,13 +122,28 @@ impl WeightInfo for TestWeightInfo { fn bitfields_count_ones( bitfields: &[UncheckedSignedAvailabilityBitfield], expected_bits: usize, + validator_count: usize, ) -> usize { + let mut last_index = None; bitfields .iter() .take(MAX_UNCHECKED_BITFIELD_ITERATIONS) .fold( BitVec::::repeat(false, expected_bits), - |acc: BitVec, cur| acc | cur.unchecked_payload().0.clone(), + |acc: BitVec, cur| { + if cheap_bitfield_checks( + cur, + expected_bits, + validator_count, + last_index, + &FullCheck::Skip, + ) { + last_index = Some(cur.unchecked_validator_index()); + acc | cur.unchecked_payload().0.clone() + } else { + acc + } + }, ) .count_ones() } @@ -139,13 +154,14 @@ fn paras_inherent_total_weight( bitfields: &[UncheckedSignedAvailabilityBitfield], disputes: &[DisputeStatementSet], expected_bits: usize, + validator_count: usize, config: &HostConfiguration, ) -> Weight { backed_candidates_weight::(backed_candidates) .saturating_add(signed_bitfields_weight::(bitfields.len())) .saturating_add(dispute_statements_weight::(disputes)) .saturating_add(enact_candidates_weight::( - bitfields_count_ones(bitfields, expected_bits), + bitfields_count_ones(bitfields, expected_bits, validator_count), config, )) } @@ -383,7 +399,8 @@ pub mod pallet { data.backed_candidates.as_slice(), data.bitfields.as_slice(), data.disputes.as_slice(), - >::availability_cores().len(), + scheduler::Pallet::::availability_cores().len(), + shared::Pallet::::active_validator_keys().len(), &>::config(), ), DispatchClass::Mandatory, @@ -436,7 +453,11 @@ impl Pallet { let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); let disputes_weight = dispute_statements_weight::(&disputes); let mut max_enact_candidates_weight = enact_candidates_weight::( - bitfields_count_ones(&signed_bitfields, expected_bits), + bitfields_count_ones( + &signed_bitfields, + expected_bits, + shared::Pallet::::active_validator_keys().len(), + ), &>::config(), ); @@ -765,6 +786,7 @@ impl Pallet { // Assure the maximum block weight is adhered. let max_block_weight = ::BlockWeights::get().max_block; + // TODO subtract enactment weight - return sanitized bitfields from enactment let _consumed_weight = apply_weight_limit::( &mut backed_candidates, &mut bitfields, @@ -957,6 +979,7 @@ fn apply_weight_limit( /// /// `full_check` determines if validator signatures are checked. If `::Yes`, /// bitfields that have an invalid signature will be filtered out. +// TODO also return all bitfields OR'ed together pub(crate) fn sanitize_bitfields( unchecked_bitfields: UncheckedSignedAvailabilityBitfields, disputed_bitfield: DisputedBitfield, @@ -981,14 +1004,13 @@ pub(crate) fn sanitize_bitfields( let signing_context = SigningContext { parent_hash, session_index }; for unchecked_bitfield in unchecked_bitfields { // Find and skip invalid bitfields. - if unchecked_bitfield.unchecked_payload().0.len() != expected_bits { - log::trace!( - target: LOG_TARGET, - "[{:?}] bad bitfield length: {} != {:?}", - full_check, - unchecked_bitfield.unchecked_payload().0.len(), - expected_bits, - ); + if !cheap_bitfield_checks( + &unchecked_bitfield, + expected_bits, + validators.len(), + last_index, + &full_check, + ) { continue } @@ -1005,29 +1027,6 @@ pub(crate) fn sanitize_bitfields( } let validator_index = unchecked_bitfield.unchecked_validator_index(); - - if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) { - log::trace!( - target: LOG_TARGET, - "[{:?}] bitfield validator index is not greater than last: !({:?} < {})", - full_check, - last_index.as_ref().map(|x| x.0), - validator_index.0 - ); - continue - } - - if unchecked_bitfield.unchecked_validator_index().0 as usize >= validators.len() { - log::trace!( - target: LOG_TARGET, - "[{:?}] bitfield validator index is out of bounds: {} >= {}", - full_check, - validator_index.0, - validators.len(), - ); - continue - } - let validator_public = &validators[validator_index.0 as usize]; if let FullCheck::Yes = full_check { @@ -1047,6 +1046,54 @@ pub(crate) fn sanitize_bitfields( bitfields } +// Returns `true` iff the checks pass. +fn cheap_bitfield_checks( + unchecked_bitfield: &UncheckedSignedAvailabilityBitfield, + expected_bits: usize, + validator_count: usize, + last_index: Option, + full_check: &FullCheck, +) -> bool { + if unchecked_bitfield.unchecked_payload().0.len() != expected_bits { + log::trace!( + target: LOG_TARGET, + "[{:?}] bad bitfield length: {} != {:?}", + full_check, + unchecked_bitfield.unchecked_payload().0.len(), + expected_bits, + ); + + return false + } + + let validator_index = unchecked_bitfield.unchecked_validator_index(); + if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) { + log::trace!( + target: LOG_TARGET, + "[{:?}] bitfield validator index is not greater than last: !({:?} < {})", + full_check, + last_index.as_ref().map(|x| x.0), + validator_index.0 + ); + + return false + } + + if unchecked_bitfield.unchecked_validator_index().0 as usize >= validator_count { + log::trace!( + target: LOG_TARGET, + "[{:?}] bitfield validator index is out of bounds: {} >= {}", + full_check, + validator_index.0, + validator_count, + ); + + return false + } + + true +} + /// Filter out any candidates that have a concluded invalid dispute. /// /// `scheduled` follows the same naming scheme as provided in the From 14faefb6eee5c35a6c5bd3cf477b798bea0a37ad Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 16:18:42 -0800 Subject: [PATCH 20/27] Track cores voted for in sanitize_bitfields --- runtime/parachains/src/inclusion.rs | 2 +- runtime/parachains/src/paras_inherent.rs | 67 +++++++++++++++++------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion.rs index 9dbdcb5ef925..9c677fd35457 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion.rs @@ -420,7 +420,7 @@ impl Pallet { let session_index = shared::Pallet::::session_index(); let parent_hash = frame_system::Pallet::::parent_hash(); - let checked_bitfields = sanitize_bitfields::( + let (checked_bitfields, _) = sanitize_bitfields::( signed_bitfields, disputed_bitfield, expected_bits, diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs index 65b54f83e4fa..8f3422f6e9bb 100644 --- a/runtime/parachains/src/paras_inherent.rs +++ b/runtime/parachains/src/paras_inherent.rs @@ -161,7 +161,7 @@ fn paras_inherent_total_weight( .saturating_add(signed_bitfields_weight::(bitfields.len())) .saturating_add(dispute_statements_weight::(disputes)) .saturating_add(enact_candidates_weight::( - bitfields_count_ones(bitfields, expected_bits, validator_count), + bitfields_count_ones(bitfields, expected_bits, validator_count) as u32, config, )) } @@ -184,7 +184,7 @@ fn signed_bitfields_weight(bitfields_len: usize) -> Weight { // Calculate the worst case weight for enacting the given number of candidates. fn enact_candidates_weight( - candidate_count: usize, + candidate_count: u32, config: &HostConfiguration, ) -> Weight { >::enact_candidate_weight( @@ -457,7 +457,7 @@ impl Pallet { &signed_bitfields, expected_bits, shared::Pallet::::active_validator_keys().len(), - ), + ) as u32, &>::config(), ); @@ -662,7 +662,7 @@ impl Pallet { T::DisputesHandler::filter_multi_dispute_data(&mut disputes); - let (mut backed_candidates, mut bitfields) = + let (mut backed_candidates, mut bitfields, cores_with_votes) = frame_support::storage::with_transaction(|| { // we don't care about fresh or not disputes // this writes them to storage, so let's query it via those means @@ -722,7 +722,7 @@ impl Pallet { // The following 3 calls are equiv to a call to `process_bitfields` // but we can retain access to `bitfields`. - let bitfields = sanitize_bitfields::( + let (bitfields, cores_with_votes) = sanitize_bitfields::( bitfields, disputed_bitfield, expected_bits, @@ -778,6 +778,8 @@ impl Pallet { backed_candidates, // filtered bitfields bitfields, + // number of cores voted on by filtered bitfields + cores_with_votes.count_ones(), )) }); @@ -791,8 +793,10 @@ impl Pallet { &mut backed_candidates, &mut bitfields, &mut disputes, + cores_with_votes as u32, max_block_weight, &mut rng, + &>::config(), ); Some(ParachainsInherentData:: { @@ -896,17 +900,25 @@ fn apply_weight_limit( candidates: &mut Vec::Hash>>, bitfields: &mut UncheckedSignedAvailabilityBitfields, disputes: &mut MultiDisputeStatementSet, + cores_with_votes: u32, max_block_weight: Weight, rng: &mut rand_chacha::ChaChaRng, + config: &HostConfiguration, ) -> Weight { // include as many disputes as possible, always let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); let total_candidates_weight = backed_candidates_weight::(candidates.as_slice()); + // we include the worst case weight of enacting the candidates voted for by the bitfields let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); - let total = total_bitfields_weight.saturating_add(total_candidates_weight); + let total_enact_candidates_weight = + enact_candidates_weight::(cores_with_votes, config); + + let total = total_bitfields_weight + .saturating_add(total_candidates_weight) + .saturating_add(total_enact_candidates_weight); // candidates + bitfields fit into the block if remaining_weight >= total { @@ -925,7 +937,10 @@ fn apply_weight_limit( // There is weight remaining to be consumed by a subset of candidates // which are going to be picked now. - if let Some(remaining_weight) = remaining_weight.checked_sub(total_bitfields_weight) { + if let Some(remaining_weight) = remaining_weight + .checked_sub(total_bitfields_weight) + .and_then(|r| r.checked_sub(total_enact_candidates_weight)) + { let (acc_candidate_weight, indices) = random_sel::::Hash>, _>( rng, @@ -953,7 +968,7 @@ fn apply_weight_limit( // accounting for that when tracking individual bitfield weight, thus the weight here likely // ends up being an underestimate. |_| <::WeightInfo as WeightInfo>::enter_bitfields(), - remaining_weight, + remaining_weight.saturating_sub(total_enact_candidates_weight), ); bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok()); @@ -988,7 +1003,7 @@ pub(crate) fn sanitize_bitfields( session_index: SessionIndex, validators: &[ValidatorId], full_check: FullCheck, -) -> UncheckedSignedAvailabilityBitfields { +) -> (UncheckedSignedAvailabilityBitfields, usize) { let mut bitfields = Vec::with_capacity(unchecked_bitfields.len()); let mut last_index: Option = None; @@ -997,10 +1012,11 @@ pub(crate) fn sanitize_bitfields( // This is a system logic error that should never occur, but we want to handle it gracefully // so we just drop all bitfields log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits"); - return vec![] + return (vec![], 0) } let all_zeros = BitVec::::repeat(false, expected_bits); + let mut cores_with_votes = BitVec::::repeat(false, expected_bits); let signing_context = SigningContext { parent_hash, session_index }; for unchecked_bitfield in unchecked_bitfields { // Find and skip invalid bitfields. @@ -1033,17 +1049,20 @@ pub(crate) fn sanitize_bitfields( if let Ok(signed_bitfield) = unchecked_bitfield.try_into_checked(&signing_context, validator_public) { + cores_with_votes |= signed_bitfield.payload().0.clone(); bitfields.push(signed_bitfield.into_unchecked()); } else { log::warn!(target: LOG_TARGET, "Invalid bitfield signature"); }; } else { + cores_with_votes |= unchecked_bitfield.unchecked_payload().0.clone(); bitfields.push(unchecked_bitfield); } last_index = Some(validator_index); } - bitfields + + (bitfields, cores_with_votes.count_ones()) } // Returns `true` iff the checks pass. @@ -2032,7 +2051,7 @@ mod tests { &validator_public[..], FullCheck::Skip, ), - unchecked_bitfields.clone() + (unchecked_bitfields.clone(), expected_bits) ); assert_eq!( sanitize_bitfields::( @@ -2044,7 +2063,7 @@ mod tests { &validator_public[..], FullCheck::Yes ), - unchecked_bitfields.clone() + (unchecked_bitfields.clone(), expected_bits) ); } @@ -2065,6 +2084,7 @@ mod tests { &validator_public[..], FullCheck::Yes ) + .0 .len(), 1 ); @@ -2078,6 +2098,7 @@ mod tests { &validator_public[..], FullCheck::Skip ) + .0 .len(), 1 ); @@ -2094,6 +2115,7 @@ mod tests { &validator_public[..], FullCheck::Yes ) + .0 .is_empty()); assert!(sanitize_bitfields::( unchecked_bitfields.clone(), @@ -2104,6 +2126,7 @@ mod tests { &validator_public[..], FullCheck::Skip ) + .0 .is_empty()); } @@ -2119,7 +2142,8 @@ mod tests { session_index, &validator_public[..shortened], FullCheck::Yes, - )[..], + ) + .0[..], &unchecked_bitfields[..shortened] ); assert_eq!( @@ -2131,7 +2155,8 @@ mod tests { session_index, &validator_public[..shortened], FullCheck::Skip, - )[..], + ) + .0[..], &unchecked_bitfields[..shortened] ); } @@ -2150,7 +2175,8 @@ mod tests { session_index, &validator_public[..], FullCheck::Yes - )[..], + ) + .0[..], &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] ); assert_eq!( @@ -2162,7 +2188,8 @@ mod tests { session_index, &validator_public[..], FullCheck::Skip - )[..], + ) + .0[..], &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] ); } @@ -2187,7 +2214,8 @@ mod tests { session_index, &validator_public[..], FullCheck::Yes - )[..], + ) + .0[..], &unchecked_bitfields[..last_bit_idx] ); assert_eq!( @@ -2199,7 +2227,8 @@ mod tests { session_index, &validator_public[..], FullCheck::Skip - )[..], + ) + .0[..], &unchecked_bitfields[..] ); } From 0c0c6a59dd79db57c066456d6778c4df11dc7967 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:15:22 -0800 Subject: [PATCH 21/27] Prepare paras_inherent for merging master --- runtime/parachains/src/paras_inherent.rs | 2380 ----------------- runtime/parachains/src/paras_inherent/misc.rs | 40 + runtime/parachains/src/paras_inherent/mod.rs | 1093 ++++++++ .../parachains/src/paras_inherent/tests.rs | 1119 ++++++++ .../parachains/src/paras_inherent/weights.rs | 178 ++ 5 files changed, 2430 insertions(+), 2380 deletions(-) delete mode 100644 runtime/parachains/src/paras_inherent.rs create mode 100644 runtime/parachains/src/paras_inherent/misc.rs create mode 100644 runtime/parachains/src/paras_inherent/mod.rs create mode 100644 runtime/parachains/src/paras_inherent/tests.rs create mode 100644 runtime/parachains/src/paras_inherent/weights.rs diff --git a/runtime/parachains/src/paras_inherent.rs b/runtime/parachains/src/paras_inherent.rs deleted file mode 100644 index 8f3422f6e9bb..000000000000 --- a/runtime/parachains/src/paras_inherent.rs +++ /dev/null @@ -1,2380 +0,0 @@ -// 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 crate::{ - configuration, - configuration::HostConfiguration, - disputes::DisputesHandler, - inclusion, - inclusion::{CandidateCheckContext, FullCheck}, - initializer, - scheduler::{self, CoreAssignment, FreedReason}, - shared, ump, -}; -use bitvec::{order::Lsb0 as BitOrderLsb0, prelude::BitVec}; -use frame_support::{ - inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, - pallet_prelude::*, - traits::Randomness, -}; -use frame_system::pallet_prelude::*; -use pallet_babe::{self, CurrentBlockRandomness}; -use primitives::v1::{ - BackedCandidate, CandidateHash, CoreIndex, DisputeStatementSet, - InherentData as ParachainsInherentData, MultiDisputeStatementSet, ScrapedOnChainVotes, - SessionIndex, SigningContext, UncheckedSignedAvailabilityBitfield, - UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, - PARACHAINS_INHERENT_IDENTIFIER, -}; -use rand::{seq::SliceRandom, SeedableRng}; - -use scale_info::TypeInfo; -use sp_runtime::traits::{Header as HeaderT, One}; -use sp_std::{ - cmp::Ordering, - collections::{btree_map::BTreeMap, btree_set::BTreeSet}, - prelude::*, - vec::Vec, -}; - -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - -const LOG_TARGET: &str = "runtime::inclusion-inherent"; -const MAX_UNCHECKED_BITFIELD_ITERATIONS: usize = 1_000; - -pub trait WeightInfo { - /// Variant over `v`, the count of dispute statements in a dispute statement set. This gives the - /// weight of a single dispute statement set. - fn enter_variable_disputes(v: u32) -> Weight; - /// The weight of one bitfield. - fn enter_bitfields() -> Weight; - /// Variant over `v`, the count of validity votes for a backed candidate. This gives the weight - /// of a single backed candidate. - fn enter_backed_candidates_variable(v: u32) -> Weight; - /// The weight of a single backed candidate with a code upgrade. - fn enter_backed_candidate_code_upgrade() -> Weight; -} - -pub struct TestWeightInfo; -// `WeightInfo` impl for unit and integration tests. Based off of the `max_block` weight for the -// mock. -#[cfg(not(feature = "runtime-benchmarks"))] -impl WeightInfo for TestWeightInfo { - fn enter_variable_disputes(v: u32) -> Weight { - // MAX Block Weight should fit 4 disputes - 80_000 * v as Weight + 80_000 - } - fn enter_bitfields() -> Weight { - // MAX Block Weight should fit 4 backed candidates - 40_000 as Weight - } - fn enter_backed_candidates_variable(v: u32) -> Weight { - // MAX Block Weight should fit 4 backed candidates - 40_000 * v as Weight + 40_000 - } - fn enter_backed_candidate_code_upgrade() -> Weight { - 0 - } -} -// To simplify benchmarks running as tests, we set all the weights to 0. `enter` will exit early -// when if the data causes it to be over weight, but we don't want that to block a benchmark from -// running as a test. -#[cfg(feature = "runtime-benchmarks")] -impl WeightInfo for TestWeightInfo { - fn enter_variable_disputes(_v: u32) -> Weight { - 0 - } - fn enter_bitfields() -> Weight { - 0 - } - fn enter_backed_candidates_variable(_v: u32) -> Weight { - 0 - } - fn enter_backed_candidate_code_upgrade() -> Weight { - 0 - } -} - -// OR together all bitfields and count the ones from the result. -// Note that this will only OR together the first `MAX_UNCHECKED_BITFIELD_ITERATIONS` to avoid -// excessive iteration. -fn bitfields_count_ones( - bitfields: &[UncheckedSignedAvailabilityBitfield], - expected_bits: usize, - validator_count: usize, -) -> usize { - let mut last_index = None; - bitfields - .iter() - .take(MAX_UNCHECKED_BITFIELD_ITERATIONS) - .fold( - BitVec::::repeat(false, expected_bits), - |acc: BitVec, cur| { - if cheap_bitfield_checks( - cur, - expected_bits, - validator_count, - last_index, - &FullCheck::Skip, - ) { - last_index = Some(cur.unchecked_validator_index()); - acc | cur.unchecked_payload().0.clone() - } else { - acc - } - }, - ) - .count_ones() -} - -// Note that this does a storage read. -fn paras_inherent_total_weight( - backed_candidates: &[BackedCandidate<::Hash>], - bitfields: &[UncheckedSignedAvailabilityBitfield], - disputes: &[DisputeStatementSet], - expected_bits: usize, - validator_count: usize, - config: &HostConfiguration, -) -> Weight { - backed_candidates_weight::(backed_candidates) - .saturating_add(signed_bitfields_weight::(bitfields.len())) - .saturating_add(dispute_statements_weight::(disputes)) - .saturating_add(enact_candidates_weight::( - bitfields_count_ones(bitfields, expected_bits, validator_count) as u32, - config, - )) -} - -fn dispute_statements_weight(disputes: &[DisputeStatementSet]) -> Weight { - disputes - .iter() - .map(|d| { - <::WeightInfo as WeightInfo>::enter_variable_disputes( - d.statements.len() as u32 - ) - }) - .fold(0, |acc, x| acc.saturating_add(x)) -} - -fn signed_bitfields_weight(bitfields_len: usize) -> Weight { - <::WeightInfo as WeightInfo>::enter_bitfields() - .saturating_mul(bitfields_len as Weight) -} - -// Calculate the worst case weight for enacting the given number of candidates. -fn enact_candidates_weight( - candidate_count: u32, - config: &HostConfiguration, -) -> Weight { - >::enact_candidate_weight( - config.hrmp_max_parathread_inbound_channels, - config.hrmp_max_parachain_inbound_channels, - config.max_upward_message_num_per_candidate, - config.hrmp_max_message_num_per_candidate, - ) - .saturating_mul(candidate_count as Weight) -} - -fn backed_candidate_weight( - candidate: &BackedCandidate, -) -> Weight { - if candidate.candidate.commitments.new_validation_code.is_some() { - <::WeightInfo as WeightInfo>::enter_backed_candidate_code_upgrade() - } else { - <::WeightInfo as WeightInfo>::enter_backed_candidates_variable( - candidate.validity_votes.len() as u32, - ) - } -} - -// Calculate the max weight of the given candidates. -fn backed_candidates_weight( - candidates: &[BackedCandidate], -) -> Weight { - candidates - .iter() - .map(|c| backed_candidate_weight::(c)) - .fold(0, |acc, x| acc.saturating_add(x)) -} - -/// A helper trait to allow calling retain while getting access -/// to the index of the item in the `vec`. -trait IndexedRetain { - /// Retains only the elements specified by the predicate. - /// - /// In other words, remove all elements `e` residing at - /// index `i` such that `f(i, &e)` returns `false`. This method - /// operates in place, visiting each element exactly once in the - /// original order, and preserves the order of the retained elements. - fn indexed_retain(&mut self, f: impl FnMut(usize, &T) -> bool); -} - -impl IndexedRetain for Vec { - fn indexed_retain(&mut self, mut f: impl FnMut(usize, &T) -> bool) { - let mut idx = 0_usize; - self.retain(move |item| { - let ret = f(idx, item); - idx += 1_usize; - ret - }) - } -} - -/// A bitfield concerning concluded disputes for candidates -/// associated to the core index equivalent to the bit position. -#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub(crate) struct DisputedBitfield(pub(crate) BitVec); - -impl From> for DisputedBitfield { - fn from(inner: BitVec) -> Self { - Self(inner) - } -} - -#[cfg(test)] -impl DisputedBitfield { - /// Create a new bitfield, where each bit is set to `false`. - pub fn zeros(n: usize) -> Self { - Self::from(BitVec::::repeat(false, n)) - } -} - -pub use pallet::*; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::config] - #[pallet::disable_frame_system_supertrait_check] - pub trait Config: - inclusion::Config + scheduler::Config + initializer::Config + pallet_babe::Config - { - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - } - - #[pallet::error] - pub enum Error { - /// 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, - /// Disputed candidate that was concluded invalid. - CandidateConcludedInvalid, - /// The data given to the inherent will result in an overweight block. - InherentOverweight, - } - - /// 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. - #[pallet::storage] - pub(crate) type Included = StorageValue<_, ()>; - - /// Scraped on chain data for extracting resolved disputes as well as backing votes. - #[pallet::storage] - #[pallet::getter(fn on_chain_votes)] - pub(crate) type OnChainVotes = StorageValue<_, ScrapedOnChainVotes>; - - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(_: T::BlockNumber) -> Weight { - T::DbWeight::get().reads_writes(1, 1) // in on_finalize. - } - - fn on_finalize(_: T::BlockNumber) { - if Included::::take().is_none() { - panic!("Bitfields and heads must be included every block"); - } - } - } - - #[pallet::inherent] - impl ProvideInherent for Pallet { - type Call = Call; - type Error = MakeFatalError<()>; - const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER; - - fn create_inherent(data: &InherentData) -> Option { - let inherent_data = Self::create_inherent_inner(data)?; - // Sanity check: session changes can invalidate an inherent, - // and we _really_ don't want that to happen. - // See - - // Calling `Self::enter` here is a safe-guard, to avoid any discrepancy between on-chain checks - // (`enter`) and the off-chain checks by the block author (this function). Once we are confident - // in all the logic in this module this check should be removed to optimize performance. - - let inherent_data = match Self::enter_inner(inherent_data.clone(), FullCheck::Skip) { - Ok(_) => inherent_data, - Err(err) => { - log::error!( - target: LOG_TARGET, - "dropping paras inherent data because they produced \ - an invalid paras inherent: {:?}", - err.error, - ); - - ParachainsInherentData { - bitfields: Vec::new(), - backed_candidates: Vec::new(), - disputes: Vec::new(), - parent_header: inherent_data.parent_header, - } - }, - }; - - Some(Call::enter { data: inherent_data }) - } - - fn is_inherent(call: &Self::Call) -> bool { - matches!(call, Call::enter { .. }) - } - } - - /// Collect all freed cores based on storage data. (i.e. append cores freed from timeouts to - /// the given `freed_concluded`). - /// - /// The parameter `freed_concluded` contains all core indicies that became - /// free due to candidate that became available. - pub(crate) fn collect_all_freed_cores( - freed_concluded: I, - ) -> BTreeMap - where - I: core::iter::IntoIterator, - T: Config, - { - // 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 freed = freed_concluded - .into_iter() - .map(|(c, _hash)| (c, FreedReason::Concluded)) - .chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut))) - .collect::>(); - freed - } - - #[pallet::call] - impl Pallet { - /// Enter the paras inherent. This will process disputes, bitfields and backed candidates. - #[pallet::weight(( - paras_inherent_total_weight::( - data.backed_candidates.as_slice(), - data.bitfields.as_slice(), - data.disputes.as_slice(), - scheduler::Pallet::::availability_cores().len(), - shared::Pallet::::active_validator_keys().len(), - &>::config(), - ), - DispatchClass::Mandatory, - ))] - pub fn enter( - origin: OriginFor, - data: ParachainsInherentData, - ) -> DispatchResultWithPostInfo { - ensure_none(origin)?; - - ensure!(!Included::::exists(), Error::::TooManyInclusionInherents); - Included::::set(Some(())); - - Self::enter_inner(data, FullCheck::Yes) - } - } -} - -impl Pallet { - pub(crate) fn enter_inner( - data: ParachainsInherentData, - full_check: FullCheck, - ) -> DispatchResultWithPostInfo { - let ParachainsInherentData { - bitfields: mut signed_bitfields, - mut backed_candidates, - parent_header, - mut disputes, - } = data; - - log::debug!( - target: LOG_TARGET, - "[enter] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}", - signed_bitfields.len(), - backed_candidates.len(), - disputes.len() - ); - - // 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, - ); - - let now = >::block_number(); - let expected_bits = >::availability_cores().len(); - - let mut candidate_weight = backed_candidates_weight::(&backed_candidates); - let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); - let disputes_weight = dispute_statements_weight::(&disputes); - let mut max_enact_candidates_weight = enact_candidates_weight::( - bitfields_count_ones( - &signed_bitfields, - expected_bits, - shared::Pallet::::active_validator_keys().len(), - ) as u32, - &>::config(), - ); - - let max_block_weight = ::BlockWeights::get().max_block; - - // Potentially trim inherent data to ensure processing will be within weight limits - let total_weight = { - if candidate_weight - .saturating_add(bitfields_weight) - .saturating_add(disputes_weight) - .saturating_add(max_enact_candidates_weight) > - max_block_weight - { - // if the total weight is over the max block weight, first try clearing backed - // candidates and bitfields. - backed_candidates.clear(); - candidate_weight = 0; - signed_bitfields.clear(); - bitfields_weight = 0; - max_enact_candidates_weight = 0; - } - - if disputes_weight > max_block_weight { - // if disputes are by themselves overweight already, trim the disputes. - debug_assert!(candidate_weight == 0 && bitfields_weight == 0); - - let entropy = compute_entropy::(parent_hash); - let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into()); - - let remaining_weight = - limit_disputes::(&mut disputes, max_block_weight, &mut rng); - max_block_weight.saturating_sub(remaining_weight) - } else { - candidate_weight - .saturating_add(bitfields_weight) - .saturating_add(disputes_weight) - .saturating_add(max_enact_candidates_weight) - } - }; - - // Handle disputes logic. - let current_session = >::session_index(); - let disputed_bitfield = { - let new_current_dispute_sets: Vec<_> = disputes - .iter() - .filter(|s| s.session == current_session) - .map(|s| (s.session, s.candidate_hash)) - .collect(); - - // Note that `provide_multi_dispute_data` will iterate, verify, and import each - // dispute; so the input here must be reasonably bounded. - let _ = T::DisputesHandler::provide_multi_dispute_data(disputes.clone())?; - if T::DisputesHandler::is_frozen() { - // The relay chain we are currently on is invalid. Proceed no further on parachains. - return Ok(Some(dispute_statements_weight::(&disputes)).into()) - } - - let mut freed_disputed = if !new_current_dispute_sets.is_empty() { - let concluded_invalid_disputes = new_current_dispute_sets - .iter() - .filter(|(session, candidate)| { - T::DisputesHandler::concluded_invalid(*session, *candidate) - }) - .map(|(_, candidate)| *candidate) - .collect::>(); - - let freed_disputed = - >::collect_disputed(&concluded_invalid_disputes) - .into_iter() - .map(|core| (core, FreedReason::Concluded)) - .collect(); - freed_disputed - } else { - Vec::new() - }; - - // Create a bit index from the set of core indices where each index corresponds to - // a core index that was freed due to a dispute. - let disputed_bitfield = create_disputed_bitfield( - expected_bits, - freed_disputed.iter().map(|(core_index, _)| core_index), - ); - - if !freed_disputed.is_empty() { - // unstable sort is fine, because core indices are unique - // i.e. the same candidate can't occupy 2 cores at once. - freed_disputed.sort_unstable_by_key(|pair| pair.0); // sort by core index - >::free_cores(freed_disputed); - } - - disputed_bitfield - }; - - // Process new availability bitfields, yielding any availability cores whose - // work has now concluded. - let (freed_concluded, actual_enact_candidates_weight) = - >::process_bitfields( - expected_bits, - signed_bitfields, - disputed_bitfield, - >::core_para, - ); - - // Inform the disputes module of all included candidates. - for (_, candidate_hash) in &freed_concluded { - T::DisputesHandler::note_included(current_session, *candidate_hash, now); - } - - let freed = collect_all_freed_cores::(freed_concluded.iter().cloned()); - - >::clear(); - >::schedule(freed, now); - - let scheduled = >::scheduled(); - let backed_candidates = sanitize_backed_candidates::( - parent_hash, - backed_candidates, - move |_candidate_index: usize, backed_candidate: &BackedCandidate| -> bool { - ::DisputesHandler::concluded_invalid(current_session, backed_candidate.hash()) - // `fn process_candidates` does the verification checks - }, - &scheduled[..], - ); - - // Process backed candidates according to scheduled cores. - let parent_storage_root = parent_header.state_root().clone(); - let inclusion::ProcessedCandidates::<::Hash> { - core_indices: occupied, - candidate_receipt_with_backing_validator_indices, - } = >::process_candidates( - parent_storage_root, - backed_candidates, - scheduled, - >::group_validators, - full_check, - )?; - - // The number of disputes included in a block is - // limited by the weight as well as the number of candidate blocks. - OnChainVotes::::put(ScrapedOnChainVotes::<::Hash> { - session: current_session, - backing_validators_per_candidate: candidate_receipt_with_backing_validator_indices, - disputes, - }); - - // Note which of the scheduled cores were actually occupied by a backed candidate. - >::occupied(&occupied); - - // Give some time slice to dispatch pending upward messages. - // this is max config.ump_service_total_weight - let _ump_weight = >::process_pending_upward_messages(); - - let actual_total_weight = total_weight - // subtract the max enact candidate weight, - .saturating_sub(max_enact_candidates_weight) - // and add back the actual enact candidate weight - .saturating_add(actual_enact_candidates_weight); - - Ok(Some(actual_total_weight).into()) - } -} - -impl Pallet { - /// Create the `ParachainsInherentData` that gets passed to [`Self::enter`] in [`Self::create_inherent`]. - /// This code is pulled out of [`Self::create_inherent`] so it can be unit tested. - fn create_inherent_inner(data: &InherentData) -> Option> { - let ParachainsInherentData:: { - bitfields, - backed_candidates, - mut disputes, - parent_header, - } = 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 - }, - }; - - log::debug!( - target: LOG_TARGET, - "[create_inherent_inner] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}", - bitfields.len(), - backed_candidates.len(), - disputes.len() - ); - - let parent_hash = >::parent_hash(); - - if parent_hash != parent_header.hash() { - log::warn!( - target: LOG_TARGET, - "ParachainsInherentData references a different parent header hash than frame" - ); - return None - } - - let current_session = >::session_index(); - let expected_bits = >::availability_cores().len(); - let validator_public = shared::Pallet::::active_validator_keys(); - - T::DisputesHandler::filter_multi_dispute_data(&mut disputes); - - let (mut backed_candidates, mut bitfields, cores_with_votes) = - frame_support::storage::with_transaction(|| { - // we don't care about fresh or not disputes - // this writes them to storage, so let's query it via those means - // if this fails for whatever reason, that's ok - let _ = - T::DisputesHandler::provide_multi_dispute_data(disputes.clone()).map_err(|e| { - log::warn!( - target: LOG_TARGET, - "MultiDisputesData failed to update: {:?}", - e - ); - e - }); - - // Contains the disputes that are concluded in the current session only, - // since these are the only ones that are relevant for the occupied cores - // and lightens the load on `collect_disputed` significantly. - // Cores can't be occupied with candidates of the previous sessions, and only - // things with new votes can have just concluded. We only need to collect - // cores with disputes that conclude just now, because disputes that - // concluded longer ago have already had any corresponding cores cleaned up. - let current_concluded_invalid_disputes = disputes - .iter() - .filter(|dss| dss.session == current_session) - .map(|dss| (dss.session, dss.candidate_hash)) - .filter(|(session, candidate)| { - ::DisputesHandler::concluded_invalid(*session, *candidate) - }) - .map(|(_session, candidate)| candidate) - .collect::>(); - - // All concluded invalid disputes, that are relevant for the set of candidates - // the inherent provided. - let concluded_invalid_disputes = backed_candidates - .iter() - .map(|backed_candidate| backed_candidate.hash()) - .filter(|candidate| { - ::DisputesHandler::concluded_invalid(current_session, *candidate) - }) - .collect::>(); - - let mut freed_disputed: Vec<_> = - >::collect_disputed(¤t_concluded_invalid_disputes) - .into_iter() - .map(|core| (core, FreedReason::Concluded)) - .collect(); - - let disputed_bitfield = - create_disputed_bitfield(expected_bits, freed_disputed.iter().map(|(x, _)| x)); - - if !freed_disputed.is_empty() { - // unstable sort is fine, because core indices are unique - // i.e. the same candidate can't occupy 2 cores at once. - freed_disputed.sort_unstable_by_key(|pair| pair.0); // sort by core index - >::free_cores(freed_disputed.clone()); - } - - // The following 3 calls are equiv to a call to `process_bitfields` - // but we can retain access to `bitfields`. - let (bitfields, cores_with_votes) = sanitize_bitfields::( - bitfields, - disputed_bitfield, - expected_bits, - parent_hash, - current_session, - &validator_public[..], - FullCheck::Skip, - ); - - let (freed_concluded, _enact_candidate_weight) = - >::update_pending_availability_and_get_freed_cores::< - _, - false, - >( - expected_bits, - &validator_public[..], - bitfields.clone(), - >::core_para, - ); - - let freed = collect_all_freed_cores::(freed_concluded.iter().cloned()); - - >::clear(); - let now = >::block_number(); - >::schedule(freed, now); - - let scheduled = >::scheduled(); - - let relay_parent_number = now - One::one(); - - let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); - let backed_candidates = sanitize_backed_candidates::( - parent_hash, - backed_candidates, - move |candidate_idx: usize, - backed_candidate: &BackedCandidate<::Hash>| - -> bool { - // never include a concluded-invalid candidate - concluded_invalid_disputes.contains(&backed_candidate.hash()) || - // Instead of checking the candidates with code upgrades twice - // move the checking up here and skip it in the training wheels fallback. - // That way we avoid possible duplicate checks while assuring all - // backed candidates fine to pass on. - check_ctx - .verify_backed_candidate(parent_hash, candidate_idx, backed_candidate) - .is_err() - }, - &scheduled[..], - ); - - frame_support::storage::TransactionOutcome::Rollback(( - // filtered backed candidates - backed_candidates, - // filtered bitfields - bitfields, - // number of cores voted on by filtered bitfields - cores_with_votes.count_ones(), - )) - }); - - let entropy = compute_entropy::(parent_hash); - let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into()); - - // Assure the maximum block weight is adhered. - let max_block_weight = ::BlockWeights::get().max_block; - // TODO subtract enactment weight - return sanitized bitfields from enactment - let _consumed_weight = apply_weight_limit::( - &mut backed_candidates, - &mut bitfields, - &mut disputes, - cores_with_votes as u32, - max_block_weight, - &mut rng, - &>::config(), - ); - - Some(ParachainsInherentData:: { - bitfields, - backed_candidates, - disputes, - parent_header, - }) - } -} - -/// Derive a bitfield from dispute -pub(super) fn create_disputed_bitfield<'a, I>( - expected_bits: usize, - freed_cores: I, -) -> DisputedBitfield -where - I: 'a + IntoIterator, -{ - let mut bitvec = BitVec::repeat(false, expected_bits); - for core_idx in freed_cores { - let core_idx = core_idx.0 as usize; - if core_idx < expected_bits { - bitvec.set(core_idx, true); - } - } - DisputedBitfield::from(bitvec) -} - -/// Select a random subset, with preference for certain indices. -/// -/// Adds random items to the set until all candidates -/// are tried or the remaining weight is depleted. -/// -/// Returns the weight of all selected items from `selectables` -/// as well as their indices in ascending order. -fn random_sel Weight>( - rng: &mut rand_chacha::ChaChaRng, - selectables: Vec, - mut preferred_indices: Vec, - weight_fn: F, - weight_limit: Weight, -) -> (Weight, Vec) { - if selectables.is_empty() { - return (0 as Weight, Vec::new()) - } - // all indices that are not part of the preferred set - let mut indices = (0..selectables.len()) - .into_iter() - .filter(|idx| !preferred_indices.contains(idx)) - .collect::>(); - let mut picked_indices = Vec::with_capacity(selectables.len().saturating_sub(1)); - - let mut weight_acc = 0 as Weight; - - preferred_indices.shuffle(rng); - for preferred_idx in preferred_indices { - // preferred indices originate from outside - if let Some(item) = selectables.get(preferred_idx) { - let updated = weight_acc.saturating_add(weight_fn(item)); - if updated > weight_limit { - continue - } - weight_acc = updated; - picked_indices.push(preferred_idx); - } - } - - indices.shuffle(rng); - for idx in indices { - let item = &selectables[idx]; - let updated = weight_acc.saturating_add(weight_fn(item)); - - if updated > weight_limit { - continue - } - weight_acc = updated; - - picked_indices.push(idx); - } - - // sorting indices, so the ordering is retained - // unstable sorting is fine, since there are no duplicates - picked_indices.sort_unstable(); - (weight_acc, picked_indices) -} - -/// Considers an upper threshold that the inherent data must not exceed. -/// -/// If there is sufficient space, all disputes, all bitfields and all candidates -/// will be included. -/// -/// Otherwise tries to include all disputes, and then tries to fill the remaining space with bitfields and then candidates. -/// -/// The selection process is random. For candidates, there is an exception for code upgrades as they are preferred. -/// And for disputes, local and older disputes are preferred (see `limit_disputes`). -/// for backed candidates, since with a increasing number of parachains their chances of -/// inclusion become slim. All backed candidates are checked beforehands in `fn create_inherent_inner` -/// which guarantees sanity. -fn apply_weight_limit( - candidates: &mut Vec::Hash>>, - bitfields: &mut UncheckedSignedAvailabilityBitfields, - disputes: &mut MultiDisputeStatementSet, - cores_with_votes: u32, - max_block_weight: Weight, - rng: &mut rand_chacha::ChaChaRng, - config: &HostConfiguration, -) -> Weight { - // include as many disputes as possible, always - let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); - - let total_candidates_weight = backed_candidates_weight::(candidates.as_slice()); - - // we include the worst case weight of enacting the candidates voted for by the bitfields - let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); - - let total_enact_candidates_weight = - enact_candidates_weight::(cores_with_votes, config); - - let total = total_bitfields_weight - .saturating_add(total_candidates_weight) - .saturating_add(total_enact_candidates_weight); - - // candidates + bitfields fit into the block - if remaining_weight >= total { - return total - } - - // Prefer code upgrades, they tend to be large and hence stand no chance to be picked - // late while maintaining the weight bounds - let preferred_indices = candidates - .iter() - .enumerate() - .filter_map(|(idx, candidate)| { - candidate.candidate.commitments.new_validation_code.as_ref().map(|_code| idx) - }) - .collect::>(); - - // There is weight remaining to be consumed by a subset of candidates - // which are going to be picked now. - if let Some(remaining_weight) = remaining_weight - .checked_sub(total_bitfields_weight) - .and_then(|r| r.checked_sub(total_enact_candidates_weight)) - { - let (acc_candidate_weight, indices) = - random_sel::::Hash>, _>( - rng, - candidates.clone(), - preferred_indices, - |c| backed_candidate_weight::(c), - remaining_weight, - ); - candidates.indexed_retain(|idx, _backed_candidate| indices.binary_search(&idx).is_ok()); - // pick all bitfields, and - // fill the remaining space with candidates - let total = acc_candidate_weight.saturating_add(total_bitfields_weight); - return total - } - - candidates.clear(); - - // insufficient space for even the bitfields alone, so only try to fit as many of those - // into the block and skip the candidates entirely - let (total, indices) = random_sel::( - rng, - bitfields.clone(), - vec![], - // bitfields can lead to enacting a candidate; however we don't have a good way of - // accounting for that when tracking individual bitfield weight, thus the weight here likely - // ends up being an underestimate. - |_| <::WeightInfo as WeightInfo>::enter_bitfields(), - remaining_weight.saturating_sub(total_enact_candidates_weight), - ); - - bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok()); - - total -} - -/// Filter bitfields based on freed core indices, validity, and other sanity checks. -/// -/// Do sanity checks on the bitfields: -/// -/// 1. no more than one bitfield per validator -/// 2. bitfields are ascending by validator index. -/// 3. each bitfield has exactly `expected_bits` -/// 4. signature is valid -/// 5. remove any disputed core indices -/// -/// If any of those is not passed, the bitfield is dropped. -/// -/// While this function technically returns a set of unchecked bitfields, -/// they were actually checked and filtered to allow using it in both -/// cases, as `filtering` and `checking` stage. -/// -/// `full_check` determines if validator signatures are checked. If `::Yes`, -/// bitfields that have an invalid signature will be filtered out. -// TODO also return all bitfields OR'ed together -pub(crate) fn sanitize_bitfields( - unchecked_bitfields: UncheckedSignedAvailabilityBitfields, - disputed_bitfield: DisputedBitfield, - expected_bits: usize, - parent_hash: T::Hash, - session_index: SessionIndex, - validators: &[ValidatorId], - full_check: FullCheck, -) -> (UncheckedSignedAvailabilityBitfields, usize) { - let mut bitfields = Vec::with_capacity(unchecked_bitfields.len()); - - let mut last_index: Option = None; - - if disputed_bitfield.0.len() != expected_bits { - // This is a system logic error that should never occur, but we want to handle it gracefully - // so we just drop all bitfields - log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits"); - return (vec![], 0) - } - - let all_zeros = BitVec::::repeat(false, expected_bits); - let mut cores_with_votes = BitVec::::repeat(false, expected_bits); - let signing_context = SigningContext { parent_hash, session_index }; - for unchecked_bitfield in unchecked_bitfields { - // Find and skip invalid bitfields. - if !cheap_bitfield_checks( - &unchecked_bitfield, - expected_bits, - validators.len(), - last_index, - &full_check, - ) { - continue - } - - if unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() != - all_zeros - { - log::trace!( - target: LOG_TARGET, - "[{:?}] bitfield contains disputed cores: {:?}", - full_check, - unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() - ); - continue - } - - let validator_index = unchecked_bitfield.unchecked_validator_index(); - let validator_public = &validators[validator_index.0 as usize]; - - if let FullCheck::Yes = full_check { - if let Ok(signed_bitfield) = - unchecked_bitfield.try_into_checked(&signing_context, validator_public) - { - cores_with_votes |= signed_bitfield.payload().0.clone(); - bitfields.push(signed_bitfield.into_unchecked()); - } else { - log::warn!(target: LOG_TARGET, "Invalid bitfield signature"); - }; - } else { - cores_with_votes |= unchecked_bitfield.unchecked_payload().0.clone(); - bitfields.push(unchecked_bitfield); - } - - last_index = Some(validator_index); - } - - (bitfields, cores_with_votes.count_ones()) -} - -// Returns `true` iff the checks pass. -fn cheap_bitfield_checks( - unchecked_bitfield: &UncheckedSignedAvailabilityBitfield, - expected_bits: usize, - validator_count: usize, - last_index: Option, - full_check: &FullCheck, -) -> bool { - if unchecked_bitfield.unchecked_payload().0.len() != expected_bits { - log::trace!( - target: LOG_TARGET, - "[{:?}] bad bitfield length: {} != {:?}", - full_check, - unchecked_bitfield.unchecked_payload().0.len(), - expected_bits, - ); - - return false - } - - let validator_index = unchecked_bitfield.unchecked_validator_index(); - if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) { - log::trace!( - target: LOG_TARGET, - "[{:?}] bitfield validator index is not greater than last: !({:?} < {})", - full_check, - last_index.as_ref().map(|x| x.0), - validator_index.0 - ); - - return false - } - - if unchecked_bitfield.unchecked_validator_index().0 as usize >= validator_count { - log::trace!( - target: LOG_TARGET, - "[{:?}] bitfield validator index is out of bounds: {} >= {}", - full_check, - validator_index.0, - validator_count, - ); - - return false - } - - true -} - -/// Filter out any candidates that have a concluded invalid dispute. -/// -/// `scheduled` follows the same naming scheme as provided in the -/// guide: Currently `free` but might become `occupied`. -/// For the filtering here the relevant part is only the current `free` -/// state. -/// -/// `candidate_has_concluded_invalid_dispute` must return `true` if the candidate -/// is disputed, false otherwise -fn sanitize_backed_candidates< - T: crate::inclusion::Config, - F: FnMut(usize, &BackedCandidate) -> bool, ->( - relay_parent: T::Hash, - mut backed_candidates: Vec>, - mut candidate_has_concluded_invalid_dispute_or_is_invalid: F, - scheduled: &[CoreAssignment], -) -> Vec> { - // Remove any candidates that were concluded invalid. - backed_candidates.indexed_retain(move |idx, backed_candidate| { - !candidate_has_concluded_invalid_dispute_or_is_invalid(idx, backed_candidate) - }); - - // Assure the backed candidate's `ParaId`'s core is free. - // This holds under the assumption that `Scheduler::schedule` is called _before_. - // Also checks the candidate references the correct relay parent. - let scheduled_paras_set = scheduled - .into_iter() - .map(|core_assignment| core_assignment.para_id) - .collect::>(); - backed_candidates.retain(|backed_candidate| { - let desc = backed_candidate.descriptor(); - desc.relay_parent == relay_parent && scheduled_paras_set.contains(&desc.para_id) - }); - - backed_candidates -} - -/// Derive entropy from babe provided per block randomness. -/// -/// In the odd case none is available, uses the `parent_hash` and -/// a const value, while emitting a warning. -fn compute_entropy(parent_hash: T::Hash) -> [u8; 32] { - const CANDIDATE_SEED_SUBJECT: [u8; 32] = *b"candidate-seed-selection-subject"; - let vrf_random = CurrentBlockRandomness::::random(&CANDIDATE_SEED_SUBJECT[..]).0; - let mut entropy: [u8; 32] = CANDIDATE_SEED_SUBJECT.clone(); - if let Some(vrf_random) = vrf_random { - entropy.as_mut().copy_from_slice(vrf_random.as_ref()); - } else { - // in case there is no vrf randomness present, we utilize the relay parent - // as seed, it's better than a static value. - log::warn!(target: LOG_TARGET, "CurrentBlockRandomness did not provide entropy"); - entropy.as_mut().copy_from_slice(parent_hash.as_ref()); - } - entropy -} - -/// Limit disputes in place. -/// -/// Returns the unused weight of `remaining_weight`. -fn limit_disputes( - disputes: &mut MultiDisputeStatementSet, - remaining_weight: Weight, - rng: &mut rand_chacha::ChaChaRng, -) -> Weight { - let mut remaining_weight = remaining_weight; - let disputes_weight = dispute_statements_weight::(&disputes); - if disputes_weight > remaining_weight { - // Sort the dispute statements according to the following prioritization: - // 1. Prioritize local disputes over remote disputes. - // 2. Prioritize older disputes over newer disputes. - disputes.sort_by(|a, b| { - let a_local_block = T::DisputesHandler::included_state(a.session, a.candidate_hash); - let b_local_block = T::DisputesHandler::included_state(b.session, b.candidate_hash); - match (a_local_block, b_local_block) { - // Prioritize local disputes over remote disputes. - (None, Some(_)) => Ordering::Greater, - (Some(_), None) => Ordering::Less, - // For local disputes, prioritize those that occur at an earlier height. - (Some(a_height), Some(b_height)) => a_height.cmp(&b_height), - // Prioritize earlier remote disputes using session as rough proxy. - (None, None) => a.session.cmp(&b.session), - } - }); - - // Since the disputes array is sorted, we may use binary search to find the beginning of - // remote disputes - let idx = disputes - .binary_search_by(|probe| { - if T::DisputesHandler::included_state(probe.session, probe.candidate_hash).is_some() - { - Ordering::Greater - } else { - Ordering::Less - } - }) - // The above predicate will never find an item and therefore we are guaranteed to obtain - // an error, which we can safely unwrap. QED. - .unwrap_err(); - - // Due to the binary search predicate above, the index computed will constitute the beginning - // of the remote disputes sub-array - let remote_disputes = disputes.split_off(idx); - - // Select disputes in-order until the remaining weight is attained - disputes.retain(|d| { - let dispute_weight = <::WeightInfo as WeightInfo>::enter_variable_disputes( - d.statements.len() as u32, - ); - if remaining_weight >= dispute_weight { - remaining_weight -= dispute_weight; - true - } else { - false - } - }); - - // Compute the statements length of all remote disputes - let d = remote_disputes.iter().map(|d| d.statements.len() as u32).collect::>(); - - // Select remote disputes at random until the block is full - let (acc_remote_disputes_weight, indices) = random_sel::( - rng, - d, - vec![], - |v| <::WeightInfo as WeightInfo>::enter_variable_disputes(*v), - remaining_weight, - ); - - // Collect all remote disputes - let mut remote_disputes = - indices.into_iter().map(|idx| disputes[idx].clone()).collect::>(); - - // Construct the full list of selected disputes - disputes.append(&mut remote_disputes); - - // Update the remaining weight - remaining_weight = remaining_weight.saturating_sub(acc_remote_disputes_weight); - } - - remaining_weight -} - -#[cfg(test)] -mod tests { - use super::*; - - // In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl - // that uses 0 for all the weights. Because all the weights are 0, the tests that rely on - // weights for limiting data will fail, so we don't run them when using the benchmark feature. - #[cfg(not(feature = "runtime-benchmarks"))] - mod enter { - use super::*; - use crate::{ - builder::{Bench, BenchBuilder}, - mock::{new_test_ext, MockGenesisConfig, Test}, - }; - use frame_support::assert_ok; - use sp_std::collections::btree_map::BTreeMap; - - struct TestConfig { - dispute_statements: BTreeMap, - dispute_sessions: Vec, - backed_and_concluding: BTreeMap, - num_validators_per_core: u32, - includes_code_upgrade: Option, - } - - fn make_inherent_data( - TestConfig { - dispute_statements, - dispute_sessions, - backed_and_concluding, - num_validators_per_core, - includes_code_upgrade, - }: TestConfig, - ) -> Bench { - BenchBuilder::::new() - .set_max_validators((dispute_sessions.len() as u32) * num_validators_per_core) - .set_max_validators_per_core(num_validators_per_core) - .set_dispute_statements(dispute_statements) - .build(backed_and_concluding, dispute_sessions.as_slice(), includes_code_upgrade) - } - - #[test] - // Validate that if we create 2 backed candidates which are assigned to 2 cores that will be freed via - // becoming fully available, the backed candidates will not be filtered out in `create_inherent` and - // will not cause `enter` to early. - fn include_backed_candidates() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - backed_and_concluding.insert(0, 1); - backed_and_concluding.insert(1, 1); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0], - backed_and_concluding, - num_validators_per_core: 1, - includes_code_upgrade: None, - }); - - // We expect the scenario to have cores 0 & 1 with pending availability. The backed - // candidates are also created for cores 0 & 1, so once the pending available - // become fully available those cores are marked as free and scheduled for the backed - // candidates. - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (2 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 2); - // * 1 backed candidate per core (2 cores) - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 0 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 0); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - assert_eq!( - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(), - expected_para_inherent_data - ); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 backed candidates - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data - )); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 2 - ); - }); - } - - #[test] - // Ensure that disputes are filtered out if the session is in the future. - fn filter_multi_dispute_data() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![ - 1, 2, 3, /* Session 3 too new, will get filtered out */ - ], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 3 disputes => 3 cores, 15 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 15); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - let multi_dispute_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Dispute for session that lies too far in the future should be filtered out - assert!(multi_dispute_inherent_data != expected_para_inherent_data); - - assert_eq!(multi_dispute_inherent_data.disputes.len(), 2); - - // Assert that the first 2 disputes are included - assert_eq!( - &multi_dispute_inherent_data.disputes[..2], - &expected_para_inherent_data.disputes[..2], - ); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - multi_dispute_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know there - // where no backed candidates included - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); - }); - } - - #[test] - // Ensure that when dispute data establishes an over weight block that we adequately - // filter out disputes according to our prioritization rule - fn limit_dispute_data() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - // No backed and concluding cores, so all cores will be fileld with disputesw - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes - backed_and_concluding, - num_validators_per_core: 6, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 18); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Expect that inherent data is filtered to include only 2 disputes - assert!(limit_inherent_data != expected_para_inherent_data); - - // Ensure that the included disputes are sorted by session - assert_eq!(limit_inherent_data.disputes.len(), 2); - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); - - assert_eq!( - // Ensure that our inherent data did not included backed candidates as expected - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); - }); - } - - #[test] - // Ensure that when dispute data establishes an over weight block that we abort - // due to an over weight block - fn limit_dispute_data_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - // No backed and concluding cores, so all cores will be fileld with disputesw - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes - backed_and_concluding, - num_validators_per_core: 6, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 18); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); - }); - } - - #[test] - // Ensure that when a block is over weight due to disputes, but there is still sufficient - // block weight to include a number of signed bitfields, the inherent data is filtered - // as expected - fn limit_dispute_data_ignore_backed_candidates() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 4, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) - assert_eq!(expected_para_inherent_data.bitfields.len(), 20); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - assert!(limit_inherent_data != expected_para_inherent_data); - - // Three disputes is over weight (see previous test), so we expect to only see 2 disputes - assert_eq!(limit_inherent_data.disputes.len(), 2); - // Ensure disputes are filtered as expected - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - // Ensure all bitfields are included as these are still not over weight - assert_eq!( - limit_inherent_data.bitfields.len(), - expected_para_inherent_data.bitfields.len() - ); - // Ensure that all backed candidates are filtered out as either would make the block over weight - assert_eq!(limit_inherent_data.backed_candidates.len(), 0); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); - }); - } - - #[test] - // Ensure that we abort if we encounter an over weight block for disputes + bitfields - fn limit_dispute_data_ignore_backed_candidates_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 4, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) - assert_eq!(expected_para_inherent_data.bitfields.len(), 20); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Ensure that calling enter with 3 disputes and 2 candidates is over weight - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); - }); - } - - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, the bitfields are - // filtered to accommodate the block size and no backed candidates are included. - fn limit_bitfields() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Cap the number of statements per dispute to 20 in order to ensure we have enough - // space in the block for some (but not all) bitfields - dispute_statements.insert(2, 20); - dispute_statements.insert(3, 20); - dispute_statements.insert(4, 20); - - let mut backed_and_concluding = BTreeMap::new(); - // Schedule 2 backed candidates - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20), - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates, - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - assert!(limit_inherent_data != expected_para_inherent_data); - - // Three disputes is over weight (see previous test), so we expect to only see 2 disputes - assert_eq!(limit_inherent_data.disputes.len(), 2); - // Ensure disputes are filtered as expected - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - // Ensure all bitfields are included as these are still not over weight - assert_eq!(limit_inherent_data.bitfields.len(), 20,); - // Ensure that all backed candidates are filtered out as either would make the block over weight - assert_eq!(limit_inherent_data.backed_candidates.len(), 0); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); - }); - } - - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_bitfields_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 20); - dispute_statements.insert(3, 20); - dispute_statements.insert(4, 20); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); - }); - } - - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_candidates_over_weight_1() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 17); - dispute_statements.insert(3, 17); - dispute_statements.insert(4, 17); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 16); - backed_and_concluding.insert(1, 25); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Expect that inherent data is filtered to include only 1 backed candidate and 2 disputes - assert!(limit_inherent_data != expected_para_inherent_data); - - // * 1 bitfields - assert_eq!(limit_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(limit_inherent_data.backed_candidates.len(), 1); - // * 3 disputes. - assert_eq!(limit_inherent_data.disputes.len(), 2); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 1 - ); - }); - } - - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_candidates_over_weight_0() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 17); - dispute_statements.insert(3, 17); - dispute_statements.insert(4, 17); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 16); - backed_and_concluding.insert(1, 25); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); - - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); - }); - } - } - - fn default_header() -> primitives::v1::Header { - primitives::v1::Header { - parent_hash: Default::default(), - number: 0, - state_root: Default::default(), - extrinsics_root: Default::default(), - digest: Default::default(), - } - } - - mod sanitizers { - use super::*; - - use crate::inclusion::tests::{ - back_candidate, collator_sign_candidate, BackingKind, TestCandidateBuilder, - }; - use bitvec::order::Lsb0; - use primitives::v1::{ - AvailabilityBitfield, GroupIndex, Hash, Id as ParaId, SignedAvailabilityBitfield, - ValidatorIndex, - }; - - use crate::mock::Test; - use futures::executor::block_on; - use keyring::Sr25519Keyring; - use primitives::v0::PARACHAIN_KEY_TYPE_ID; - use sc_keystore::LocalKeystore; - use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; - use std::sync::Arc; - - fn validator_pubkeys(val_ids: &[keyring::Sr25519Keyring]) -> Vec { - val_ids.iter().map(|v| v.public().into()).collect() - } - - #[test] - fn bitfields() { - let header = default_header(); - let parent_hash = header.hash(); - // 2 cores means two bits - let expected_bits = 2; - let session_index = SessionIndex::from(0_u32); - - let crypto_store = LocalKeystore::in_memory(); - let crypto_store = Arc::new(crypto_store) as SyncCryptoStorePtr; - let signing_context = SigningContext { parent_hash, session_index }; - - let validators = vec![ - keyring::Sr25519Keyring::Alice, - keyring::Sr25519Keyring::Bob, - keyring::Sr25519Keyring::Charlie, - keyring::Sr25519Keyring::Dave, - ]; - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*crypto_store, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); - } - let validator_public = validator_pubkeys(&validators); - - let unchecked_bitfields = [ - BitVec::::repeat(true, expected_bits), - BitVec::::repeat(true, expected_bits), - { - let mut bv = BitVec::::repeat(false, expected_bits); - bv.set(expected_bits - 1, true); - bv - }, - ] - .iter() - .enumerate() - .map(|(vi, ab)| { - let validator_index = ValidatorIndex::from(vi as u32); - block_on(SignedAvailabilityBitfield::sign( - &crypto_store, - AvailabilityBitfield::from(ab.clone()), - &signing_context, - validator_index, - &validator_public[vi], - )) - .unwrap() - .unwrap() - .into_unchecked() - }) - .collect::>(); - - let disputed_bitfield = DisputedBitfield::zeros(expected_bits); - - { - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip, - ), - (unchecked_bitfields.clone(), expected_bits) - ); - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ), - (unchecked_bitfields.clone(), expected_bits) - ); - } - - // disputed bitfield is non-zero - { - let mut disputed_bitfield = DisputedBitfield::zeros(expected_bits); - // pretend the first core was freed by either a malicious validator - // or by resolved dispute - disputed_bitfield.0.set(0, true); - - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0 - .len(), - 1 - ); - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0 - .len(), - 1 - ); - } - - // bitfield size mismatch - { - assert!(sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits + 1, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0 - .is_empty()); - assert!(sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits + 1, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0 - .is_empty()); - } - - // remove the last validator - { - let shortened = validator_public.len() - 2; - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..shortened], - FullCheck::Yes, - ) - .0[..], - &unchecked_bitfields[..shortened] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..shortened], - FullCheck::Skip, - ) - .0[..], - &unchecked_bitfields[..shortened] - ); - } - - // switch ordering of bitfields - { - let mut unchecked_bitfields = unchecked_bitfields.clone(); - let x = unchecked_bitfields.swap_remove(0); - unchecked_bitfields.push(x); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0[..], - &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0[..], - &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] - ); - } - - // check the validators signature - { - use primitives::v1::ValidatorSignature; - let mut unchecked_bitfields = unchecked_bitfields.clone(); - - // insert a bad signature for the last bitfield - let last_bit_idx = unchecked_bitfields.len() - 1; - unchecked_bitfields - .get_mut(last_bit_idx) - .and_then(|u| Some(u.set_signature(ValidatorSignature::default()))) - .expect("we are accessing a valid index"); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0[..], - &unchecked_bitfields[..last_bit_idx] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0[..], - &unchecked_bitfields[..] - ); - } - } - - #[test] - fn candidates() { - const RELAY_PARENT_NUM: u32 = 3; - - let header = default_header(); - let relay_parent = header.hash(); - let session_index = SessionIndex::from(0_u32); - - let keystore = LocalKeystore::in_memory(); - let keystore = Arc::new(keystore) as SyncCryptoStorePtr; - let signing_context = SigningContext { parent_hash: relay_parent, session_index }; - - let validators = vec![ - keyring::Sr25519Keyring::Alice, - keyring::Sr25519Keyring::Bob, - keyring::Sr25519Keyring::Charlie, - keyring::Sr25519Keyring::Dave, - ]; - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); - } - - let has_concluded_invalid = - |_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false }; - - let scheduled = (0_usize..2) - .into_iter() - .map(|idx| { - let ca = CoreAssignment { - kind: scheduler::AssignmentKind::Parachain, - group_idx: GroupIndex::from(idx as u32), - para_id: ParaId::from(1_u32 + idx as u32), - core: CoreIndex::from(idx as u32), - }; - ca - }) - .collect::>(); - let scheduled = &scheduled[..]; - - let group_validators = |group_index: GroupIndex| { - match group_index { - group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), - group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), - _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), - } - .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) - }; - - let backed_candidates = (0_usize..2) - .into_iter() - .map(|idx0| { - let idx1 = idx0 + 1; - let mut candidate = TestCandidateBuilder { - para_id: ParaId::from(idx1), - relay_parent, - pov_hash: Hash::repeat_byte(idx1 as u8), - persisted_validation_data_hash: [42u8; 32].into(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(idx0 as u32)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - backed - }) - .collect::>(); - - // happy path - assert_eq!( - sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ), - backed_candidates - ); - - // nothing is scheduled, so no paraids match, thus all backed candidates are skipped - { - let scheduled = &[][..]; - assert!(sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ) - .is_empty()); - } - - // relay parent mismatch - { - let relay_parent = Hash::repeat_byte(0xFA); - assert!(sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ) - .is_empty()); - } - - // candidates that have concluded as invalid are filtered out - { - // mark every second one as concluded invalid - let set = { - let mut set = std::collections::HashSet::new(); - for (idx, backed_candidate) in backed_candidates.iter().enumerate() { - if idx & 0x01 == 0 { - set.insert(backed_candidate.hash().clone()); - } - } - set - }; - let has_concluded_invalid = - |_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash()); - assert_eq!( - sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ) - .len(), - backed_candidates.len() / 2 - ); - } - } - } -} diff --git a/runtime/parachains/src/paras_inherent/misc.rs b/runtime/parachains/src/paras_inherent/misc.rs new file mode 100644 index 000000000000..51b1253e9483 --- /dev/null +++ b/runtime/parachains/src/paras_inherent/misc.rs @@ -0,0 +1,40 @@ +// 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 . + +use sp_std::vec::Vec; + +/// A helper trait to allow calling retain while getting access +/// to the index of the item in the `vec`. +pub trait IndexedRetain { + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all elements `e` residing at + /// index `i` such that `f(i, &e)` returns `false`. This method + /// operates in place, visiting each element exactly once in the + /// original order, and preserves the order of the retained elements. + fn indexed_retain(&mut self, f: impl FnMut(usize, &T) -> bool); +} + +impl IndexedRetain for Vec { + fn indexed_retain(&mut self, mut f: impl FnMut(usize, &T) -> bool) { + let mut idx = 0_usize; + self.retain(move |item| { + let ret = f(idx, item); + idx += 1_usize; + ret + }) + } +} diff --git a/runtime/parachains/src/paras_inherent/mod.rs b/runtime/parachains/src/paras_inherent/mod.rs new file mode 100644 index 000000000000..b16446c5c57b --- /dev/null +++ b/runtime/parachains/src/paras_inherent/mod.rs @@ -0,0 +1,1093 @@ +// 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 crate::{ + disputes::DisputesHandler, + inclusion, + inclusion::{CandidateCheckContext, FullCheck}, + initializer, + scheduler::{self, CoreAssignment, FreedReason}, + shared, ump, + configuration +}; +use crate::configuration::HostConfiguration; +use bitvec::prelude::BitVec; +use frame_support::{ + inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, + pallet_prelude::*, + traits::Randomness, +}; +use frame_system::pallet_prelude::*; +use pallet_babe::{self, CurrentBlockRandomness}; +use primitives::v1::{ + BackedCandidate, CandidateHash, CoreIndex, DisputeStatementSet, + InherentData as ParachainsInherentData, MultiDisputeStatementSet, ScrapedOnChainVotes, + SessionIndex, SigningContext, UncheckedSignedAvailabilityBitfield, + UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, + PARACHAINS_INHERENT_IDENTIFIER, +}; +use rand::{seq::SliceRandom, SeedableRng}; + +use scale_info::TypeInfo; +use sp_runtime::traits::{Header as HeaderT, One}; +use sp_std::{ + cmp::Ordering, + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + prelude::*, + vec::Vec, +}; + +mod misc; +mod weights; + +pub use self::{ + misc::IndexedRetain, + weights::{ + backed_candidate_weight, backed_candidates_weight, dispute_statements_weight, + paras_inherent_total_weight, signed_bitfields_weight, TestWeightInfo, WeightInfo, + enact_candidates_weight, bitfields_count_ones + }, +}; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod tests; + +const LOG_TARGET: &str = "runtime::inclusion-inherent"; + +/// A bitfield concerning concluded disputes for candidates +/// associated to the core index equivalent to the bit position. +#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub(crate) struct DisputedBitfield(pub(crate) BitVec); + +impl From> for DisputedBitfield { + fn from(inner: BitVec) -> Self { + Self(inner) + } +} + +#[cfg(test)] +impl DisputedBitfield { + /// Create a new bitfield, where each bit is set to `false`. + pub fn zeros(n: usize) -> Self { + Self::from(BitVec::::repeat(false, n)) + } +} + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + #[pallet::disable_frame_system_supertrait_check] + pub trait Config: + inclusion::Config + scheduler::Config + initializer::Config + pallet_babe::Config + { + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + /// 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, + /// Disputed candidate that was concluded invalid. + CandidateConcludedInvalid, + /// The data given to the inherent will result in an overweight block. + InherentOverweight, + } + + /// 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. + #[pallet::storage] + pub(crate) type Included = StorageValue<_, ()>; + + /// Scraped on chain data for extracting resolved disputes as well as backing votes. + #[pallet::storage] + #[pallet::getter(fn on_chain_votes)] + pub(crate) type OnChainVotes = StorageValue<_, ScrapedOnChainVotes>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_: T::BlockNumber) -> Weight { + T::DbWeight::get().reads_writes(1, 1) // in on_finalize. + } + + fn on_finalize(_: T::BlockNumber) { + if Included::::take().is_none() { + panic!("Bitfields and heads must be included every block"); + } + } + } + + #[pallet::inherent] + impl ProvideInherent for Pallet { + type Call = Call; + type Error = MakeFatalError<()>; + const INHERENT_IDENTIFIER: InherentIdentifier = PARACHAINS_INHERENT_IDENTIFIER; + + fn create_inherent(data: &InherentData) -> Option { + let inherent_data = Self::create_inherent_inner(data)?; + // Sanity check: session changes can invalidate an inherent, + // and we _really_ don't want that to happen. + // See + + // Calling `Self::enter` here is a safe-guard, to avoid any discrepancy between on-chain checks + // (`enter`) and the off-chain checks by the block author (this function). Once we are confident + // in all the logic in this module this check should be removed to optimize performance. + + let inherent_data = match Self::enter_inner(inherent_data.clone(), FullCheck::Skip) { + Ok(_) => inherent_data, + Err(err) => { + log::error!( + target: LOG_TARGET, + "dropping paras inherent data because they produced \ + an invalid paras inherent: {:?}", + err.error, + ); + + ParachainsInherentData { + bitfields: Vec::new(), + backed_candidates: Vec::new(), + disputes: Vec::new(), + parent_header: inherent_data.parent_header, + } + }, + }; + + Some(Call::enter { data: inherent_data }) + } + + fn is_inherent(call: &Self::Call) -> bool { + matches!(call, Call::enter { .. }) + } + } + + /// Collect all freed cores based on storage data. (i.e. append cores freed from timeouts to + /// the given `freed_concluded`). + /// + /// The parameter `freed_concluded` contains all core indicies that became + /// free due to candidate that became available. + pub(crate) fn collect_all_freed_cores( + freed_concluded: I, + ) -> BTreeMap + where + I: core::iter::IntoIterator, + T: Config, + { + // 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 freed = freed_concluded + .into_iter() + .map(|(c, _hash)| (c, FreedReason::Concluded)) + .chain(freed_timeout.into_iter().map(|c| (c, FreedReason::TimedOut))) + .collect::>(); + freed + } + + #[pallet::call] + impl Pallet { + /// Enter the paras inherent. This will process disputes, bitfields and backed candidates. + #[pallet::weight(( + paras_inherent_total_weight::( + data.backed_candidates.as_slice(), + data.bitfields.as_slice(), + data.disputes.as_slice(), + scheduler::Pallet::::availability_cores().len(), + shared::Pallet::::active_validator_keys().len(), + &>::config(), + ), + DispatchClass::Mandatory, + ))] + pub fn enter( + origin: OriginFor, + data: ParachainsInherentData, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + + ensure!(!Included::::exists(), Error::::TooManyInclusionInherents); + Included::::set(Some(())); + + Self::enter_inner(data, FullCheck::Yes) + } + } +} + +impl Pallet { + pub(crate) fn enter_inner( + data: ParachainsInherentData, + full_check: FullCheck, + ) -> DispatchResultWithPostInfo { + let ParachainsInherentData { + bitfields: mut signed_bitfields, + mut backed_candidates, + parent_header, + mut disputes, + } = data; + + log::debug!( + target: LOG_TARGET, + "[enter] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}", + signed_bitfields.len(), + backed_candidates.len(), + disputes.len() + ); + + // 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, + ); + + let now = >::block_number(); + let expected_bits = >::availability_cores().len(); + + let mut candidate_weight = backed_candidates_weight::(&backed_candidates); + let mut bitfields_weight = signed_bitfields_weight::(signed_bitfields.len()); + let disputes_weight = dispute_statements_weight::(&disputes); + let mut max_enact_candidates_weight = enact_candidates_weight::( + bitfields_count_ones( + &signed_bitfields, + expected_bits, + shared::Pallet::::active_validator_keys().len(), + ) as u32, + &>::config(), + ); + + let max_block_weight = ::BlockWeights::get().max_block; + + // Potentially trim inherent data to ensure processing will be within weight limits + let total_weight = { + if candidate_weight + .saturating_add(bitfields_weight) + .saturating_add(disputes_weight) + .saturating_add(max_enact_candidates_weight) > + max_block_weight + { + // if the total weight is over the max block weight, first try clearing backed + // candidates and bitfields. + backed_candidates.clear(); + candidate_weight = 0; + signed_bitfields.clear(); + bitfields_weight = 0; + max_enact_candidates_weight = 0; + } + + if disputes_weight > max_block_weight { + // if disputes are by themselves overweight already, trim the disputes. + debug_assert!(candidate_weight == 0 && bitfields_weight == 0); + + let entropy = compute_entropy::(parent_hash); + let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into()); + + let remaining_weight = + limit_disputes::(&mut disputes, max_block_weight, &mut rng); + max_block_weight.saturating_sub(remaining_weight) + } else { + candidate_weight + .saturating_add(bitfields_weight) + .saturating_add(disputes_weight) + .saturating_add(max_enact_candidates_weight) + } + }; + + // Handle disputes logic. + let current_session = >::session_index(); + let disputed_bitfield = { + let new_current_dispute_sets: Vec<_> = disputes + .iter() + .filter(|s| s.session == current_session) + .map(|s| (s.session, s.candidate_hash)) + .collect(); + + // Note that `provide_multi_dispute_data` will iterate, verify, and import each + // dispute; so the input here must be reasonably bounded. + let _ = T::DisputesHandler::provide_multi_dispute_data(disputes.clone())?; + if T::DisputesHandler::is_frozen() { + // The relay chain we are currently on is invalid. Proceed no further on parachains. + return Ok(Some(dispute_statements_weight::(&disputes)).into()) + } + + let mut freed_disputed = if !new_current_dispute_sets.is_empty() { + let concluded_invalid_disputes = new_current_dispute_sets + .iter() + .filter(|(session, candidate)| { + T::DisputesHandler::concluded_invalid(*session, *candidate) + }) + .map(|(_, candidate)| *candidate) + .collect::>(); + + let freed_disputed = + >::collect_disputed(&concluded_invalid_disputes) + .into_iter() + .map(|core| (core, FreedReason::Concluded)) + .collect(); + freed_disputed + } else { + Vec::new() + }; + + // Create a bit index from the set of core indices where each index corresponds to + // a core index that was freed due to a dispute. + let disputed_bitfield = create_disputed_bitfield( + expected_bits, + freed_disputed.iter().map(|(core_index, _)| core_index), + ); + + if !freed_disputed.is_empty() { + // unstable sort is fine, because core indices are unique + // i.e. the same candidate can't occupy 2 cores at once. + freed_disputed.sort_unstable_by_key(|pair| pair.0); // sort by core index + >::free_cores(freed_disputed); + } + + disputed_bitfield + }; + + // Process new availability bitfields, yielding any availability cores whose + // work has now concluded. + let (freed_concluded, actual_enact_candidates_weight) = + >::process_bitfields( + expected_bits, + signed_bitfields, + disputed_bitfield, + >::core_para, + ); + + // Inform the disputes module of all included candidates. + for (_, candidate_hash) in &freed_concluded { + T::DisputesHandler::note_included(current_session, *candidate_hash, now); + } + + let freed = collect_all_freed_cores::(freed_concluded.iter().cloned()); + + >::clear(); + >::schedule(freed, now); + + let scheduled = >::scheduled(); + let backed_candidates = sanitize_backed_candidates::( + parent_hash, + backed_candidates, + move |_candidate_index: usize, backed_candidate: &BackedCandidate| -> bool { + ::DisputesHandler::concluded_invalid(current_session, backed_candidate.hash()) + // `fn process_candidates` does the verification checks + }, + &scheduled[..], + ); + + // Process backed candidates according to scheduled cores. + let parent_storage_root = parent_header.state_root().clone(); + let inclusion::ProcessedCandidates::<::Hash> { + core_indices: occupied, + candidate_receipt_with_backing_validator_indices, + } = >::process_candidates( + parent_storage_root, + backed_candidates, + scheduled, + >::group_validators, + full_check, + )?; + + // The number of disputes included in a block is + // limited by the weight as well as the number of candidate blocks. + OnChainVotes::::put(ScrapedOnChainVotes::<::Hash> { + session: current_session, + backing_validators_per_candidate: candidate_receipt_with_backing_validator_indices, + disputes, + }); + + // Note which of the scheduled cores were actually occupied by a backed candidate. + >::occupied(&occupied); + + // Give some time slice to dispatch pending upward messages. + // this is max config.ump_service_total_weight + let _ump_weight = >::process_pending_upward_messages(); + + let actual_total_weight = total_weight + // subtract the max enact candidate weight, + .saturating_sub(max_enact_candidates_weight) + // and add back the actual enact candidate weight + .saturating_add(actual_enact_candidates_weight); + + Ok(Some(actual_total_weight).into()) + } +} + +impl Pallet { + /// Create the `ParachainsInherentData` that gets passed to [`Self::enter`] in [`Self::create_inherent`]. + /// This code is pulled out of [`Self::create_inherent`] so it can be unit tested. + fn create_inherent_inner(data: &InherentData) -> Option> { + let ParachainsInherentData:: { + bitfields, + backed_candidates, + mut disputes, + parent_header, + } = 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 + }, + }; + + log::debug!( + target: LOG_TARGET, + "[create_inherent_inner] bitfields.len(): {}, backed_candidates.len(): {}, disputes.len() {}", + bitfields.len(), + backed_candidates.len(), + disputes.len() + ); + + let parent_hash = >::parent_hash(); + + if parent_hash != parent_header.hash() { + log::warn!( + target: LOG_TARGET, + "ParachainsInherentData references a different parent header hash than frame" + ); + return None + } + + let current_session = >::session_index(); + let expected_bits = >::availability_cores().len(); + let validator_public = shared::Pallet::::active_validator_keys(); + + T::DisputesHandler::filter_multi_dispute_data(&mut disputes); + + let (mut backed_candidates, mut bitfields, cores_with_votes) = + frame_support::storage::with_transaction(|| { + // we don't care about fresh or not disputes + // this writes them to storage, so let's query it via those means + // if this fails for whatever reason, that's ok + let _ = + T::DisputesHandler::provide_multi_dispute_data(disputes.clone()).map_err(|e| { + log::warn!( + target: LOG_TARGET, + "MultiDisputesData failed to update: {:?}", + e + ); + e + }); + + // Contains the disputes that are concluded in the current session only, + // since these are the only ones that are relevant for the occupied cores + // and lightens the load on `collect_disputed` significantly. + // Cores can't be occupied with candidates of the previous sessions, and only + // things with new votes can have just concluded. We only need to collect + // cores with disputes that conclude just now, because disputes that + // concluded longer ago have already had any corresponding cores cleaned up. + let current_concluded_invalid_disputes = disputes + .iter() + .filter(|dss| dss.session == current_session) + .map(|dss| (dss.session, dss.candidate_hash)) + .filter(|(session, candidate)| { + ::DisputesHandler::concluded_invalid(*session, *candidate) + }) + .map(|(_session, candidate)| candidate) + .collect::>(); + + // All concluded invalid disputes, that are relevant for the set of candidates + // the inherent provided. + let concluded_invalid_disputes = backed_candidates + .iter() + .map(|backed_candidate| backed_candidate.hash()) + .filter(|candidate| { + ::DisputesHandler::concluded_invalid(current_session, *candidate) + }) + .collect::>(); + + let mut freed_disputed: Vec<_> = + >::collect_disputed(¤t_concluded_invalid_disputes) + .into_iter() + .map(|core| (core, FreedReason::Concluded)) + .collect(); + + let disputed_bitfield = + create_disputed_bitfield(expected_bits, freed_disputed.iter().map(|(x, _)| x)); + + if !freed_disputed.is_empty() { + // unstable sort is fine, because core indices are unique + // i.e. the same candidate can't occupy 2 cores at once. + freed_disputed.sort_unstable_by_key(|pair| pair.0); // sort by core index + >::free_cores(freed_disputed.clone()); + } + + // The following 3 calls are equiv to a call to `process_bitfields` + // but we can retain access to `bitfields`. + let (bitfields, cores_with_votes) = sanitize_bitfields::( + bitfields, + disputed_bitfield, + expected_bits, + parent_hash, + current_session, + &validator_public[..], + FullCheck::Skip, + ); + + let (freed_concluded, _enact_candidate_weight) = + >::update_pending_availability_and_get_freed_cores::< + _, + false, + >( + expected_bits, + &validator_public[..], + bitfields.clone(), + >::core_para, + ); + + let freed = collect_all_freed_cores::(freed_concluded.iter().cloned()); + + >::clear(); + let now = >::block_number(); + >::schedule(freed, now); + + let scheduled = >::scheduled(); + + let relay_parent_number = now - One::one(); + + let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); + let backed_candidates = sanitize_backed_candidates::( + parent_hash, + backed_candidates, + move |candidate_idx: usize, + backed_candidate: &BackedCandidate<::Hash>| + -> bool { + // never include a concluded-invalid candidate + concluded_invalid_disputes.contains(&backed_candidate.hash()) || + // Instead of checking the candidates with code upgrades twice + // move the checking up here and skip it in the training wheels fallback. + // That way we avoid possible duplicate checks while assuring all + // backed candidates fine to pass on. + check_ctx + .verify_backed_candidate(parent_hash, candidate_idx, backed_candidate) + .is_err() + }, + &scheduled[..], + ); + + frame_support::storage::TransactionOutcome::Rollback(( + // filtered backed candidates + backed_candidates, + // filtered bitfields + bitfields, + // number of cores voted on by filtered bitfields + cores_with_votes.count_ones(), + )) + }); + + let entropy = compute_entropy::(parent_hash); + let mut rng = rand_chacha::ChaChaRng::from_seed(entropy.into()); + + // Assure the maximum block weight is adhered. + let max_block_weight = ::BlockWeights::get().max_block; + // TODO subtract enactment weight - return sanitized bitfields from enactment + let _consumed_weight = apply_weight_limit::( + &mut backed_candidates, + &mut bitfields, + &mut disputes, + cores_with_votes as u32, + max_block_weight, + &mut rng, + &>::config(), + ); + + Some(ParachainsInherentData:: { + bitfields, + backed_candidates, + disputes, + parent_header, + }) + } +} + +/// Derive a bitfield from dispute +pub(super) fn create_disputed_bitfield<'a, I>( + expected_bits: usize, + freed_cores: I, +) -> DisputedBitfield +where + I: 'a + IntoIterator, +{ + let mut bitvec = BitVec::repeat(false, expected_bits); + for core_idx in freed_cores { + let core_idx = core_idx.0 as usize; + if core_idx < expected_bits { + bitvec.set(core_idx, true); + } + } + DisputedBitfield::from(bitvec) +} + +/// Select a random subset, with preference for certain indices. +/// +/// Adds random items to the set until all candidates +/// are tried or the remaining weight is depleted. +/// +/// Returns the weight of all selected items from `selectables` +/// as well as their indices in ascending order. +fn random_sel Weight>( + rng: &mut rand_chacha::ChaChaRng, + selectables: Vec, + mut preferred_indices: Vec, + weight_fn: F, + weight_limit: Weight, +) -> (Weight, Vec) { + if selectables.is_empty() { + return (0 as Weight, Vec::new()) + } + // all indices that are not part of the preferred set + let mut indices = (0..selectables.len()) + .into_iter() + .filter(|idx| !preferred_indices.contains(idx)) + .collect::>(); + let mut picked_indices = Vec::with_capacity(selectables.len().saturating_sub(1)); + + let mut weight_acc = 0 as Weight; + + preferred_indices.shuffle(rng); + for preferred_idx in preferred_indices { + // preferred indices originate from outside + if let Some(item) = selectables.get(preferred_idx) { + let updated = weight_acc.saturating_add(weight_fn(item)); + if updated > weight_limit { + continue + } + weight_acc = updated; + picked_indices.push(preferred_idx); + } + } + + indices.shuffle(rng); + for idx in indices { + let item = &selectables[idx]; + let updated = weight_acc.saturating_add(weight_fn(item)); + + if updated > weight_limit { + continue + } + weight_acc = updated; + + picked_indices.push(idx); + } + + // sorting indices, so the ordering is retained + // unstable sorting is fine, since there are no duplicates + picked_indices.sort_unstable(); + (weight_acc, picked_indices) +} + +/// Considers an upper threshold that the inherent data must not exceed. +/// +/// If there is sufficient space, all disputes, all bitfields and all candidates +/// will be included. +/// +/// Otherwise tries to include all disputes, and then tries to fill the remaining space with bitfields and then candidates. +/// +/// The selection process is random. For candidates, there is an exception for code upgrades as they are preferred. +/// And for disputes, local and older disputes are preferred (see `limit_disputes`). +/// for backed candidates, since with a increasing number of parachains their chances of +/// inclusion become slim. All backed candidates are checked beforehands in `fn create_inherent_inner` +/// which guarantees sanity. +fn apply_weight_limit( + candidates: &mut Vec::Hash>>, + bitfields: &mut UncheckedSignedAvailabilityBitfields, + disputes: &mut MultiDisputeStatementSet, + cores_with_votes: u32, + max_block_weight: Weight, + rng: &mut rand_chacha::ChaChaRng, + config: &HostConfiguration, +) -> Weight { + // include as many disputes as possible, always + let remaining_weight = limit_disputes::(disputes, max_block_weight, rng); + + let total_candidates_weight = backed_candidates_weight::(candidates.as_slice()); + + // we include the worst case weight of enacting the candidates voted for by the bitfields + let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); + + let total_enact_candidates_weight = + enact_candidates_weight::(cores_with_votes, config); + + let total = total_bitfields_weight + .saturating_add(total_candidates_weight) + .saturating_add(total_enact_candidates_weight); + + // candidates + bitfields fit into the block + if remaining_weight >= total { + return total + } + + // Prefer code upgrades, they tend to be large and hence stand no chance to be picked + // late while maintaining the weight bounds + let preferred_indices = candidates + .iter() + .enumerate() + .filter_map(|(idx, candidate)| { + candidate.candidate.commitments.new_validation_code.as_ref().map(|_code| idx) + }) + .collect::>(); + + // There is weight remaining to be consumed by a subset of candidates + // which are going to be picked now. + if let Some(remaining_weight) = remaining_weight + .checked_sub(total_bitfields_weight) + .and_then(|r| r.checked_sub(total_enact_candidates_weight)) + { + let (acc_candidate_weight, indices) = + random_sel::::Hash>, _>( + rng, + candidates.clone(), + preferred_indices, + |c| backed_candidate_weight::(c), + remaining_weight, + ); + candidates.indexed_retain(|idx, _backed_candidate| indices.binary_search(&idx).is_ok()); + // pick all bitfields, and + // fill the remaining space with candidates + let total = acc_candidate_weight.saturating_add(total_bitfields_weight); + return total + } + + candidates.clear(); + + // insufficient space for even the bitfields alone, so only try to fit as many of those + // into the block and skip the candidates entirely + let (total, indices) = random_sel::( + rng, + bitfields.clone(), + vec![], + // bitfields can lead to enacting a candidate; however we don't have a good way of + // accounting for that when tracking individual bitfield weight, thus the weight here likely + // ends up being an underestimate. + |_| <::WeightInfo as WeightInfo>::enter_bitfields(), + remaining_weight.saturating_sub(total_enact_candidates_weight), + ); + + bitfields.indexed_retain(|idx, _bitfield| indices.binary_search(&idx).is_ok()); + + total +} + +/// Filter bitfields based on freed core indices, validity, and other sanity checks. +/// +/// Do sanity checks on the bitfields: +/// +/// 1. no more than one bitfield per validator +/// 2. bitfields are ascending by validator index. +/// 3. each bitfield has exactly `expected_bits` +/// 4. signature is valid +/// 5. remove any disputed core indices +/// +/// If any of those is not passed, the bitfield is dropped. +/// +/// While this function technically returns a set of unchecked bitfields, +/// they were actually checked and filtered to allow using it in both +/// cases, as `filtering` and `checking` stage. +/// +/// `full_check` determines if validator signatures are checked. If `::Yes`, +/// bitfields that have an invalid signature will be filtered out. +// TODO also return all bitfields OR'ed together +pub(crate) fn sanitize_bitfields( + unchecked_bitfields: UncheckedSignedAvailabilityBitfields, + disputed_bitfield: DisputedBitfield, + expected_bits: usize, + parent_hash: T::Hash, + session_index: SessionIndex, + validators: &[ValidatorId], + full_check: FullCheck, +) -> (UncheckedSignedAvailabilityBitfields, usize) { + let mut bitfields = Vec::with_capacity(unchecked_bitfields.len()); + + let mut last_index: Option = None; + + if disputed_bitfield.0.len() != expected_bits { + // This is a system logic error that should never occur, but we want to handle it gracefully + // so we just drop all bitfields + log::error!(target: LOG_TARGET, "BUG: disputed_bitfield != expected_bits"); + return (vec![], 0) + } + + let all_zeros = BitVec::::repeat(false, expected_bits); + let mut cores_with_votes = BitVec::::repeat(false, expected_bits); + let signing_context = SigningContext { parent_hash, session_index }; + for unchecked_bitfield in unchecked_bitfields { + // Find and skip invalid bitfields. + if !cheap_bitfield_checks( + &unchecked_bitfield, + expected_bits, + validators.len(), + last_index, + &full_check, + ) { + continue + } + + if unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() != + all_zeros + { + log::trace!( + target: LOG_TARGET, + "[{:?}] bitfield contains disputed cores: {:?}", + full_check, + unchecked_bitfield.unchecked_payload().0.clone() & disputed_bitfield.0.clone() + ); + continue + } + + let validator_index = unchecked_bitfield.unchecked_validator_index(); + let validator_public = &validators[validator_index.0 as usize]; + + if let FullCheck::Yes = full_check { + if let Ok(signed_bitfield) = + unchecked_bitfield.try_into_checked(&signing_context, validator_public) + { + cores_with_votes |= signed_bitfield.payload().0.clone(); + bitfields.push(signed_bitfield.into_unchecked()); + } else { + log::warn!(target: LOG_TARGET, "Invalid bitfield signature"); + }; + } else { + cores_with_votes |= unchecked_bitfield.unchecked_payload().0.clone(); + bitfields.push(unchecked_bitfield); + } + + last_index = Some(validator_index); + } + + (bitfields, cores_with_votes.count_ones()) +} + +// Returns `true` iff the checks pass. +fn cheap_bitfield_checks( + unchecked_bitfield: &UncheckedSignedAvailabilityBitfield, + expected_bits: usize, + validator_count: usize, + last_index: Option, + full_check: &FullCheck, +) -> bool { + if unchecked_bitfield.unchecked_payload().0.len() != expected_bits { + log::trace!( + target: LOG_TARGET, + "[{:?}] bad bitfield length: {} != {:?}", + full_check, + unchecked_bitfield.unchecked_payload().0.len(), + expected_bits, + ); + + return false + } + + let validator_index = unchecked_bitfield.unchecked_validator_index(); + if !last_index.map_or(true, |last_index: ValidatorIndex| last_index < validator_index) { + log::trace!( + target: LOG_TARGET, + "[{:?}] bitfield validator index is not greater than last: !({:?} < {})", + full_check, + last_index.as_ref().map(|x| x.0), + validator_index.0 + ); + + return false + } + + if unchecked_bitfield.unchecked_validator_index().0 as usize >= validator_count { + log::trace!( + target: LOG_TARGET, + "[{:?}] bitfield validator index is out of bounds: {} >= {}", + full_check, + validator_index.0, + validator_count, + ); + + return false + } + + true +} + +/// Filter out any candidates that have a concluded invalid dispute. +/// +/// `scheduled` follows the same naming scheme as provided in the +/// guide: Currently `free` but might become `occupied`. +/// For the filtering here the relevant part is only the current `free` +/// state. +/// +/// `candidate_has_concluded_invalid_dispute` must return `true` if the candidate +/// is disputed, false otherwise +fn sanitize_backed_candidates< + T: crate::inclusion::Config, + F: FnMut(usize, &BackedCandidate) -> bool, +>( + relay_parent: T::Hash, + mut backed_candidates: Vec>, + mut candidate_has_concluded_invalid_dispute_or_is_invalid: F, + scheduled: &[CoreAssignment], +) -> Vec> { + // Remove any candidates that were concluded invalid. + backed_candidates.indexed_retain(move |idx, backed_candidate| { + !candidate_has_concluded_invalid_dispute_or_is_invalid(idx, backed_candidate) + }); + + // Assure the backed candidate's `ParaId`'s core is free. + // This holds under the assumption that `Scheduler::schedule` is called _before_. + // Also checks the candidate references the correct relay parent. + let scheduled_paras_set = scheduled + .into_iter() + .map(|core_assignment| core_assignment.para_id) + .collect::>(); + backed_candidates.retain(|backed_candidate| { + let desc = backed_candidate.descriptor(); + desc.relay_parent == relay_parent && scheduled_paras_set.contains(&desc.para_id) + }); + + backed_candidates +} + +/// Derive entropy from babe provided per block randomness. +/// +/// In the odd case none is available, uses the `parent_hash` and +/// a const value, while emitting a warning. +fn compute_entropy(parent_hash: T::Hash) -> [u8; 32] { + const CANDIDATE_SEED_SUBJECT: [u8; 32] = *b"candidate-seed-selection-subject"; + let vrf_random = CurrentBlockRandomness::::random(&CANDIDATE_SEED_SUBJECT[..]).0; + let mut entropy: [u8; 32] = CANDIDATE_SEED_SUBJECT.clone(); + if let Some(vrf_random) = vrf_random { + entropy.as_mut().copy_from_slice(vrf_random.as_ref()); + } else { + // in case there is no vrf randomness present, we utilize the relay parent + // as seed, it's better than a static value. + log::warn!(target: LOG_TARGET, "CurrentBlockRandomness did not provide entropy"); + entropy.as_mut().copy_from_slice(parent_hash.as_ref()); + } + entropy +} + +/// Limit disputes in place. +/// +/// Returns the unused weight of `remaining_weight`. +fn limit_disputes( + disputes: &mut MultiDisputeStatementSet, + remaining_weight: Weight, + rng: &mut rand_chacha::ChaChaRng, +) -> Weight { + let mut remaining_weight = remaining_weight; + let disputes_weight = dispute_statements_weight::(&disputes); + if disputes_weight > remaining_weight { + // Sort the dispute statements according to the following prioritization: + // 1. Prioritize local disputes over remote disputes. + // 2. Prioritize older disputes over newer disputes. + disputes.sort_by(|a, b| { + let a_local_block = T::DisputesHandler::included_state(a.session, a.candidate_hash); + let b_local_block = T::DisputesHandler::included_state(b.session, b.candidate_hash); + match (a_local_block, b_local_block) { + // Prioritize local disputes over remote disputes. + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + // For local disputes, prioritize those that occur at an earlier height. + (Some(a_height), Some(b_height)) => a_height.cmp(&b_height), + // Prioritize earlier remote disputes using session as rough proxy. + (None, None) => a.session.cmp(&b.session), + } + }); + + // Since the disputes array is sorted, we may use binary search to find the beginning of + // remote disputes + let idx = disputes + .binary_search_by(|probe| { + if T::DisputesHandler::included_state(probe.session, probe.candidate_hash).is_some() + { + Ordering::Greater + } else { + Ordering::Less + } + }) + // The above predicate will never find an item and therefore we are guaranteed to obtain + // an error, which we can safely unwrap. QED. + .unwrap_err(); + + // Due to the binary search predicate above, the index computed will constitute the beginning + // of the remote disputes sub-array + let remote_disputes = disputes.split_off(idx); + + // Select disputes in-order until the remaining weight is attained + disputes.retain(|d| { + let dispute_weight = <::WeightInfo as WeightInfo>::enter_variable_disputes( + d.statements.len() as u32, + ); + if remaining_weight >= dispute_weight { + remaining_weight -= dispute_weight; + true + } else { + false + } + }); + + // Compute the statements length of all remote disputes + let d = remote_disputes.iter().map(|d| d.statements.len() as u32).collect::>(); + + // Select remote disputes at random until the block is full + let (acc_remote_disputes_weight, indices) = random_sel::( + rng, + d, + vec![], + |v| <::WeightInfo as WeightInfo>::enter_variable_disputes(*v), + remaining_weight, + ); + + // Collect all remote disputes + let mut remote_disputes = + indices.into_iter().map(|idx| disputes[idx].clone()).collect::>(); + + // Construct the full list of selected disputes + disputes.append(&mut remote_disputes); + + // Update the remaining weight + remaining_weight = remaining_weight.saturating_sub(acc_remote_disputes_weight); + } + + remaining_weight +} diff --git a/runtime/parachains/src/paras_inherent/tests.rs b/runtime/parachains/src/paras_inherent/tests.rs new file mode 100644 index 000000000000..f098a369d059 --- /dev/null +++ b/runtime/parachains/src/paras_inherent/tests.rs @@ -0,0 +1,1119 @@ + use super::*; + + // In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl + // that uses 0 for all the weights. Because all the weights are 0, the tests that rely on + // weights for limiting data will fail, so we don't run them when using the benchmark feature. + #[cfg(not(feature = "runtime-benchmarks"))] + mod enter { + use super::*; + use crate::{ + builder::{Bench, BenchBuilder}, + mock::{new_test_ext, MockGenesisConfig, Test}, + }; + use frame_support::assert_ok; + use sp_std::collections::btree_map::BTreeMap; + + struct TestConfig { + dispute_statements: BTreeMap, + dispute_sessions: Vec, + backed_and_concluding: BTreeMap, + num_validators_per_core: u32, + includes_code_upgrade: Option, + } + + fn make_inherent_data( + TestConfig { + dispute_statements, + dispute_sessions, + backed_and_concluding, + num_validators_per_core, + includes_code_upgrade, + }: TestConfig, + ) -> Bench { + BenchBuilder::::new() + .set_max_validators((dispute_sessions.len() as u32) * num_validators_per_core) + .set_max_validators_per_core(num_validators_per_core) + .set_dispute_statements(dispute_statements) + .build(backed_and_concluding, dispute_sessions.as_slice(), includes_code_upgrade) + } + + #[test] + // Validate that if we create 2 backed candidates which are assigned to 2 cores that will be freed via + // becoming fully available, the backed candidates will not be filtered out in `create_inherent` and + // will not cause `enter` to early. + fn include_backed_candidates() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let dispute_statements = BTreeMap::new(); + + let mut backed_and_concluding = BTreeMap::new(); + backed_and_concluding.insert(0, 1); + backed_and_concluding.insert(1, 1); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![0, 0], + backed_and_concluding, + num_validators_per_core: 1, + includes_code_upgrade: None, + }); + + // We expect the scenario to have cores 0 & 1 with pending availability. The backed + // candidates are also created for cores 0 & 1, so once the pending available + // become fully available those cores are marked as free and scheduled for the backed + // candidates. + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (2 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 2); + // * 1 backed candidate per core (2 cores) + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 0 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 0); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Nothing is filtered out (including the backed candidates.) + assert_eq!( + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(), + expected_para_inherent_data + ); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 backed candidates + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data + )); + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 2 + ); + }); + } + + #[test] + // Ensure that disputes are filtered out if the session is in the future. + fn filter_multi_dispute_data() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![ + 1, 2, 3, /* Session 3 too new, will get filtered out */ + ], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 3 disputes => 3 cores, 15 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 15); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + let multi_dispute_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Dispute for session that lies too far in the future should be filtered out + assert!(multi_dispute_inherent_data != expected_para_inherent_data); + + assert_eq!(multi_dispute_inherent_data.disputes.len(), 2); + + // Assert that the first 2 disputes are included + assert_eq!( + &multi_dispute_inherent_data.disputes[..2], + &expected_para_inherent_data.disputes[..2], + ); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + multi_dispute_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know there + // where no backed candidates included + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0 + ); + }); + } + + #[test] + // Ensure that when dispute data establishes an over weight block that we adequately + // filter out disputes according to our prioritization rule + fn limit_dispute_data() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + // No backed and concluding cores, so all cores will be fileld with disputesw + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes + backed_and_concluding, + num_validators_per_core: 6, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 18); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Expect that inherent data is filtered to include only 2 disputes + assert!(limit_inherent_data != expected_para_inherent_data); + + // Ensure that the included disputes are sorted by session + assert_eq!(limit_inherent_data.disputes.len(), 2); + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // Ensure that our inherent data did not included backed candidates as expected + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0 + ); + }); + } + + #[test] + // Ensure that when dispute data establishes an over weight block that we abort + // due to an over weight block + fn limit_dispute_data_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + // No backed and concluding cores, so all cores will be fileld with disputesw + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes + backed_and_concluding, + num_validators_per_core: 6, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 18); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes, but there is still sufficient + // block weight to include a number of signed bitfields, the inherent data is filtered + // as expected + fn limit_dispute_data_ignore_backed_candidates() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 4, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) + assert_eq!(expected_para_inherent_data.bitfields.len(), 20); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Nothing is filtered out (including the backed candidates.) + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + assert!(limit_inherent_data != expected_para_inherent_data); + + // Three disputes is over weight (see previous test), so we expect to only see 2 disputes + assert_eq!(limit_inherent_data.disputes.len(), 2); + // Ensure disputes are filtered as expected + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + // Ensure all bitfields are included as these are still not over weight + assert_eq!( + limit_inherent_data.bitfields.len(), + expected_para_inherent_data.bitfields.len() + ); + // Ensure that all backed candidates are filtered out as either would make the block over weight + assert_eq!(limit_inherent_data.backed_candidates.len(), 0); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0, + ); + }); + } + + #[test] + // Ensure that we abort if we encounter an over weight block for disputes + bitfields + fn limit_dispute_data_ignore_backed_candidates_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 4, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) + assert_eq!(expected_para_inherent_data.bitfields.len(), 20); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Ensure that calling enter with 3 disputes and 2 candidates is over weight + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0, + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, the bitfields are + // filtered to accommodate the block size and no backed candidates are included. + fn limit_bitfields() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Cap the number of statements per dispute to 20 in order to ensure we have enough + // space in the block for some (but not all) bitfields + dispute_statements.insert(2, 20); + dispute_statements.insert(3, 20); + dispute_statements.insert(4, 20); + + let mut backed_and_concluding = BTreeMap::new(); + // Schedule 2 backed candidates + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20), + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates, + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Nothing is filtered out (including the backed candidates.) + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + assert!(limit_inherent_data != expected_para_inherent_data); + + // Three disputes is over weight (see previous test), so we expect to only see 2 disputes + assert_eq!(limit_inherent_data.disputes.len(), 2); + // Ensure disputes are filtered as expected + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + // Ensure all bitfields are included as these are still not over weight + assert_eq!(limit_inherent_data.bitfields.len(), 20,); + // Ensure that all backed candidates are filtered out as either would make the block over weight + assert_eq!(limit_inherent_data.backed_candidates.len(), 0); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0, + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_bitfields_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 20); + dispute_statements.insert(3, 20); + dispute_statements.insert(4, 20); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0, + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_candidates_over_weight_1() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 17); + dispute_statements.insert(3, 17); + dispute_statements.insert(4, 17); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 16); + backed_and_concluding.insert(1, 25); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Expect that inherent data is filtered to include only 1 backed candidate and 2 disputes + assert!(limit_inherent_data != expected_para_inherent_data); + + // * 1 bitfields + assert_eq!(limit_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(limit_inherent_data.backed_candidates.len(), 1); + // * 3 disputes. + assert_eq!(limit_inherent_data.disputes.len(), 2); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 1 + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_candidates_over_weight_0() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 17); + dispute_statements.insert(3, 17); + dispute_statements.insert(4, 17); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 16); + backed_and_concluding.insert(1, 25); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes() + .unwrap() + .backing_validators_per_candidate + .len(), + 0 + ); + }); + } + } + + fn default_header() -> primitives::v1::Header { + primitives::v1::Header { + parent_hash: Default::default(), + number: 0, + state_root: Default::default(), + extrinsics_root: Default::default(), + digest: Default::default(), + } + } + + mod sanitizers { + use super::*; + + use crate::inclusion::tests::{ + back_candidate, collator_sign_candidate, BackingKind, TestCandidateBuilder, + }; + use bitvec::order::Lsb0; + use primitives::v1::{ + AvailabilityBitfield, GroupIndex, Hash, Id as ParaId, SignedAvailabilityBitfield, + ValidatorIndex, + }; + + use crate::mock::Test; + use futures::executor::block_on; + use keyring::Sr25519Keyring; + use primitives::v0::PARACHAIN_KEY_TYPE_ID; + use sc_keystore::LocalKeystore; + use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; + use std::sync::Arc; + + fn validator_pubkeys(val_ids: &[keyring::Sr25519Keyring]) -> Vec { + val_ids.iter().map(|v| v.public().into()).collect() + } + + #[test] + fn bitfields() { + let header = default_header(); + let parent_hash = header.hash(); + // 2 cores means two bits + let expected_bits = 2; + let session_index = SessionIndex::from(0_u32); + + let crypto_store = LocalKeystore::in_memory(); + let crypto_store = Arc::new(crypto_store) as SyncCryptoStorePtr; + let signing_context = SigningContext { parent_hash, session_index }; + + let validators = vec![ + keyring::Sr25519Keyring::Alice, + keyring::Sr25519Keyring::Bob, + keyring::Sr25519Keyring::Charlie, + keyring::Sr25519Keyring::Dave, + ]; + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*crypto_store, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); + + let unchecked_bitfields = [ + BitVec::::repeat(true, expected_bits), + BitVec::::repeat(true, expected_bits), + { + let mut bv = BitVec::::repeat(false, expected_bits); + bv.set(expected_bits - 1, true); + bv + }, + ] + .iter() + .enumerate() + .map(|(vi, ab)| { + let validator_index = ValidatorIndex::from(vi as u32); + block_on(SignedAvailabilityBitfield::sign( + &crypto_store, + AvailabilityBitfield::from(ab.clone()), + &signing_context, + validator_index, + &validator_public[vi], + )) + .unwrap() + .unwrap() + .into_unchecked() + }) + .collect::>(); + + let disputed_bitfield = DisputedBitfield::zeros(expected_bits); + + { + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip, + ), + (unchecked_bitfields.clone(), expected_bits) + ); + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ), + (unchecked_bitfields.clone(), expected_bits) + ); + } + + // disputed bitfield is non-zero + { + let mut disputed_bitfield = DisputedBitfield::zeros(expected_bits); + // pretend the first core was freed by either a malicious validator + // or by resolved dispute + disputed_bitfield.0.set(0, true); + + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0 + .len(), + 1 + ); + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0 + .len(), + 1 + ); + } + + // bitfield size mismatch + { + assert!(sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits + 1, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0 + .is_empty()); + assert!(sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits + 1, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0 + .is_empty()); + } + + // remove the last validator + { + let shortened = validator_public.len() - 2; + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..shortened], + FullCheck::Yes, + ) + .0[..], + &unchecked_bitfields[..shortened] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..shortened], + FullCheck::Skip, + ) + .0[..], + &unchecked_bitfields[..shortened] + ); + } + + // switch ordering of bitfields + { + let mut unchecked_bitfields = unchecked_bitfields.clone(); + let x = unchecked_bitfields.swap_remove(0); + unchecked_bitfields.push(x); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0[..], + &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0[..], + &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] + ); + } + + // check the validators signature + { + use primitives::v1::ValidatorSignature; + let mut unchecked_bitfields = unchecked_bitfields.clone(); + + // insert a bad signature for the last bitfield + let last_bit_idx = unchecked_bitfields.len() - 1; + unchecked_bitfields + .get_mut(last_bit_idx) + .and_then(|u| Some(u.set_signature(ValidatorSignature::default()))) + .expect("we are accessing a valid index"); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0[..], + &unchecked_bitfields[..last_bit_idx] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0[..], + &unchecked_bitfields[..] + ); + } + } + + #[test] + fn candidates() { + const RELAY_PARENT_NUM: u32 = 3; + + let header = default_header(); + let relay_parent = header.hash(); + let session_index = SessionIndex::from(0_u32); + + let keystore = LocalKeystore::in_memory(); + let keystore = Arc::new(keystore) as SyncCryptoStorePtr; + let signing_context = SigningContext { parent_hash: relay_parent, session_index }; + + let validators = vec![ + keyring::Sr25519Keyring::Alice, + keyring::Sr25519Keyring::Bob, + keyring::Sr25519Keyring::Charlie, + keyring::Sr25519Keyring::Dave, + ]; + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + + let has_concluded_invalid = + |_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false }; + + let scheduled = (0_usize..2) + .into_iter() + .map(|idx| { + let ca = CoreAssignment { + kind: scheduler::AssignmentKind::Parachain, + group_idx: GroupIndex::from(idx as u32), + para_id: ParaId::from(1_u32 + idx as u32), + core: CoreIndex::from(idx as u32), + }; + ca + }) + .collect::>(); + let scheduled = &scheduled[..]; + + let group_validators = |group_index: GroupIndex| { + match group_index { + group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), + group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), + _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), + } + .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) + }; + + let backed_candidates = (0_usize..2) + .into_iter() + .map(|idx0| { + let idx1 = idx0 + 1; + let mut candidate = TestCandidateBuilder { + para_id: ParaId::from(idx1), + relay_parent, + pov_hash: Hash::repeat_byte(idx1 as u8), + persisted_validation_data_hash: [42u8; 32].into(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); + + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(idx0 as u32)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + backed + }) + .collect::>(); + + // happy path + assert_eq!( + sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ), + backed_candidates + ); + + // nothing is scheduled, so no paraids match, thus all backed candidates are skipped + { + let scheduled = &[][..]; + assert!(sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ) + .is_empty()); + } + + // relay parent mismatch + { + let relay_parent = Hash::repeat_byte(0xFA); + assert!(sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ) + .is_empty()); + } + + // candidates that have concluded as invalid are filtered out + { + // mark every second one as concluded invalid + let set = { + let mut set = std::collections::HashSet::new(); + for (idx, backed_candidate) in backed_candidates.iter().enumerate() { + if idx & 0x01 == 0 { + set.insert(backed_candidate.hash().clone()); + } + } + set + }; + let has_concluded_invalid = + |_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash()); + assert_eq!( + sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ) + .len(), + backed_candidates.len() / 2 + ); + } + } + } \ No newline at end of file diff --git a/runtime/parachains/src/paras_inherent/weights.rs b/runtime/parachains/src/paras_inherent/weights.rs new file mode 100644 index 000000000000..edc56b1fb22e --- /dev/null +++ b/runtime/parachains/src/paras_inherent/weights.rs @@ -0,0 +1,178 @@ +// Copyright 2021 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 . +use super::{ + BackedCandidate, Config, DisputeStatementSet, UncheckedSignedAvailabilityBitfield, Weight, + cheap_bitfield_checks +}; +use crate::configuration::HostConfiguration; +use crate::inclusion::{self, FullCheck}; + use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec}; +const MAX_UNCHECKED_BITFIELD_ITERATIONS: usize = 1_000; + +pub trait WeightInfo { + /// Variant over `v`, the count of dispute statements in a dispute statement set. This gives the + /// weight of a single dispute statement set. + fn enter_variable_disputes(v: u32) -> Weight; + /// The weight of one bitfield. + fn enter_bitfields() -> Weight; + /// Variant over `v`, the count of validity votes for a backed candidate. This gives the weight + /// of a single backed candidate. + fn enter_backed_candidates_variable(v: u32) -> Weight; + /// The weight of a single backed candidate with a code upgrade. + fn enter_backed_candidate_code_upgrade() -> Weight; +} + +pub struct TestWeightInfo; +// `WeightInfo` impl for unit and integration tests. Based off of the `max_block` weight for the +// mock. +#[cfg(not(feature = "runtime-benchmarks"))] +impl WeightInfo for TestWeightInfo { + fn enter_variable_disputes(v: u32) -> Weight { + // MAX Block Weight should fit 4 disputes + 80_000 * v as Weight + 80_000 + } + fn enter_bitfields() -> Weight { + // MAX Block Weight should fit 4 backed candidates + 40_000 as Weight + } + fn enter_backed_candidates_variable(v: u32) -> Weight { + // MAX Block Weight should fit 4 backed candidates + 40_000 * v as Weight + 40_000 + } + fn enter_backed_candidate_code_upgrade() -> Weight { + 0 + } +} +// To simplify benchmarks running as tests, we set all the weights to 0. `enter` will exit early +// when if the data causes it to be over weight, but we don't want that to block a benchmark from +// running as a test. +#[cfg(feature = "runtime-benchmarks")] +impl WeightInfo for TestWeightInfo { + fn enter_variable_disputes(_v: u32) -> Weight { + 0 + } + fn enter_bitfields() -> Weight { + 0 + } + fn enter_backed_candidates_variable(_v: u32) -> Weight { + 0 + } + fn enter_backed_candidate_code_upgrade() -> Weight { + 0 + } +} + +// OR together all bitfields and count the ones from the result. +// Note that this will only OR together the first `MAX_UNCHECKED_BITFIELD_ITERATIONS` to avoid +// excessive iteration. +pub fn bitfields_count_ones( + bitfields: &[UncheckedSignedAvailabilityBitfield], + expected_bits: usize, + validator_count: usize, +) -> usize { + let mut last_index = None; + bitfields + .iter() + .take(MAX_UNCHECKED_BITFIELD_ITERATIONS) + .fold( + BitVec::::repeat(false, expected_bits), + |acc: BitVec, cur| { + if cheap_bitfield_checks( + cur, + expected_bits, + validator_count, + last_index, + &FullCheck::Skip, + ) { + last_index = Some(cur.unchecked_validator_index()); + acc | cur.unchecked_payload().0.clone() + } else { + acc + } + }, + ) + .count_ones() +} + +// Note that this does a storage read. +pub fn paras_inherent_total_weight( + backed_candidates: &[BackedCandidate<::Hash>], + bitfields: &[UncheckedSignedAvailabilityBitfield], + disputes: &[DisputeStatementSet], + expected_bits: usize, + validator_count: usize, + config: &HostConfiguration, +) -> Weight { + backed_candidates_weight::(backed_candidates) + .saturating_add(signed_bitfields_weight::(bitfields.len())) + .saturating_add(dispute_statements_weight::(disputes)) + .saturating_add(enact_candidates_weight::( + bitfields_count_ones(bitfields, expected_bits, validator_count) as u32, + config, + )) +} + +pub fn dispute_statements_weight(disputes: &[DisputeStatementSet]) -> Weight { + disputes + .iter() + .map(|d| { + <::WeightInfo as WeightInfo>::enter_variable_disputes( + d.statements.len() as u32 + ) + }) + .fold(0, |acc, x| acc.saturating_add(x)) +} + +pub fn signed_bitfields_weight(bitfields_len: usize) -> Weight { + <::WeightInfo as WeightInfo>::enter_bitfields() + .saturating_mul(bitfields_len as Weight) +} + +// Calculate the worst case weight for enacting the given number of candidates. +pub fn enact_candidates_weight( + candidate_count: u32, + config: &HostConfiguration, +) -> Weight { + >::enact_candidate_weight( + config.hrmp_max_parathread_inbound_channels, + config.hrmp_max_parachain_inbound_channels, + config.max_upward_message_num_per_candidate, + config.hrmp_max_message_num_per_candidate, + ) + .saturating_mul(candidate_count as Weight) +} + +pub fn backed_candidate_weight( + candidate: &BackedCandidate, +) -> Weight { + if candidate.candidate.commitments.new_validation_code.is_some() { + <::WeightInfo as WeightInfo>::enter_backed_candidate_code_upgrade() + } else { + <::WeightInfo as WeightInfo>::enter_backed_candidates_variable( + candidate.validity_votes.len() as u32, + ) + } +} + +// Calculate the max weight of the given candidates. +pub fn backed_candidates_weight( + candidates: &[BackedCandidate], +) -> Weight { + candidates + .iter() + .map(|c| backed_candidate_weight::(c)) + .fold(0, |acc, x| acc.saturating_add(x)) +} \ No newline at end of file From a43f56a3f8d4e6f51e242a4c5a16415420cc848e Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:27:24 -0800 Subject: [PATCH 22/27] Prepare inclusion for merging master --- runtime/parachains/src/inclusion/mod.rs | 1072 ++++++++++++++++ .../src/{inclusion.rs => inclusion/tests.rs} | 1075 +---------------- 2 files changed, 1073 insertions(+), 1074 deletions(-) create mode 100644 runtime/parachains/src/inclusion/mod.rs rename runtime/parachains/src/{inclusion.rs => inclusion/tests.rs} (60%) diff --git a/runtime/parachains/src/inclusion/mod.rs b/runtime/parachains/src/inclusion/mod.rs new file mode 100644 index 000000000000..79c1cc1e5042 --- /dev/null +++ b/runtime/parachains/src/inclusion/mod.rs @@ -0,0 +1,1072 @@ +// 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 . + +//! The inclusion pallet is responsible for inclusion and availability of scheduled parachains +//! and parathreads. +//! +//! It is responsible for carrying candidates from being backable to being backed, and then from backed +//! to included. + +use crate::{ + configuration, disputes, dmp, hrmp, paras, + paras_inherent::{sanitize_bitfields, DisputedBitfield}, + scheduler::CoreAssignment, + shared, ump, +}; +use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec}; +use frame_support::pallet_prelude::*; +use parity_scale_codec::{Decode, Encode}; +use primitives::v1::{ + AvailabilityBitfield, BackedCandidate, CandidateCommitments, CandidateDescriptor, + CandidateHash, CandidateReceipt, CommittedCandidateReceipt, CoreIndex, GroupIndex, Hash, + HeadData, Id as ParaId, SigningContext, UncheckedSignedAvailabilityBitfields, ValidatorId, + ValidatorIndex, ValidityAttestation, +}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{One, Saturating}, + DispatchError, +}; +use sp_std::{collections::btree_set::BTreeSet, prelude::*}; + +pub use pallet::*; + +#[cfg(test)] +pub(crate) mod tests; + +/// A bitfield signed by a validator indicating that it is keeping its piece of the erasure-coding +/// for any backed candidates referred to by a `1` bit available. +/// +/// The bitfield's signature should be checked at the point of submission. Afterwards it can be +/// dropped. +#[derive(Encode, Decode, TypeInfo)] +#[cfg_attr(test, derive(Debug))] +pub struct AvailabilityBitfieldRecord { + bitfield: AvailabilityBitfield, // one bit per core. + submitted_at: N, // for accounting, as meaning of bits may change over time. +} + +/// Determines if all checks should be applied or if a subset was already completed +/// in a code path that will be executed afterwards or was already executed before. +#[derive(Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub(crate) enum FullCheck { + /// Yes, do a full check, skip nothing. + Yes, + /// Skip a subset of checks that are already completed before. + /// + /// Attention: Should only be used when absolutely sure that the required + /// checks are completed before. + Skip, +} + +/// A backed candidate pending availability. +#[derive(Encode, Decode, PartialEq, TypeInfo)] +#[cfg_attr(test, derive(Debug, Default))] +pub struct CandidatePendingAvailability { + /// The availability core this is assigned to. + core: CoreIndex, + /// The candidate hash. + hash: CandidateHash, + /// The candidate descriptor. + descriptor: CandidateDescriptor, + /// The received availability votes. One bit per validator. + availability_votes: BitVec, + /// The backers of the candidate pending availability. + backers: BitVec, + /// The block number of the relay-parent of the receipt. + relay_parent_number: N, + /// The block number of the relay-chain block this was backed in. + backed_in_number: N, + /// The group index backing this block. + backing_group: GroupIndex, +} + +impl CandidatePendingAvailability { + /// Get the availability votes on the candidate. + pub(crate) fn availability_votes(&self) -> &BitVec { + &self.availability_votes + } + + /// Get the relay-chain block number this was backed in. + pub(crate) fn backed_in_number(&self) -> &N { + &self.backed_in_number + } + + /// Get the core index. + pub(crate) fn core_occupied(&self) -> CoreIndex { + self.core.clone() + } + + /// Get the candidate hash. + pub(crate) fn candidate_hash(&self) -> CandidateHash { + self.hash + } + + /// Get the candidate descriptor. + pub(crate) fn candidate_descriptor(&self) -> &CandidateDescriptor { + &self.descriptor + } + + #[cfg(any(feature = "runtime-benchmarks", test))] + pub(crate) fn new( + core: CoreIndex, + hash: CandidateHash, + descriptor: CandidateDescriptor, + availability_votes: BitVec, + backers: BitVec, + relay_parent_number: N, + backed_in_number: N, + backing_group: GroupIndex, + ) -> Self { + Self { + core, + hash, + descriptor, + availability_votes, + backers, + relay_parent_number, + backed_in_number, + backing_group, + } + } +} + +/// A hook for applying validator rewards +pub trait RewardValidators { + // Reward the validators with the given indices for issuing backing statements. + fn reward_backing(validators: impl IntoIterator); + // Reward the validators with the given indices for issuing availability bitfields. + // Validators are sent to this hook when they have contributed to the availability + // of a candidate by setting a bit in their bitfield. + fn reward_bitfields(validators: impl IntoIterator); +} + +/// Helper return type for `process_candidates`. +#[derive(Encode, Decode, PartialEq, TypeInfo)] +#[cfg_attr(test, derive(Debug))] +pub(crate) struct ProcessedCandidates { + pub(crate) core_indices: Vec, + pub(crate) candidate_receipt_with_backing_validator_indices: + Vec<(CandidateReceipt, Vec<(ValidatorIndex, ValidityAttestation)>)>, +} + +impl Default for ProcessedCandidates { + fn default() -> Self { + Self { + core_indices: Vec::new(), + candidate_receipt_with_backing_validator_indices: Vec::new(), + } + } +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + + shared::Config + + paras::Config + + dmp::Config + + ump::Config + + hrmp::Config + + configuration::Config + { + type Event: From> + IsType<::Event>; + type DisputesHandler: disputes::DisputesHandler; + type RewardValidators: RewardValidators; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A candidate was backed. `[candidate, head_data]` + CandidateBacked(CandidateReceipt, HeadData, CoreIndex, GroupIndex), + /// A candidate was included. `[candidate, head_data]` + CandidateIncluded(CandidateReceipt, HeadData, CoreIndex, GroupIndex), + /// A candidate timed out. `[candidate, head_data]` + CandidateTimedOut(CandidateReceipt, HeadData, CoreIndex), + } + + #[pallet::error] + pub enum Error { + /// Availability bitfield has unexpected size. + WrongBitfieldSize, + /// Multiple bitfields submitted by same validator or validators out of order by index. + BitfieldDuplicateOrUnordered, + /// Validator index out of bounds. + ValidatorIndexOutOfBounds, + /// Invalid signature + InvalidBitfieldSignature, + /// Candidate submitted but para not scheduled. + UnscheduledCandidate, + /// Candidate scheduled despite pending candidate already existing for the para. + CandidateScheduledBeforeParaFree, + /// Candidate included with the wrong collator. + WrongCollator, + /// Scheduled cores out of order. + ScheduledOutOfOrder, + /// Head data exceeds the configured maximum. + HeadDataTooLarge, + /// Code upgrade prematurely. + PrematureCodeUpgrade, + /// Output code is too large + NewCodeTooLarge, + /// Candidate not in parent context. + CandidateNotInParentContext, + /// Invalid group index in core assignment. + InvalidGroupIndex, + /// Insufficient (non-majority) backing. + InsufficientBacking, + /// Invalid (bad signature, unknown validator, etc.) backing. + InvalidBacking, + /// Collator did not sign PoV. + NotCollatorSigned, + /// The validation data hash does not match expected. + ValidationDataHashMismatch, + /// The downward message queue is not processed correctly. + IncorrectDownwardMessageHandling, + /// At least one upward message sent does not pass the acceptance criteria. + InvalidUpwardMessages, + /// The candidate didn't follow the rules of HRMP watermark advancement. + HrmpWatermarkMishandling, + /// The HRMP messages sent by the candidate is not valid. + InvalidOutboundHrmp, + /// The validation code hash of the candidate is not valid. + InvalidValidationCodeHash, + /// The `para_head` hash in the candidate descriptor doesn't match the hash of the actual para head in the + /// commitments. + ParaHeadMismatch, + /// A bitfield that references a freed core, + /// either intentionally or as part of a concluded + /// invalid dispute. + BitfieldReferencesFreedCore, + } + + /// The latest bitfield for each validator, referred to by their index in the validator set. + #[pallet::storage] + pub(crate) type AvailabilityBitfields = + StorageMap<_, Twox64Concat, ValidatorIndex, AvailabilityBitfieldRecord>; + + /// Candidates pending availability by `ParaId`. + #[pallet::storage] + pub(crate) type PendingAvailability = + StorageMap<_, Twox64Concat, ParaId, CandidatePendingAvailability>; + + /// The commitments of candidates pending availability, by `ParaId`. + #[pallet::storage] + pub(crate) type PendingAvailabilityCommitments = + StorageMap<_, Twox64Concat, ParaId, CandidateCommitments>; + + #[pallet::call] + impl Pallet {} +} + +const LOG_TARGET: &str = "runtime::inclusion"; + +impl Pallet { + /// Block initialization logic, called by initializer. + pub(crate) fn initializer_initialize(_now: T::BlockNumber) -> Weight { + 0 + } + + /// Block finalization logic, called by initializer. + pub(crate) fn initializer_finalize() {} + + /// Handle an incoming session change. + pub(crate) fn initializer_on_new_session( + _notification: &crate::initializer::SessionChangeNotification, + ) { + // unlike most drain methods, drained elements are not cleared on `Drop` of the iterator + // and require consumption. + for _ in >::drain() {} + for _ in >::drain() {} + for _ in >::drain() {} + } + + /// Extract the freed cores based on cores that became available. + /// + /// Updates storage items `PendingAvailability` and `AvailabilityBitfields`. + pub(crate) fn update_pending_availability_and_get_freed_cores( + expected_bits: usize, + validators: &[ValidatorId], + signed_bitfields: UncheckedSignedAvailabilityBitfields, + core_lookup: F, + ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) + where + F: Fn(CoreIndex) -> Option, + { + let mut assigned_paras_record = (0..expected_bits) + .map(|bit_index| core_lookup(CoreIndex::from(bit_index as u32))) + .map(|opt_para_id| { + opt_para_id.map(|para_id| (para_id, PendingAvailability::::get(¶_id))) + }) + .collect::>(); + + let now = >::block_number(); + for (checked_bitfield, validator_index) in + signed_bitfields.into_iter().map(|signed_bitfield| { + // extracting unchecked data, since it's checked in `fn sanitize_bitfields` already. + let validator_idx = signed_bitfield.unchecked_validator_index(); + let checked_bitfield = signed_bitfield.unchecked_into_payload(); + (checked_bitfield, validator_idx) + }) { + for (bit_idx, _) in checked_bitfield.0.iter().enumerate().filter(|(_, is_av)| **is_av) { + let pending_availability = if let Some((_, pending_availability)) = + assigned_paras_record[bit_idx].as_mut() + { + pending_availability + } else { + // For honest validators, this happens in case of unoccupied cores, + // which in turn happens in case of a disputed candidate. + // A malicious one might include arbitrary indices, but they are represented + // by `None` values and will be sorted out in the next if case. + continue + }; + + // defensive check - this is constructed by loading the availability bitfield record, + // which is always `Some` if the core is occupied - that's why we're here. + let validator_index = validator_index.0 as usize; + if let Some(mut bit) = + pending_availability.as_mut().and_then(|candidate_pending_availability| { + candidate_pending_availability.availability_votes.get_mut(validator_index) + }) { + *bit = true; + } + } + + let record = + AvailabilityBitfieldRecord { bitfield: checked_bitfield, submitted_at: now }; + + >::insert(&validator_index, record); + } + + let mut enacted_candidate_weight = 0; + let threshold = availability_threshold(validators.len()); + + let mut freed_cores = Vec::with_capacity(expected_bits); + for (para_id, pending_availability) in assigned_paras_record + .into_iter() + .filter_map(|x| x) + .filter_map(|(id, p)| p.map(|p| (id, p))) + { + if pending_availability.availability_votes.count_ones() >= threshold { + >::remove(¶_id); + let commitments = match PendingAvailabilityCommitments::::take(¶_id) { + Some(commitments) => commitments, + None => { + log::warn!( + target: LOG_TARGET, + "Inclusion::process_bitfields: PendingAvailability and PendingAvailabilityCommitments + are out of sync, did someone mess with the storage?", + ); + continue + }, + }; + + if ON_CHAIN_USE { + let receipt = CommittedCandidateReceipt { + descriptor: pending_availability.descriptor, + commitments, + }; + + let weight = Self::enact_candidate( + pending_availability.relay_parent_number, + receipt, + pending_availability.backers, + pending_availability.availability_votes, + pending_availability.core, + pending_availability.backing_group, + ); + + enacted_candidate_weight = enacted_candidate_weight.saturating_add(weight); + } + + freed_cores.push((pending_availability.core, pending_availability.hash)); + } else { + >::insert(¶_id, &pending_availability); + } + } + + (freed_cores, enacted_candidate_weight) + } + + /// Process a set of incoming bitfields. + /// + /// Returns a `Vec` of `CandidateHash`es and their respective `AvailabilityCore`s that became available, + /// and cores free. Additionally returns the weight consumed by enacted candidates. + pub(crate) fn process_bitfields( + expected_bits: usize, + signed_bitfields: UncheckedSignedAvailabilityBitfields, + disputed_bitfield: DisputedBitfield, + core_lookup: impl Fn(CoreIndex) -> Option, + ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) { + let validators = shared::Pallet::::active_validator_keys(); + let session_index = shared::Pallet::::session_index(); + let parent_hash = frame_system::Pallet::::parent_hash(); + + let (checked_bitfields, _) = sanitize_bitfields::( + signed_bitfields, + disputed_bitfield, + expected_bits, + parent_hash, + session_index, + &validators[..], + FullCheck::Yes, + ); + + let (freed_cores, enacted_candidate_weight) = + Self::update_pending_availability_and_get_freed_cores::<_, true>( + expected_bits, + &validators[..], + checked_bitfields, + core_lookup, + ); + + (freed_cores, enacted_candidate_weight) + } + + /// Process candidates that have been backed. Provide the relay storage root, a set of candidates + /// and scheduled cores. + /// + /// Both should be sorted ascending by core index, and the candidates should be a subset of + /// scheduled cores. If these conditions are not met, the execution of the function fails. + pub(crate) fn process_candidates( + parent_storage_root: T::Hash, + candidates: Vec>, + scheduled: Vec, + group_validators: GV, + full_check: FullCheck, + ) -> Result, DispatchError> + where + GV: Fn(GroupIndex) -> Option>, + { + ensure!(candidates.len() <= scheduled.len(), Error::::UnscheduledCandidate); + + if scheduled.is_empty() { + return Ok(ProcessedCandidates::default()) + } + + let validators = shared::Pallet::::active_validator_keys(); + let parent_hash = >::parent_hash(); + + // At the moment we assume (and in fact enforce, below) that the relay-parent is always one + // before of the block where we include a candidate (i.e. this code path). + let now = >::block_number(); + let relay_parent_number = now - One::one(); + let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); + + // Collect candidate receipts with backers. + let mut candidate_receipt_with_backing_validator_indices = + Vec::with_capacity(candidates.len()); + + // Do all checks before writing storage. + let core_indices_and_backers = { + let mut skip = 0; + let mut core_indices_and_backers = Vec::with_capacity(candidates.len()); + let mut last_core = None; + + let mut check_assignment_in_order = |assignment: &CoreAssignment| -> DispatchResult { + ensure!( + last_core.map_or(true, |core| assignment.core > core), + Error::::ScheduledOutOfOrder, + ); + + last_core = Some(assignment.core); + Ok(()) + }; + + let signing_context = + SigningContext { parent_hash, session_index: shared::Pallet::::session_index() }; + + // We combine an outer loop over candidates with an inner loop over the scheduled, + // where each iteration of the outer loop picks up at the position + // in scheduled just after the past iteration left off. + // + // If the candidates appear in the same order as they appear in `scheduled`, + // then they should always be found. If the end of `scheduled` is reached, + // then the candidate was either not scheduled or out-of-order. + // + // In the meantime, we do certain sanity checks on the candidates and on the scheduled + // list. + 'next_backed_candidate: for (candidate_idx, backed_candidate) in + candidates.iter().enumerate() + { + if let FullCheck::Yes = full_check { + check_ctx.verify_backed_candidate( + parent_hash, + candidate_idx, + backed_candidate, + )?; + } + + let para_id = backed_candidate.descriptor().para_id; + let mut backers = bitvec::bitvec![BitOrderLsb0, u8; 0; validators.len()]; + + for (i, assignment) in scheduled[skip..].iter().enumerate() { + check_assignment_in_order(assignment)?; + + if para_id == assignment.para_id { + if let Some(required_collator) = assignment.required_collator() { + ensure!( + required_collator == &backed_candidate.descriptor().collator, + Error::::WrongCollator, + ); + } + + { + // this should never fail because the para is registered + let persisted_validation_data = + match crate::util::make_persisted_validation_data::( + para_id, + relay_parent_number, + parent_storage_root, + ) { + Some(l) => l, + None => { + // We don't want to error out here because it will + // brick the relay-chain. So we return early without + // doing anything. + return Ok(ProcessedCandidates::default()) + }, + }; + + let expected = persisted_validation_data.hash(); + + ensure!( + expected == + backed_candidate.descriptor().persisted_validation_data_hash, + Error::::ValidationDataHashMismatch, + ); + } + + ensure!( + >::get(¶_id).is_none() && + >::get(¶_id).is_none(), + Error::::CandidateScheduledBeforeParaFree, + ); + + // account for already skipped, and then skip this one. + skip = i + skip + 1; + + let group_vals = group_validators(assignment.group_idx) + .ok_or_else(|| Error::::InvalidGroupIndex)?; + + // check the signatures in the backing and that it is a majority. + { + let maybe_amount_validated = primitives::v1::check_candidate_backing( + &backed_candidate, + &signing_context, + group_vals.len(), + |intra_group_vi| { + group_vals + .get(intra_group_vi) + .and_then(|vi| validators.get(vi.0 as usize)) + .map(|v| v.clone()) + }, + ); + + match maybe_amount_validated { + Ok(amount_validated) => ensure!( + amount_validated * 2 > group_vals.len(), + Error::::InsufficientBacking, + ), + Err(()) => { + Err(Error::::InvalidBacking)?; + }, + } + + let mut backer_idx_and_attestation = + Vec::<(ValidatorIndex, ValidityAttestation)>::with_capacity( + backed_candidate.validator_indices.count_ones(), + ); + let candidate_receipt = backed_candidate.receipt(); + + for ((bit_idx, _), attestation) in backed_candidate + .validator_indices + .iter() + .enumerate() + .filter(|(_, signed)| **signed) + .zip(backed_candidate.validity_votes.iter().cloned()) + { + let val_idx = group_vals + .get(bit_idx) + .expect("this query succeeded above; qed"); + backer_idx_and_attestation.push((*val_idx, attestation)); + + backers.set(val_idx.0 as _, true); + } + candidate_receipt_with_backing_validator_indices + .push((candidate_receipt, backer_idx_and_attestation)); + } + + core_indices_and_backers.push(( + assignment.core, + backers, + assignment.group_idx, + )); + continue 'next_backed_candidate + } + } + + // end of loop reached means that the candidate didn't appear in the non-traversed + // section of the `scheduled` slice. either it was not scheduled or didn't appear in + // `candidates` in the correct order. + ensure!(false, Error::::UnscheduledCandidate); + } + + // check remainder of scheduled cores, if any. + for assignment in scheduled[skip..].iter() { + check_assignment_in_order(assignment)?; + } + + core_indices_and_backers + }; + + // one more sweep for actually writing to storage. + let core_indices = + core_indices_and_backers.iter().map(|&(ref c, _, _)| c.clone()).collect(); + for (candidate, (core, backers, group)) in + candidates.into_iter().zip(core_indices_and_backers) + { + let para_id = candidate.descriptor().para_id; + + // initialize all availability votes to 0. + let availability_votes: BitVec = + bitvec::bitvec![BitOrderLsb0, u8; 0; validators.len()]; + + Self::deposit_event(Event::::CandidateBacked( + candidate.candidate.to_plain(), + candidate.candidate.commitments.head_data.clone(), + core, + group, + )); + + let candidate_hash = candidate.candidate.hash(); + + let (descriptor, commitments) = + (candidate.candidate.descriptor, candidate.candidate.commitments); + + >::insert( + ¶_id, + CandidatePendingAvailability { + core, + hash: candidate_hash, + descriptor, + availability_votes, + relay_parent_number, + backers: backers.to_bitvec(), + backed_in_number: check_ctx.now, + backing_group: group, + }, + ); + >::insert(¶_id, commitments); + } + + Ok(ProcessedCandidates:: { + core_indices, + candidate_receipt_with_backing_validator_indices, + }) + } + + /// Run the acceptance criteria checks on the given candidate commitments. + pub(crate) fn check_validation_outputs_for_runtime_api( + para_id: ParaId, + validation_outputs: primitives::v1::CandidateCommitments, + ) -> bool { + // This function is meant to be called from the runtime APIs against the relay-parent, hence + // `relay_parent_number` is equal to `now`. + let now = >::block_number(); + let relay_parent_number = now; + let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); + + if let Err(err) = check_ctx.check_validation_outputs( + para_id, + &validation_outputs.head_data, + &validation_outputs.new_validation_code, + validation_outputs.processed_downward_messages, + &validation_outputs.upward_messages, + T::BlockNumber::from(validation_outputs.hrmp_watermark), + &validation_outputs.horizontal_messages, + ) { + log::debug!( + target: LOG_TARGET, + "Validation outputs checking for parachain `{}` failed: {:?}", + u32::from(para_id), + err, + ); + false + } else { + true + } + } + + fn enact_candidate( + relay_parent_number: T::BlockNumber, + receipt: CommittedCandidateReceipt, + backers: BitVec, + availability_votes: BitVec, + core_index: CoreIndex, + backing_group: GroupIndex, + ) -> Weight { + let plain = receipt.to_plain(); + let commitments = receipt.commitments; + let config = >::config(); + // initial weight is config read. + let mut weight = T::DbWeight::get().reads_writes(1, 0); + + T::RewardValidators::reward_backing( + backers + .iter() + .enumerate() + .filter(|(_, backed)| **backed) + .map(|(i, _)| ValidatorIndex(i as _)), + ); + + T::RewardValidators::reward_bitfields( + availability_votes + .iter() + .enumerate() + .filter(|(_, voted)| **voted) + .map(|(i, _)| ValidatorIndex(i as _)), + ); + + if let Some(new_code) = commitments.new_validation_code { + weight += >::schedule_code_upgrade( + receipt.descriptor.para_id, + new_code, + relay_parent_number, + &config, + ); + } + + // enact the messaging facet of the candidate. + weight += >::prune_dmq( + receipt.descriptor.para_id, + commitments.processed_downward_messages, + ); + weight += >::receive_upward_messages( + receipt.descriptor.para_id, + commitments.upward_messages, + ); + weight += >::prune_hrmp( + receipt.descriptor.para_id, + T::BlockNumber::from(commitments.hrmp_watermark), + ); + weight += >::queue_outbound_hrmp( + receipt.descriptor.para_id, + commitments.horizontal_messages, + ); + + Self::deposit_event(Event::::CandidateIncluded( + plain, + commitments.head_data.clone(), + core_index, + backing_group, + )); + + weight + + >::note_new_head( + receipt.descriptor.para_id, + commitments.head_data, + relay_parent_number, + ) + } + + /// Worst case weight for `enact_candidate`. + pub(crate) fn enact_candidate_weight( + hrmp_max_message_num_per_candidate: u32, + max_upward_message_num_per_candidate: u32, + hrmp_max_parachain_inbound_channels: u32, + hrmp_max_parathread_inbound_channels: u32, + ) -> Weight { + T::DbWeight::get() + .reads(1) + .saturating_add(>::schedule_code_upgrade_weight()) + .saturating_add(>::prune_dmq_weight()) + .saturating_add(>::receive_upward_messages_weight( + max_upward_message_num_per_candidate, + )) + .saturating_add(>::prune_hrmp_weight( + hrmp_max_parachain_inbound_channels, + hrmp_max_parathread_inbound_channels, + )) + .saturating_add(>::queue_outbound_hrmp_weight( + hrmp_max_message_num_per_candidate, + )) + .saturating_add(>::note_new_head_weight()) + } + + /// Cleans up all paras pending availability that the predicate returns true for. + /// + /// The predicate accepts the index of the core and the block number the core has been occupied + /// since (i.e. the block number the candidate was backed at in this fork of the relay chain). + /// + /// Returns a vector of cleaned-up core IDs. + pub(crate) fn collect_pending( + pred: impl Fn(CoreIndex, T::BlockNumber) -> bool, + ) -> Vec { + let mut cleaned_up_ids = Vec::new(); + let mut cleaned_up_cores = Vec::new(); + + for (para_id, pending_record) in >::iter() { + if pred(pending_record.core, pending_record.backed_in_number) { + cleaned_up_ids.push(para_id); + cleaned_up_cores.push(pending_record.core); + } + } + + for para_id in cleaned_up_ids { + let pending = >::take(¶_id); + let commitments = >::take(¶_id); + + if let (Some(pending), Some(commitments)) = (pending, commitments) { + // defensive: this should always be true. + let candidate = CandidateReceipt { + descriptor: pending.descriptor, + commitments_hash: commitments.hash(), + }; + + Self::deposit_event(Event::::CandidateTimedOut( + candidate, + commitments.head_data, + pending.core, + )); + } + } + + cleaned_up_cores + } + + /// Cleans up all paras pending availability that are in the given list of disputed candidates. + /// + /// Returns a vector of cleaned-up core IDs. + pub(crate) fn collect_disputed(disputed: &BTreeSet) -> Vec { + let mut cleaned_up_ids = Vec::new(); + let mut cleaned_up_cores = Vec::new(); + + for (para_id, pending_record) in >::iter() { + if disputed.contains(&pending_record.hash) { + cleaned_up_ids.push(para_id); + cleaned_up_cores.push(pending_record.core); + } + } + + for para_id in cleaned_up_ids { + let _ = >::take(¶_id); + let _ = >::take(¶_id); + } + + cleaned_up_cores + } + + /// Forcibly enact the candidate with the given ID as though it had been deemed available + /// by bitfields. + /// + /// Is a no-op if there is no candidate pending availability for this para-id. + /// This should generally not be used but it is useful during execution of Runtime APIs, + /// where the changes to the state are expected to be discarded directly after. + pub(crate) fn force_enact(para: ParaId) { + let pending = >::take(¶); + let commitments = >::take(¶); + + if let (Some(pending), Some(commitments)) = (pending, commitments) { + let candidate = + CommittedCandidateReceipt { descriptor: pending.descriptor, commitments }; + + Self::enact_candidate( + pending.relay_parent_number, + candidate, + pending.backers, + pending.availability_votes, + pending.core, + pending.backing_group, + ); + } + } + + /// Returns the `CommittedCandidateReceipt` pending availability for the para provided, if any. + pub(crate) fn candidate_pending_availability( + para: ParaId, + ) -> Option> { + >::get(¶) + .map(|p| p.descriptor) + .and_then(|d| >::get(¶).map(move |c| (d, c))) + .map(|(d, c)| CommittedCandidateReceipt { descriptor: d, commitments: c }) + } + + /// Returns the metadata around the candidate pending availability for the + /// para provided, if any. + pub(crate) fn pending_availability( + para: ParaId, + ) -> Option> { + >::get(¶) + } +} + +const fn availability_threshold(n_validators: usize) -> usize { + let mut threshold = (n_validators * 2) / 3; + threshold += (n_validators * 2) % 3; + threshold +} + +#[derive(derive_more::From, Debug)] +enum AcceptanceCheckErr { + HeadDataTooLarge, + PrematureCodeUpgrade, + NewCodeTooLarge, + ProcessedDownwardMessages(dmp::ProcessedDownwardMessagesAcceptanceErr), + UpwardMessages(ump::AcceptanceCheckErr), + HrmpWatermark(hrmp::HrmpWatermarkAcceptanceErr), + OutboundHrmp(hrmp::OutboundHrmpAcceptanceErr), +} + +impl AcceptanceCheckErr { + /// Returns the same error so that it can be threaded through a needle of `DispatchError` and + /// ultimately returned from a `Dispatchable`. + fn strip_into_dispatch_err(self) -> Error { + use AcceptanceCheckErr::*; + match self { + HeadDataTooLarge => Error::::HeadDataTooLarge, + PrematureCodeUpgrade => Error::::PrematureCodeUpgrade, + NewCodeTooLarge => Error::::NewCodeTooLarge, + ProcessedDownwardMessages(_) => Error::::IncorrectDownwardMessageHandling, + UpwardMessages(_) => Error::::InvalidUpwardMessages, + HrmpWatermark(_) => Error::::HrmpWatermarkMishandling, + OutboundHrmp(_) => Error::::InvalidOutboundHrmp, + } + } +} + +/// A collection of data required for checking a candidate. +pub(crate) struct CandidateCheckContext { + config: configuration::HostConfiguration, + now: T::BlockNumber, + relay_parent_number: T::BlockNumber, +} + +impl CandidateCheckContext { + pub(crate) fn new(now: T::BlockNumber, relay_parent_number: T::BlockNumber) -> Self { + Self { config: >::config(), now, relay_parent_number } + } + + /// Execute verification of the candidate. + /// + /// Assures: + /// * correct expected relay parent reference + /// * collator signature check passes + /// * code hash of commitments matches current code hash + /// * para head in the descriptor and commitments match + pub(crate) fn verify_backed_candidate( + &self, + parent_hash: ::Hash, + candidate_idx: usize, + backed_candidate: &BackedCandidate<::Hash>, + ) -> Result<(), Error> { + let para_id = backed_candidate.descriptor().para_id; + let now = self.now; + + // we require that the candidate is in the context of the parent block. + ensure!( + backed_candidate.descriptor().relay_parent == parent_hash, + Error::::CandidateNotInParentContext, + ); + ensure!( + backed_candidate.descriptor().check_collator_signature().is_ok(), + Error::::NotCollatorSigned, + ); + + let validation_code_hash = >::validation_code_hash_at(para_id, now, None) + // A candidate for a parachain without current validation code is not scheduled. + .ok_or_else(|| Error::::UnscheduledCandidate)?; + ensure!( + backed_candidate.descriptor().validation_code_hash == validation_code_hash, + Error::::InvalidValidationCodeHash, + ); + + ensure!( + backed_candidate.descriptor().para_head == + backed_candidate.candidate.commitments.head_data.hash(), + Error::::ParaHeadMismatch, + ); + + if let Err(err) = self.check_validation_outputs( + para_id, + &backed_candidate.candidate.commitments.head_data, + &backed_candidate.candidate.commitments.new_validation_code, + backed_candidate.candidate.commitments.processed_downward_messages, + &backed_candidate.candidate.commitments.upward_messages, + T::BlockNumber::from(backed_candidate.candidate.commitments.hrmp_watermark), + &backed_candidate.candidate.commitments.horizontal_messages, + ) { + log::debug!( + target: LOG_TARGET, + "Validation outputs checking during inclusion of a candidate {} for parachain `{}` failed: {:?}", + candidate_idx, + u32::from(para_id), + err, + ); + Err(err.strip_into_dispatch_err::())?; + }; + Ok(()) + } + + /// Check the given outputs after candidate validation on whether it passes the acceptance + /// criteria. + fn check_validation_outputs( + &self, + para_id: ParaId, + head_data: &HeadData, + new_validation_code: &Option, + processed_downward_messages: u32, + upward_messages: &[primitives::v1::UpwardMessage], + hrmp_watermark: T::BlockNumber, + horizontal_messages: &[primitives::v1::OutboundHrmpMessage], + ) -> Result<(), AcceptanceCheckErr> { + ensure!( + head_data.0.len() <= self.config.max_head_data_size as _, + AcceptanceCheckErr::HeadDataTooLarge, + ); + + // if any, the code upgrade attempt is allowed. + if let Some(new_validation_code) = new_validation_code { + let valid_upgrade_attempt = >::last_code_upgrade(para_id, true) + .map_or(true, |last| { + last <= self.relay_parent_number && + self.relay_parent_number.saturating_sub(last) >= + self.config.validation_upgrade_frequency + }); + ensure!(valid_upgrade_attempt, AcceptanceCheckErr::PrematureCodeUpgrade); + ensure!( + new_validation_code.0.len() <= self.config.max_code_size as _, + AcceptanceCheckErr::NewCodeTooLarge, + ); + } + + // check if the candidate passes the messaging acceptance criteria + >::check_processed_downward_messages(para_id, processed_downward_messages)?; + >::check_upward_messages(&self.config, para_id, upward_messages)?; + >::check_hrmp_watermark(para_id, self.relay_parent_number, hrmp_watermark)?; + >::check_outbound_hrmp(&self.config, para_id, horizontal_messages)?; + + Ok(()) + } +} diff --git a/runtime/parachains/src/inclusion.rs b/runtime/parachains/src/inclusion/tests.rs similarity index 60% rename from runtime/parachains/src/inclusion.rs rename to runtime/parachains/src/inclusion/tests.rs index 9c677fd35457..06bef4bbab80 100644 --- a/runtime/parachains/src/inclusion.rs +++ b/runtime/parachains/src/inclusion/tests.rs @@ -1,1075 +1,3 @@ -// 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 . - -//! The inclusion pallet is responsible for inclusion and availability of scheduled parachains -//! and parathreads. -//! -//! It is responsible for carrying candidates from being backable to being backed, and then from backed -//! to included. - -use crate::{ - configuration, disputes, dmp, hrmp, paras, - paras_inherent::{sanitize_bitfields, DisputedBitfield}, - scheduler::CoreAssignment, - shared, ump, -}; -use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec}; -use frame_support::pallet_prelude::*; -use parity_scale_codec::{Decode, Encode}; -use primitives::v1::{ - AvailabilityBitfield, BackedCandidate, CandidateCommitments, CandidateDescriptor, - CandidateHash, CandidateReceipt, CommittedCandidateReceipt, CoreIndex, GroupIndex, Hash, - HeadData, Id as ParaId, SigningContext, UncheckedSignedAvailabilityBitfields, ValidatorId, - ValidatorIndex, ValidityAttestation, -}; -use scale_info::TypeInfo; -use sp_runtime::{ - traits::{One, Saturating}, - DispatchError, -}; -use sp_std::{collections::btree_set::BTreeSet, prelude::*}; - -pub use pallet::*; - -/// A bitfield signed by a validator indicating that it is keeping its piece of the erasure-coding -/// for any backed candidates referred to by a `1` bit available. -/// -/// The bitfield's signature should be checked at the point of submission. Afterwards it can be -/// dropped. -#[derive(Encode, Decode, TypeInfo)] -#[cfg_attr(test, derive(Debug))] -pub struct AvailabilityBitfieldRecord { - bitfield: AvailabilityBitfield, // one bit per core. - submitted_at: N, // for accounting, as meaning of bits may change over time. -} - -/// Determines if all checks should be applied or if a subset was already completed -/// in a code path that will be executed afterwards or was already executed before. -#[derive(Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo)] -pub(crate) enum FullCheck { - /// Yes, do a full check, skip nothing. - Yes, - /// Skip a subset of checks that are already completed before. - /// - /// Attention: Should only be used when absolutely sure that the required - /// checks are completed before. - Skip, -} - -/// A backed candidate pending availability. -#[derive(Encode, Decode, PartialEq, TypeInfo)] -#[cfg_attr(test, derive(Debug, Default))] -pub struct CandidatePendingAvailability { - /// The availability core this is assigned to. - core: CoreIndex, - /// The candidate hash. - hash: CandidateHash, - /// The candidate descriptor. - descriptor: CandidateDescriptor, - /// The received availability votes. One bit per validator. - availability_votes: BitVec, - /// The backers of the candidate pending availability. - backers: BitVec, - /// The block number of the relay-parent of the receipt. - relay_parent_number: N, - /// The block number of the relay-chain block this was backed in. - backed_in_number: N, - /// The group index backing this block. - backing_group: GroupIndex, -} - -impl CandidatePendingAvailability { - /// Get the availability votes on the candidate. - pub(crate) fn availability_votes(&self) -> &BitVec { - &self.availability_votes - } - - /// Get the relay-chain block number this was backed in. - pub(crate) fn backed_in_number(&self) -> &N { - &self.backed_in_number - } - - /// Get the core index. - pub(crate) fn core_occupied(&self) -> CoreIndex { - self.core.clone() - } - - /// Get the candidate hash. - pub(crate) fn candidate_hash(&self) -> CandidateHash { - self.hash - } - - /// Get the candidate descriptor. - pub(crate) fn candidate_descriptor(&self) -> &CandidateDescriptor { - &self.descriptor - } - - #[cfg(any(feature = "runtime-benchmarks", test))] - pub(crate) fn new( - core: CoreIndex, - hash: CandidateHash, - descriptor: CandidateDescriptor, - availability_votes: BitVec, - backers: BitVec, - relay_parent_number: N, - backed_in_number: N, - backing_group: GroupIndex, - ) -> Self { - Self { - core, - hash, - descriptor, - availability_votes, - backers, - relay_parent_number, - backed_in_number, - backing_group, - } - } -} - -/// A hook for applying validator rewards -pub trait RewardValidators { - // Reward the validators with the given indices for issuing backing statements. - fn reward_backing(validators: impl IntoIterator); - // Reward the validators with the given indices for issuing availability bitfields. - // Validators are sent to this hook when they have contributed to the availability - // of a candidate by setting a bit in their bitfield. - fn reward_bitfields(validators: impl IntoIterator); -} - -/// Helper return type for `process_candidates`. -#[derive(Encode, Decode, PartialEq, TypeInfo)] -#[cfg_attr(test, derive(Debug))] -pub(crate) struct ProcessedCandidates { - pub(crate) core_indices: Vec, - pub(crate) candidate_receipt_with_backing_validator_indices: - Vec<(CandidateReceipt, Vec<(ValidatorIndex, ValidityAttestation)>)>, -} - -impl Default for ProcessedCandidates { - fn default() -> Self { - Self { - core_indices: Vec::new(), - candidate_receipt_with_backing_validator_indices: Vec::new(), - } - } -} - -#[frame_support::pallet] -pub mod pallet { - use super::*; - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: - frame_system::Config - + shared::Config - + paras::Config - + dmp::Config - + ump::Config - + hrmp::Config - + configuration::Config - { - type Event: From> + IsType<::Event>; - type DisputesHandler: disputes::DisputesHandler; - type RewardValidators: RewardValidators; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A candidate was backed. `[candidate, head_data]` - CandidateBacked(CandidateReceipt, HeadData, CoreIndex, GroupIndex), - /// A candidate was included. `[candidate, head_data]` - CandidateIncluded(CandidateReceipt, HeadData, CoreIndex, GroupIndex), - /// A candidate timed out. `[candidate, head_data]` - CandidateTimedOut(CandidateReceipt, HeadData, CoreIndex), - } - - #[pallet::error] - pub enum Error { - /// Availability bitfield has unexpected size. - WrongBitfieldSize, - /// Multiple bitfields submitted by same validator or validators out of order by index. - BitfieldDuplicateOrUnordered, - /// Validator index out of bounds. - ValidatorIndexOutOfBounds, - /// Invalid signature - InvalidBitfieldSignature, - /// Candidate submitted but para not scheduled. - UnscheduledCandidate, - /// Candidate scheduled despite pending candidate already existing for the para. - CandidateScheduledBeforeParaFree, - /// Candidate included with the wrong collator. - WrongCollator, - /// Scheduled cores out of order. - ScheduledOutOfOrder, - /// Head data exceeds the configured maximum. - HeadDataTooLarge, - /// Code upgrade prematurely. - PrematureCodeUpgrade, - /// Output code is too large - NewCodeTooLarge, - /// Candidate not in parent context. - CandidateNotInParentContext, - /// Invalid group index in core assignment. - InvalidGroupIndex, - /// Insufficient (non-majority) backing. - InsufficientBacking, - /// Invalid (bad signature, unknown validator, etc.) backing. - InvalidBacking, - /// Collator did not sign PoV. - NotCollatorSigned, - /// The validation data hash does not match expected. - ValidationDataHashMismatch, - /// The downward message queue is not processed correctly. - IncorrectDownwardMessageHandling, - /// At least one upward message sent does not pass the acceptance criteria. - InvalidUpwardMessages, - /// The candidate didn't follow the rules of HRMP watermark advancement. - HrmpWatermarkMishandling, - /// The HRMP messages sent by the candidate is not valid. - InvalidOutboundHrmp, - /// The validation code hash of the candidate is not valid. - InvalidValidationCodeHash, - /// The `para_head` hash in the candidate descriptor doesn't match the hash of the actual para head in the - /// commitments. - ParaHeadMismatch, - /// A bitfield that references a freed core, - /// either intentionally or as part of a concluded - /// invalid dispute. - BitfieldReferencesFreedCore, - } - - /// The latest bitfield for each validator, referred to by their index in the validator set. - #[pallet::storage] - pub(crate) type AvailabilityBitfields = - StorageMap<_, Twox64Concat, ValidatorIndex, AvailabilityBitfieldRecord>; - - /// Candidates pending availability by `ParaId`. - #[pallet::storage] - pub(crate) type PendingAvailability = - StorageMap<_, Twox64Concat, ParaId, CandidatePendingAvailability>; - - /// The commitments of candidates pending availability, by `ParaId`. - #[pallet::storage] - pub(crate) type PendingAvailabilityCommitments = - StorageMap<_, Twox64Concat, ParaId, CandidateCommitments>; - - #[pallet::call] - impl Pallet {} -} - -const LOG_TARGET: &str = "runtime::inclusion"; - -impl Pallet { - /// Block initialization logic, called by initializer. - pub(crate) fn initializer_initialize(_now: T::BlockNumber) -> Weight { - 0 - } - - /// Block finalization logic, called by initializer. - pub(crate) fn initializer_finalize() {} - - /// Handle an incoming session change. - pub(crate) fn initializer_on_new_session( - _notification: &crate::initializer::SessionChangeNotification, - ) { - // unlike most drain methods, drained elements are not cleared on `Drop` of the iterator - // and require consumption. - for _ in >::drain() {} - for _ in >::drain() {} - for _ in >::drain() {} - } - - /// Extract the freed cores based on cores that became available. - /// - /// Updates storage items `PendingAvailability` and `AvailabilityBitfields`. - pub(crate) fn update_pending_availability_and_get_freed_cores( - expected_bits: usize, - validators: &[ValidatorId], - signed_bitfields: UncheckedSignedAvailabilityBitfields, - core_lookup: F, - ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) - where - F: Fn(CoreIndex) -> Option, - { - let mut assigned_paras_record = (0..expected_bits) - .map(|bit_index| core_lookup(CoreIndex::from(bit_index as u32))) - .map(|opt_para_id| { - opt_para_id.map(|para_id| (para_id, PendingAvailability::::get(¶_id))) - }) - .collect::>(); - - let now = >::block_number(); - for (checked_bitfield, validator_index) in - signed_bitfields.into_iter().map(|signed_bitfield| { - // extracting unchecked data, since it's checked in `fn sanitize_bitfields` already. - let validator_idx = signed_bitfield.unchecked_validator_index(); - let checked_bitfield = signed_bitfield.unchecked_into_payload(); - (checked_bitfield, validator_idx) - }) { - for (bit_idx, _) in checked_bitfield.0.iter().enumerate().filter(|(_, is_av)| **is_av) { - let pending_availability = if let Some((_, pending_availability)) = - assigned_paras_record[bit_idx].as_mut() - { - pending_availability - } else { - // For honest validators, this happens in case of unoccupied cores, - // which in turn happens in case of a disputed candidate. - // A malicious one might include arbitrary indices, but they are represented - // by `None` values and will be sorted out in the next if case. - continue - }; - - // defensive check - this is constructed by loading the availability bitfield record, - // which is always `Some` if the core is occupied - that's why we're here. - let validator_index = validator_index.0 as usize; - if let Some(mut bit) = - pending_availability.as_mut().and_then(|candidate_pending_availability| { - candidate_pending_availability.availability_votes.get_mut(validator_index) - }) { - *bit = true; - } - } - - let record = - AvailabilityBitfieldRecord { bitfield: checked_bitfield, submitted_at: now }; - - >::insert(&validator_index, record); - } - - let mut enacted_candidate_weight = 0; - let threshold = availability_threshold(validators.len()); - - let mut freed_cores = Vec::with_capacity(expected_bits); - for (para_id, pending_availability) in assigned_paras_record - .into_iter() - .filter_map(|x| x) - .filter_map(|(id, p)| p.map(|p| (id, p))) - { - if pending_availability.availability_votes.count_ones() >= threshold { - >::remove(¶_id); - let commitments = match PendingAvailabilityCommitments::::take(¶_id) { - Some(commitments) => commitments, - None => { - log::warn!( - target: LOG_TARGET, - "Inclusion::process_bitfields: PendingAvailability and PendingAvailabilityCommitments - are out of sync, did someone mess with the storage?", - ); - continue - }, - }; - - if ON_CHAIN_USE { - let receipt = CommittedCandidateReceipt { - descriptor: pending_availability.descriptor, - commitments, - }; - - let weight = Self::enact_candidate( - pending_availability.relay_parent_number, - receipt, - pending_availability.backers, - pending_availability.availability_votes, - pending_availability.core, - pending_availability.backing_group, - ); - - enacted_candidate_weight = enacted_candidate_weight.saturating_add(weight); - } - - freed_cores.push((pending_availability.core, pending_availability.hash)); - } else { - >::insert(¶_id, &pending_availability); - } - } - - (freed_cores, enacted_candidate_weight) - } - - /// Process a set of incoming bitfields. - /// - /// Returns a `Vec` of `CandidateHash`es and their respective `AvailabilityCore`s that became available, - /// and cores free. Additionally returns the weight consumed by enacted candidates. - pub(crate) fn process_bitfields( - expected_bits: usize, - signed_bitfields: UncheckedSignedAvailabilityBitfields, - disputed_bitfield: DisputedBitfield, - core_lookup: impl Fn(CoreIndex) -> Option, - ) -> (Vec<(CoreIndex, CandidateHash)>, Weight) { - let validators = shared::Pallet::::active_validator_keys(); - let session_index = shared::Pallet::::session_index(); - let parent_hash = frame_system::Pallet::::parent_hash(); - - let (checked_bitfields, _) = sanitize_bitfields::( - signed_bitfields, - disputed_bitfield, - expected_bits, - parent_hash, - session_index, - &validators[..], - FullCheck::Yes, - ); - - let (freed_cores, enacted_candidate_weight) = - Self::update_pending_availability_and_get_freed_cores::<_, true>( - expected_bits, - &validators[..], - checked_bitfields, - core_lookup, - ); - - (freed_cores, enacted_candidate_weight) - } - - /// Process candidates that have been backed. Provide the relay storage root, a set of candidates - /// and scheduled cores. - /// - /// Both should be sorted ascending by core index, and the candidates should be a subset of - /// scheduled cores. If these conditions are not met, the execution of the function fails. - pub(crate) fn process_candidates( - parent_storage_root: T::Hash, - candidates: Vec>, - scheduled: Vec, - group_validators: GV, - full_check: FullCheck, - ) -> Result, DispatchError> - where - GV: Fn(GroupIndex) -> Option>, - { - ensure!(candidates.len() <= scheduled.len(), Error::::UnscheduledCandidate); - - if scheduled.is_empty() { - return Ok(ProcessedCandidates::default()) - } - - let validators = shared::Pallet::::active_validator_keys(); - let parent_hash = >::parent_hash(); - - // At the moment we assume (and in fact enforce, below) that the relay-parent is always one - // before of the block where we include a candidate (i.e. this code path). - let now = >::block_number(); - let relay_parent_number = now - One::one(); - let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); - - // Collect candidate receipts with backers. - let mut candidate_receipt_with_backing_validator_indices = - Vec::with_capacity(candidates.len()); - - // Do all checks before writing storage. - let core_indices_and_backers = { - let mut skip = 0; - let mut core_indices_and_backers = Vec::with_capacity(candidates.len()); - let mut last_core = None; - - let mut check_assignment_in_order = |assignment: &CoreAssignment| -> DispatchResult { - ensure!( - last_core.map_or(true, |core| assignment.core > core), - Error::::ScheduledOutOfOrder, - ); - - last_core = Some(assignment.core); - Ok(()) - }; - - let signing_context = - SigningContext { parent_hash, session_index: shared::Pallet::::session_index() }; - - // We combine an outer loop over candidates with an inner loop over the scheduled, - // where each iteration of the outer loop picks up at the position - // in scheduled just after the past iteration left off. - // - // If the candidates appear in the same order as they appear in `scheduled`, - // then they should always be found. If the end of `scheduled` is reached, - // then the candidate was either not scheduled or out-of-order. - // - // In the meantime, we do certain sanity checks on the candidates and on the scheduled - // list. - 'next_backed_candidate: for (candidate_idx, backed_candidate) in - candidates.iter().enumerate() - { - if let FullCheck::Yes = full_check { - check_ctx.verify_backed_candidate( - parent_hash, - candidate_idx, - backed_candidate, - )?; - } - - let para_id = backed_candidate.descriptor().para_id; - let mut backers = bitvec::bitvec![BitOrderLsb0, u8; 0; validators.len()]; - - for (i, assignment) in scheduled[skip..].iter().enumerate() { - check_assignment_in_order(assignment)?; - - if para_id == assignment.para_id { - if let Some(required_collator) = assignment.required_collator() { - ensure!( - required_collator == &backed_candidate.descriptor().collator, - Error::::WrongCollator, - ); - } - - { - // this should never fail because the para is registered - let persisted_validation_data = - match crate::util::make_persisted_validation_data::( - para_id, - relay_parent_number, - parent_storage_root, - ) { - Some(l) => l, - None => { - // We don't want to error out here because it will - // brick the relay-chain. So we return early without - // doing anything. - return Ok(ProcessedCandidates::default()) - }, - }; - - let expected = persisted_validation_data.hash(); - - ensure!( - expected == - backed_candidate.descriptor().persisted_validation_data_hash, - Error::::ValidationDataHashMismatch, - ); - } - - ensure!( - >::get(¶_id).is_none() && - >::get(¶_id).is_none(), - Error::::CandidateScheduledBeforeParaFree, - ); - - // account for already skipped, and then skip this one. - skip = i + skip + 1; - - let group_vals = group_validators(assignment.group_idx) - .ok_or_else(|| Error::::InvalidGroupIndex)?; - - // check the signatures in the backing and that it is a majority. - { - let maybe_amount_validated = primitives::v1::check_candidate_backing( - &backed_candidate, - &signing_context, - group_vals.len(), - |intra_group_vi| { - group_vals - .get(intra_group_vi) - .and_then(|vi| validators.get(vi.0 as usize)) - .map(|v| v.clone()) - }, - ); - - match maybe_amount_validated { - Ok(amount_validated) => ensure!( - amount_validated * 2 > group_vals.len(), - Error::::InsufficientBacking, - ), - Err(()) => { - Err(Error::::InvalidBacking)?; - }, - } - - let mut backer_idx_and_attestation = - Vec::<(ValidatorIndex, ValidityAttestation)>::with_capacity( - backed_candidate.validator_indices.count_ones(), - ); - let candidate_receipt = backed_candidate.receipt(); - - for ((bit_idx, _), attestation) in backed_candidate - .validator_indices - .iter() - .enumerate() - .filter(|(_, signed)| **signed) - .zip(backed_candidate.validity_votes.iter().cloned()) - { - let val_idx = group_vals - .get(bit_idx) - .expect("this query succeeded above; qed"); - backer_idx_and_attestation.push((*val_idx, attestation)); - - backers.set(val_idx.0 as _, true); - } - candidate_receipt_with_backing_validator_indices - .push((candidate_receipt, backer_idx_and_attestation)); - } - - core_indices_and_backers.push(( - assignment.core, - backers, - assignment.group_idx, - )); - continue 'next_backed_candidate - } - } - - // end of loop reached means that the candidate didn't appear in the non-traversed - // section of the `scheduled` slice. either it was not scheduled or didn't appear in - // `candidates` in the correct order. - ensure!(false, Error::::UnscheduledCandidate); - } - - // check remainder of scheduled cores, if any. - for assignment in scheduled[skip..].iter() { - check_assignment_in_order(assignment)?; - } - - core_indices_and_backers - }; - - // one more sweep for actually writing to storage. - let core_indices = - core_indices_and_backers.iter().map(|&(ref c, _, _)| c.clone()).collect(); - for (candidate, (core, backers, group)) in - candidates.into_iter().zip(core_indices_and_backers) - { - let para_id = candidate.descriptor().para_id; - - // initialize all availability votes to 0. - let availability_votes: BitVec = - bitvec::bitvec![BitOrderLsb0, u8; 0; validators.len()]; - - Self::deposit_event(Event::::CandidateBacked( - candidate.candidate.to_plain(), - candidate.candidate.commitments.head_data.clone(), - core, - group, - )); - - let candidate_hash = candidate.candidate.hash(); - - let (descriptor, commitments) = - (candidate.candidate.descriptor, candidate.candidate.commitments); - - >::insert( - ¶_id, - CandidatePendingAvailability { - core, - hash: candidate_hash, - descriptor, - availability_votes, - relay_parent_number, - backers: backers.to_bitvec(), - backed_in_number: check_ctx.now, - backing_group: group, - }, - ); - >::insert(¶_id, commitments); - } - - Ok(ProcessedCandidates:: { - core_indices, - candidate_receipt_with_backing_validator_indices, - }) - } - - /// Run the acceptance criteria checks on the given candidate commitments. - pub(crate) fn check_validation_outputs_for_runtime_api( - para_id: ParaId, - validation_outputs: primitives::v1::CandidateCommitments, - ) -> bool { - // This function is meant to be called from the runtime APIs against the relay-parent, hence - // `relay_parent_number` is equal to `now`. - let now = >::block_number(); - let relay_parent_number = now; - let check_ctx = CandidateCheckContext::::new(now, relay_parent_number); - - if let Err(err) = check_ctx.check_validation_outputs( - para_id, - &validation_outputs.head_data, - &validation_outputs.new_validation_code, - validation_outputs.processed_downward_messages, - &validation_outputs.upward_messages, - T::BlockNumber::from(validation_outputs.hrmp_watermark), - &validation_outputs.horizontal_messages, - ) { - log::debug!( - target: LOG_TARGET, - "Validation outputs checking for parachain `{}` failed: {:?}", - u32::from(para_id), - err, - ); - false - } else { - true - } - } - - fn enact_candidate( - relay_parent_number: T::BlockNumber, - receipt: CommittedCandidateReceipt, - backers: BitVec, - availability_votes: BitVec, - core_index: CoreIndex, - backing_group: GroupIndex, - ) -> Weight { - let plain = receipt.to_plain(); - let commitments = receipt.commitments; - let config = >::config(); - // initial weight is config read. - let mut weight = T::DbWeight::get().reads_writes(1, 0); - - T::RewardValidators::reward_backing( - backers - .iter() - .enumerate() - .filter(|(_, backed)| **backed) - .map(|(i, _)| ValidatorIndex(i as _)), - ); - - T::RewardValidators::reward_bitfields( - availability_votes - .iter() - .enumerate() - .filter(|(_, voted)| **voted) - .map(|(i, _)| ValidatorIndex(i as _)), - ); - - if let Some(new_code) = commitments.new_validation_code { - weight += >::schedule_code_upgrade( - receipt.descriptor.para_id, - new_code, - relay_parent_number, - &config, - ); - } - - // enact the messaging facet of the candidate. - weight += >::prune_dmq( - receipt.descriptor.para_id, - commitments.processed_downward_messages, - ); - weight += >::receive_upward_messages( - receipt.descriptor.para_id, - commitments.upward_messages, - ); - weight += >::prune_hrmp( - receipt.descriptor.para_id, - T::BlockNumber::from(commitments.hrmp_watermark), - ); - weight += >::queue_outbound_hrmp( - receipt.descriptor.para_id, - commitments.horizontal_messages, - ); - - Self::deposit_event(Event::::CandidateIncluded( - plain, - commitments.head_data.clone(), - core_index, - backing_group, - )); - - weight + - >::note_new_head( - receipt.descriptor.para_id, - commitments.head_data, - relay_parent_number, - ) - } - - /// Worst case weight for `enact_candidate`. - pub(crate) fn enact_candidate_weight( - hrmp_max_message_num_per_candidate: u32, - max_upward_message_num_per_candidate: u32, - hrmp_max_parachain_inbound_channels: u32, - hrmp_max_parathread_inbound_channels: u32, - ) -> Weight { - T::DbWeight::get() - .reads(1) - .saturating_add(>::schedule_code_upgrade_weight()) - .saturating_add(>::prune_dmq_weight()) - .saturating_add(>::receive_upward_messages_weight( - max_upward_message_num_per_candidate, - )) - .saturating_add(>::prune_hrmp_weight( - hrmp_max_parachain_inbound_channels, - hrmp_max_parathread_inbound_channels, - )) - .saturating_add(>::queue_outbound_hrmp_weight( - hrmp_max_message_num_per_candidate, - )) - .saturating_add(>::note_new_head_weight()) - } - - /// Cleans up all paras pending availability that the predicate returns true for. - /// - /// The predicate accepts the index of the core and the block number the core has been occupied - /// since (i.e. the block number the candidate was backed at in this fork of the relay chain). - /// - /// Returns a vector of cleaned-up core IDs. - pub(crate) fn collect_pending( - pred: impl Fn(CoreIndex, T::BlockNumber) -> bool, - ) -> Vec { - let mut cleaned_up_ids = Vec::new(); - let mut cleaned_up_cores = Vec::new(); - - for (para_id, pending_record) in >::iter() { - if pred(pending_record.core, pending_record.backed_in_number) { - cleaned_up_ids.push(para_id); - cleaned_up_cores.push(pending_record.core); - } - } - - for para_id in cleaned_up_ids { - let pending = >::take(¶_id); - let commitments = >::take(¶_id); - - if let (Some(pending), Some(commitments)) = (pending, commitments) { - // defensive: this should always be true. - let candidate = CandidateReceipt { - descriptor: pending.descriptor, - commitments_hash: commitments.hash(), - }; - - Self::deposit_event(Event::::CandidateTimedOut( - candidate, - commitments.head_data, - pending.core, - )); - } - } - - cleaned_up_cores - } - - /// Cleans up all paras pending availability that are in the given list of disputed candidates. - /// - /// Returns a vector of cleaned-up core IDs. - pub(crate) fn collect_disputed(disputed: &BTreeSet) -> Vec { - let mut cleaned_up_ids = Vec::new(); - let mut cleaned_up_cores = Vec::new(); - - for (para_id, pending_record) in >::iter() { - if disputed.contains(&pending_record.hash) { - cleaned_up_ids.push(para_id); - cleaned_up_cores.push(pending_record.core); - } - } - - for para_id in cleaned_up_ids { - let _ = >::take(¶_id); - let _ = >::take(¶_id); - } - - cleaned_up_cores - } - - /// Forcibly enact the candidate with the given ID as though it had been deemed available - /// by bitfields. - /// - /// Is a no-op if there is no candidate pending availability for this para-id. - /// This should generally not be used but it is useful during execution of Runtime APIs, - /// where the changes to the state are expected to be discarded directly after. - pub(crate) fn force_enact(para: ParaId) { - let pending = >::take(¶); - let commitments = >::take(¶); - - if let (Some(pending), Some(commitments)) = (pending, commitments) { - let candidate = - CommittedCandidateReceipt { descriptor: pending.descriptor, commitments }; - - Self::enact_candidate( - pending.relay_parent_number, - candidate, - pending.backers, - pending.availability_votes, - pending.core, - pending.backing_group, - ); - } - } - - /// Returns the `CommittedCandidateReceipt` pending availability for the para provided, if any. - pub(crate) fn candidate_pending_availability( - para: ParaId, - ) -> Option> { - >::get(¶) - .map(|p| p.descriptor) - .and_then(|d| >::get(¶).map(move |c| (d, c))) - .map(|(d, c)| CommittedCandidateReceipt { descriptor: d, commitments: c }) - } - - /// Returns the metadata around the candidate pending availability for the - /// para provided, if any. - pub(crate) fn pending_availability( - para: ParaId, - ) -> Option> { - >::get(¶) - } -} - -const fn availability_threshold(n_validators: usize) -> usize { - let mut threshold = (n_validators * 2) / 3; - threshold += (n_validators * 2) % 3; - threshold -} - -#[derive(derive_more::From, Debug)] -enum AcceptanceCheckErr { - HeadDataTooLarge, - PrematureCodeUpgrade, - NewCodeTooLarge, - ProcessedDownwardMessages(dmp::ProcessedDownwardMessagesAcceptanceErr), - UpwardMessages(ump::AcceptanceCheckErr), - HrmpWatermark(hrmp::HrmpWatermarkAcceptanceErr), - OutboundHrmp(hrmp::OutboundHrmpAcceptanceErr), -} - -impl AcceptanceCheckErr { - /// Returns the same error so that it can be threaded through a needle of `DispatchError` and - /// ultimately returned from a `Dispatchable`. - fn strip_into_dispatch_err(self) -> Error { - use AcceptanceCheckErr::*; - match self { - HeadDataTooLarge => Error::::HeadDataTooLarge, - PrematureCodeUpgrade => Error::::PrematureCodeUpgrade, - NewCodeTooLarge => Error::::NewCodeTooLarge, - ProcessedDownwardMessages(_) => Error::::IncorrectDownwardMessageHandling, - UpwardMessages(_) => Error::::InvalidUpwardMessages, - HrmpWatermark(_) => Error::::HrmpWatermarkMishandling, - OutboundHrmp(_) => Error::::InvalidOutboundHrmp, - } - } -} - -/// A collection of data required for checking a candidate. -pub(crate) struct CandidateCheckContext { - config: configuration::HostConfiguration, - now: T::BlockNumber, - relay_parent_number: T::BlockNumber, -} - -impl CandidateCheckContext { - pub(crate) fn new(now: T::BlockNumber, relay_parent_number: T::BlockNumber) -> Self { - Self { config: >::config(), now, relay_parent_number } - } - - /// Execute verification of the candidate. - /// - /// Assures: - /// * correct expected relay parent reference - /// * collator signature check passes - /// * code hash of commitments matches current code hash - /// * para head in the descriptor and commitments match - pub(crate) fn verify_backed_candidate( - &self, - parent_hash: ::Hash, - candidate_idx: usize, - backed_candidate: &BackedCandidate<::Hash>, - ) -> Result<(), Error> { - let para_id = backed_candidate.descriptor().para_id; - let now = self.now; - - // we require that the candidate is in the context of the parent block. - ensure!( - backed_candidate.descriptor().relay_parent == parent_hash, - Error::::CandidateNotInParentContext, - ); - ensure!( - backed_candidate.descriptor().check_collator_signature().is_ok(), - Error::::NotCollatorSigned, - ); - - let validation_code_hash = >::validation_code_hash_at(para_id, now, None) - // A candidate for a parachain without current validation code is not scheduled. - .ok_or_else(|| Error::::UnscheduledCandidate)?; - ensure!( - backed_candidate.descriptor().validation_code_hash == validation_code_hash, - Error::::InvalidValidationCodeHash, - ); - - ensure!( - backed_candidate.descriptor().para_head == - backed_candidate.candidate.commitments.head_data.hash(), - Error::::ParaHeadMismatch, - ); - - if let Err(err) = self.check_validation_outputs( - para_id, - &backed_candidate.candidate.commitments.head_data, - &backed_candidate.candidate.commitments.new_validation_code, - backed_candidate.candidate.commitments.processed_downward_messages, - &backed_candidate.candidate.commitments.upward_messages, - T::BlockNumber::from(backed_candidate.candidate.commitments.hrmp_watermark), - &backed_candidate.candidate.commitments.horizontal_messages, - ) { - log::debug!( - target: LOG_TARGET, - "Validation outputs checking during inclusion of a candidate {} for parachain `{}` failed: {:?}", - candidate_idx, - u32::from(para_id), - err, - ); - Err(err.strip_into_dispatch_err::())?; - }; - Ok(()) - } - - /// Check the given outputs after candidate validation on whether it passes the acceptance - /// criteria. - fn check_validation_outputs( - &self, - para_id: ParaId, - head_data: &HeadData, - new_validation_code: &Option, - processed_downward_messages: u32, - upward_messages: &[primitives::v1::UpwardMessage], - hrmp_watermark: T::BlockNumber, - horizontal_messages: &[primitives::v1::OutboundHrmpMessage], - ) -> Result<(), AcceptanceCheckErr> { - ensure!( - head_data.0.len() <= self.config.max_head_data_size as _, - AcceptanceCheckErr::HeadDataTooLarge, - ); - - // if any, the code upgrade attempt is allowed. - if let Some(new_validation_code) = new_validation_code { - let valid_upgrade_attempt = >::last_code_upgrade(para_id, true) - .map_or(true, |last| { - last <= self.relay_parent_number && - self.relay_parent_number.saturating_sub(last) >= - self.config.validation_upgrade_frequency - }); - ensure!(valid_upgrade_attempt, AcceptanceCheckErr::PrematureCodeUpgrade); - ensure!( - new_validation_code.0.len() <= self.config.max_code_size as _, - AcceptanceCheckErr::NewCodeTooLarge, - ); - } - - // check if the candidate passes the messaging acceptance criteria - >::check_processed_downward_messages(para_id, processed_downward_messages)?; - >::check_upward_messages(&self.config, para_id, upward_messages)?; - >::check_hrmp_watermark(para_id, self.relay_parent_number, hrmp_watermark)?; - >::check_outbound_hrmp(&self.config, para_id, horizontal_messages)?; - - Ok(()) - } -} - -#[cfg(test)] -pub(crate) mod tests { use super::*; use crate::{ configuration::HostConfiguration, @@ -2988,5 +1916,4 @@ pub(crate) mod tests { }); } - // TODO [now]: test `collect_disputed` -} + // TODO [now]: test `collect_disputed` \ No newline at end of file From 86fe00f096bf053d3595c40de864a97e146a7399 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:28:37 -0800 Subject: [PATCH 23/27] fmt --- runtime/parachains/src/inclusion/tests.rs | 3362 ++++++++--------- runtime/parachains/src/paras_inherent/mod.rs | 13 +- .../parachains/src/paras_inherent/tests.rs | 2054 +++++----- .../parachains/src/paras_inherent/weights.rs | 14 +- 4 files changed, 2701 insertions(+), 2742 deletions(-) diff --git a/runtime/parachains/src/inclusion/tests.rs b/runtime/parachains/src/inclusion/tests.rs index 06bef4bbab80..878946060fa1 100644 --- a/runtime/parachains/src/inclusion/tests.rs +++ b/runtime/parachains/src/inclusion/tests.rs @@ -1,287 +1,591 @@ - use super::*; - use crate::{ - configuration::HostConfiguration, - initializer::SessionChangeNotification, - mock::{ - new_test_ext, Configuration, MockGenesisConfig, ParaInclusion, Paras, ParasShared, - System, Test, + +use super::*; +use crate::{ + configuration::HostConfiguration, + initializer::SessionChangeNotification, + mock::{ + new_test_ext, Configuration, MockGenesisConfig, ParaInclusion, Paras, ParasShared, System, + Test, + }, + paras::ParaGenesisArgs, + paras_inherent::DisputedBitfield, + scheduler::AssignmentKind, +}; +use frame_support::assert_noop; +use futures::executor::block_on; +use keyring::Sr25519Keyring; +use primitives::{ + v0::PARACHAIN_KEY_TYPE_ID, + v1::{ + BlockNumber, CandidateCommitments, CandidateDescriptor, CollatorId, + CompactStatement as Statement, Hash, SignedAvailabilityBitfield, SignedStatement, + UncheckedSignedAvailabilityBitfield, ValidationCode, ValidatorId, ValidityAttestation, + }, +}; +use sc_keystore::LocalKeystore; +use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; +use std::sync::Arc; + +fn default_config() -> HostConfiguration { + let mut config = HostConfiguration::default(); + config.parathread_cores = 1; + config.max_code_size = 3; + config +} + +pub(crate) fn genesis_config(paras: Vec<(ParaId, bool)>) -> MockGenesisConfig { + MockGenesisConfig { + paras: paras::GenesisConfig { + paras: paras + .into_iter() + .map(|(id, is_chain)| { + ( + id, + ParaGenesisArgs { + genesis_head: Vec::new().into(), + validation_code: Vec::new().into(), + parachain: is_chain, + }, + ) + }) + .collect(), + ..Default::default() }, - paras::ParaGenesisArgs, - paras_inherent::DisputedBitfield, - scheduler::AssignmentKind, - }; - use frame_support::assert_noop; - use futures::executor::block_on; - use keyring::Sr25519Keyring; - use primitives::{ - v0::PARACHAIN_KEY_TYPE_ID, - v1::{ - BlockNumber, CandidateCommitments, CandidateDescriptor, CollatorId, - CompactStatement as Statement, Hash, SignedAvailabilityBitfield, SignedStatement, - UncheckedSignedAvailabilityBitfield, ValidationCode, ValidatorId, ValidityAttestation, + configuration: configuration::GenesisConfig { + config: default_config(), + ..Default::default() }, + ..Default::default() + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum BackingKind { + #[allow(unused)] + Unanimous, + Threshold, + Lacking, +} + +pub(crate) fn collator_sign_candidate( + collator: Sr25519Keyring, + candidate: &mut CommittedCandidateReceipt, +) { + candidate.descriptor.collator = collator.public().into(); + + let payload = primitives::v1::collator_signature_payload( + &candidate.descriptor.relay_parent, + &candidate.descriptor.para_id, + &candidate.descriptor.persisted_validation_data_hash, + &candidate.descriptor.pov_hash, + &candidate.descriptor.validation_code_hash, + ); + + candidate.descriptor.signature = collator.sign(&payload[..]).into(); + assert!(candidate.descriptor().check_collator_signature().is_ok()); +} + +pub(crate) async fn back_candidate( + candidate: CommittedCandidateReceipt, + validators: &[Sr25519Keyring], + group: &[ValidatorIndex], + keystore: &SyncCryptoStorePtr, + signing_context: &SigningContext, + kind: BackingKind, +) -> BackedCandidate { + let mut validator_indices = bitvec::bitvec![BitOrderLsb0, u8; 0; group.len()]; + let threshold = (group.len() / 2) + 1; + + let signing = match kind { + BackingKind::Unanimous => group.len(), + BackingKind::Threshold => threshold, + BackingKind::Lacking => threshold.saturating_sub(1), + }; + + let mut validity_votes = Vec::with_capacity(signing); + let candidate_hash = candidate.hash(); + + for (idx_in_group, val_idx) in group.iter().enumerate().take(signing) { + let key: Sr25519Keyring = validators[val_idx.0 as usize]; + *validator_indices.get_mut(idx_in_group).unwrap() = true; + + let signature = SignedStatement::sign( + &keystore, + Statement::Valid(candidate_hash), + signing_context, + *val_idx, + &key.public().into(), + ) + .await + .unwrap() + .unwrap() + .signature() + .clone(); + + validity_votes.push(ValidityAttestation::Explicit(signature).into()); + } + + let backed = BackedCandidate { candidate, validity_votes, validator_indices }; + + let successfully_backed = + primitives::v1::check_candidate_backing(&backed, signing_context, group.len(), |i| { + Some(validators[group[i].0 as usize].public().into()) + }) + .ok() + .unwrap_or(0) * + 2 > group.len(); + + match kind { + BackingKind::Unanimous | BackingKind::Threshold => assert!(successfully_backed), + BackingKind::Lacking => assert!(!successfully_backed), }; - use sc_keystore::LocalKeystore; - use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; - use std::sync::Arc; - - fn default_config() -> HostConfiguration { - let mut config = HostConfiguration::default(); - config.parathread_cores = 1; - config.max_code_size = 3; - config + + backed +} + +pub(crate) fn run_to_block( + to: BlockNumber, + new_session: impl Fn(BlockNumber) -> Option>, +) { + while System::block_number() < to { + let b = System::block_number(); + + ParaInclusion::initializer_finalize(); + Paras::initializer_finalize(); + ParasShared::initializer_finalize(); + + if let Some(notification) = new_session(b + 1) { + ParasShared::initializer_on_new_session( + notification.session_index, + notification.random_seed, + ¬ification.new_config, + notification.validators.clone(), + ); + Paras::initializer_on_new_session(¬ification); + ParaInclusion::initializer_on_new_session(¬ification); + } + + System::on_finalize(b); + + System::on_initialize(b + 1); + System::set_block_number(b + 1); + + ParasShared::initializer_initialize(b + 1); + Paras::initializer_initialize(b + 1); + ParaInclusion::initializer_initialize(b + 1); } +} + +pub(crate) fn expected_bits() -> usize { + Paras::parachains().len() + Configuration::config().parathread_cores as usize +} + +fn default_bitfield() -> AvailabilityBitfield { + AvailabilityBitfield(bitvec::bitvec![BitOrderLsb0, u8; 0; expected_bits()]) +} + +fn default_availability_votes() -> BitVec { + bitvec::bitvec![BitOrderLsb0, u8; 0; ParasShared::active_validator_keys().len()] +} - pub(crate) fn genesis_config(paras: Vec<(ParaId, bool)>) -> MockGenesisConfig { - MockGenesisConfig { - paras: paras::GenesisConfig { - paras: paras - .into_iter() - .map(|(id, is_chain)| { - ( - id, - ParaGenesisArgs { - genesis_head: Vec::new().into(), - validation_code: Vec::new().into(), - parachain: is_chain, - }, - ) - }) - .collect(), +fn default_backing_bitfield() -> BitVec { + bitvec::bitvec![BitOrderLsb0, u8; 0; ParasShared::active_validator_keys().len()] +} + +fn backing_bitfield(v: &[usize]) -> BitVec { + let mut b = default_backing_bitfield(); + for i in v { + b.set(*i, true); + } + b +} + +pub(crate) fn validator_pubkeys(val_ids: &[Sr25519Keyring]) -> Vec { + val_ids.iter().map(|v| v.public().into()).collect() +} + +pub(crate) async fn sign_bitfield( + keystore: &SyncCryptoStorePtr, + key: &Sr25519Keyring, + validator_index: ValidatorIndex, + bitfield: AvailabilityBitfield, + signing_context: &SigningContext, +) -> SignedAvailabilityBitfield { + SignedAvailabilityBitfield::sign( + &keystore, + bitfield, + &signing_context, + validator_index, + &key.public().into(), + ) + .await + .unwrap() + .unwrap() +} + +#[derive(Default)] +pub(crate) struct TestCandidateBuilder { + pub(crate) para_id: ParaId, + pub(crate) head_data: HeadData, + pub(crate) para_head_hash: Option, + pub(crate) pov_hash: Hash, + pub(crate) relay_parent: Hash, + pub(crate) persisted_validation_data_hash: Hash, + pub(crate) new_validation_code: Option, + pub(crate) validation_code: ValidationCode, + pub(crate) hrmp_watermark: BlockNumber, +} + +impl TestCandidateBuilder { + pub(crate) fn build(self) -> CommittedCandidateReceipt { + CommittedCandidateReceipt { + descriptor: CandidateDescriptor { + para_id: self.para_id, + pov_hash: self.pov_hash, + relay_parent: self.relay_parent, + persisted_validation_data_hash: self.persisted_validation_data_hash, + validation_code_hash: self.validation_code.hash(), + para_head: self.para_head_hash.unwrap_or_else(|| self.head_data.hash()), ..Default::default() }, - configuration: configuration::GenesisConfig { - config: default_config(), + commitments: CandidateCommitments { + head_data: self.head_data, + new_validation_code: self.new_validation_code, + hrmp_watermark: self.hrmp_watermark, ..Default::default() }, - ..Default::default() } } +} + +pub(crate) fn make_vdata_hash(para_id: ParaId) -> Option { + let relay_parent_number = >::block_number() - 1; + let persisted_validation_data = crate::util::make_persisted_validation_data::( + para_id, + relay_parent_number, + Default::default(), + )?; + Some(persisted_validation_data.hash()) +} + +#[test] +fn collect_pending_cleans_up_pending() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + new_test_ext(genesis_config(paras)).execute_with(|| { + let default_candidate = TestCandidateBuilder::default().build(); + >::insert( + chain_a, + CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: default_candidate.hash(), + descriptor: default_candidate.descriptor.clone(), + availability_votes: default_availability_votes(), + relay_parent_number: 0, + backed_in_number: 0, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(0), + }, + ); + PendingAvailabilityCommitments::::insert( + chain_a, + default_candidate.commitments.clone(), + ); - #[derive(Debug, Clone, Copy, PartialEq)] - pub(crate) enum BackingKind { - #[allow(unused)] - Unanimous, - Threshold, - Lacking, + >::insert( + &chain_b, + CandidatePendingAvailability { + core: CoreIndex::from(1), + hash: default_candidate.hash(), + descriptor: default_candidate.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 0, + backed_in_number: 0, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(1), + }, + ); + PendingAvailabilityCommitments::::insert(chain_b, default_candidate.commitments); + + run_to_block(5, |_| None); + + assert!(>::get(&chain_a).is_some()); + assert!(>::get(&chain_b).is_some()); + assert!(>::get(&chain_a).is_some()); + assert!(>::get(&chain_b).is_some()); + + ParaInclusion::collect_pending(|core, _since| core == CoreIndex::from(0)); + + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_b).is_some()); + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_b).is_some()); + }); +} + +#[test] +fn bitfield_checks() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); } + let validator_public = validator_pubkeys(&validators); - pub(crate) fn collator_sign_candidate( - collator: Sr25519Keyring, - candidate: &mut CommittedCandidateReceipt, - ) { - candidate.descriptor.collator = collator.public().into(); - - let payload = primitives::v1::collator_signature_payload( - &candidate.descriptor.relay_parent, - &candidate.descriptor.para_id, - &candidate.descriptor.persisted_validation_data_hash, - &candidate.descriptor.pov_hash, - &candidate.descriptor.validation_code_hash, - ); + new_test_ext(genesis_config(paras.clone())).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); - candidate.descriptor.signature = collator.sign(&payload[..]).into(); - assert!(candidate.descriptor().check_collator_signature().is_ok()); - } + let signing_context = + SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - pub(crate) async fn back_candidate( - candidate: CommittedCandidateReceipt, - validators: &[Sr25519Keyring], - group: &[ValidatorIndex], - keystore: &SyncCryptoStorePtr, - signing_context: &SigningContext, - kind: BackingKind, - ) -> BackedCandidate { - let mut validator_indices = bitvec::bitvec![BitOrderLsb0, u8; 0; group.len()]; - let threshold = (group.len() / 2) + 1; - - let signing = match kind { - BackingKind::Unanimous => group.len(), - BackingKind::Threshold => threshold, - BackingKind::Lacking => threshold.saturating_sub(1), + let core_lookup = |core| match core { + core if core == CoreIndex::from(0) => Some(chain_a), + core if core == CoreIndex::from(1) => Some(chain_b), + core if core == CoreIndex::from(2) => Some(thread_a), + core if core == CoreIndex::from(3) => None, // for the expected_cores() + 1 test below. + _ => panic!("out of bounds for testing"), }; - let mut validity_votes = Vec::with_capacity(signing); - let candidate_hash = candidate.hash(); - - for (idx_in_group, val_idx) in group.iter().enumerate().take(signing) { - let key: Sr25519Keyring = validators[val_idx.0 as usize]; - *validator_indices.get_mut(idx_in_group).unwrap() = true; + // mark all candidates as pending availability + let set_pending_av = || { + for (p_id, _) in paras { + PendingAvailability::::insert( + p_id, + CandidatePendingAvailability { + availability_votes: default_availability_votes(), + ..Default::default() + }, + ) + } + }; - let signature = SignedStatement::sign( + // too many bits in bitfield + { + let mut bare_bitfield = default_bitfield(); + bare_bitfield.0.push(false); + let signed = block_on(sign_bitfield( &keystore, - Statement::Valid(candidate_hash), - signing_context, - *val_idx, - &key.public().into(), - ) - .await - .unwrap() - .unwrap() - .signature() - .clone(); + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); - validity_votes.push(ValidityAttestation::Explicit(signature).into()); + assert_eq!( + ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0, + vec![] + ); } - let backed = BackedCandidate { candidate, validity_votes, validator_indices }; - - let successfully_backed = - primitives::v1::check_candidate_backing(&backed, signing_context, group.len(), |i| { - Some(validators[group[i].0 as usize].public().into()) - }) - .ok() - .unwrap_or(0) * 2 > - group.len(); + // not enough bits + { + let bare_bitfield = default_bitfield(); + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); - match kind { - BackingKind::Unanimous | BackingKind::Threshold => assert!(successfully_backed), - BackingKind::Lacking => assert!(!successfully_backed), - }; + assert_eq!( + ParaInclusion::process_bitfields( + expected_bits() + 1, + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0, + vec![] + ); + } - backed - } + // duplicate. + { + set_pending_av.clone()(); + let back_core_0_bitfield = { + let mut b = default_bitfield(); + b.0.set(0, true); + b + }; + let signed: UncheckedSignedAvailabilityBitfield = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + back_core_0_bitfield, + &signing_context, + )) + .into(); - pub(crate) fn run_to_block( - to: BlockNumber, - new_session: impl Fn(BlockNumber) -> Option>, - ) { - while System::block_number() < to { - let b = System::block_number(); - - ParaInclusion::initializer_finalize(); - Paras::initializer_finalize(); - ParasShared::initializer_finalize(); - - if let Some(notification) = new_session(b + 1) { - ParasShared::initializer_on_new_session( - notification.session_index, - notification.random_seed, - ¬ification.new_config, - notification.validators.clone(), - ); - Paras::initializer_on_new_session(¬ification); - ParaInclusion::initializer_on_new_session(¬ification); - } + assert_eq!( + >::get(chain_a) + .unwrap() + .availability_votes + .count_ones(), + 0 + ); - System::on_finalize(b); + // the threshold to free a core is 4 availability votes, but we only expect 1 valid + // valid bitfield. + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.clone(), signed], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); - System::on_initialize(b + 1); - System::set_block_number(b + 1); + assert_eq!( + >::get(chain_a) + .unwrap() + .availability_votes + .count_ones(), + 1 + ); - ParasShared::initializer_initialize(b + 1); - Paras::initializer_initialize(b + 1); - ParaInclusion::initializer_initialize(b + 1); + // clean up + PendingAvailability::::remove_all(None); } - } - pub(crate) fn expected_bits() -> usize { - Paras::parachains().len() + Configuration::config().parathread_cores as usize - } + // out of order. + { + set_pending_av.clone()(); + let back_core_0_bitfield = { + let mut b = default_bitfield(); + b.0.set(0, true); + b + }; + let signed_0 = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + back_core_0_bitfield.clone(), + &signing_context, + )) + .into(); - fn default_bitfield() -> AvailabilityBitfield { - AvailabilityBitfield(bitvec::bitvec![BitOrderLsb0, u8; 0; expected_bits()]) - } + let signed_1 = block_on(sign_bitfield( + &keystore, + &validators[1], + ValidatorIndex(1), + back_core_0_bitfield, + &signing_context, + )) + .into(); - fn default_availability_votes() -> BitVec { - bitvec::bitvec![BitOrderLsb0, u8; 0; ParasShared::active_validator_keys().len()] - } + assert_eq!( + >::get(chain_a) + .unwrap() + .availability_votes + .count_ones(), + 0 + ); - fn default_backing_bitfield() -> BitVec { - bitvec::bitvec![BitOrderLsb0, u8; 0; ParasShared::active_validator_keys().len()] - } + // the threshold to free a core is 4 availability votes, but we only expect 1 valid + // valid bitfield because `signed_0` will get skipped for being out of order. + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed_1, signed_0], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); + + assert_eq!( + >::get(chain_a) + .unwrap() + .availability_votes + .count_ones(), + 1 + ); - fn backing_bitfield(v: &[usize]) -> BitVec { - let mut b = default_backing_bitfield(); - for i in v { - b.set(*i, true); + PendingAvailability::::remove_all(None); } - b - } - pub(crate) fn validator_pubkeys(val_ids: &[Sr25519Keyring]) -> Vec { - val_ids.iter().map(|v| v.public().into()).collect() - } + // non-pending bit set. + { + let mut bare_bitfield = default_bitfield(); + *bare_bitfield.0.get_mut(0).unwrap() = true; + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); - pub(crate) async fn sign_bitfield( - keystore: &SyncCryptoStorePtr, - key: &Sr25519Keyring, - validator_index: ValidatorIndex, - bitfield: AvailabilityBitfield, - signing_context: &SigningContext, - ) -> SignedAvailabilityBitfield { - SignedAvailabilityBitfield::sign( - &keystore, - bitfield, - &signing_context, - validator_index, - &key.public().into(), - ) - .await - .unwrap() - .unwrap() - } + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); + } - #[derive(Default)] - pub(crate) struct TestCandidateBuilder { - pub(crate) para_id: ParaId, - pub(crate) head_data: HeadData, - pub(crate) para_head_hash: Option, - pub(crate) pov_hash: Hash, - pub(crate) relay_parent: Hash, - pub(crate) persisted_validation_data_hash: Hash, - pub(crate) new_validation_code: Option, - pub(crate) validation_code: ValidationCode, - pub(crate) hrmp_watermark: BlockNumber, - } + // empty bitfield signed: always ok, but kind of useless. + { + let bare_bitfield = default_bitfield(); + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); - impl TestCandidateBuilder { - pub(crate) fn build(self) -> CommittedCandidateReceipt { - CommittedCandidateReceipt { - descriptor: CandidateDescriptor { - para_id: self.para_id, - pov_hash: self.pov_hash, - relay_parent: self.relay_parent, - persisted_validation_data_hash: self.persisted_validation_data_hash, - validation_code_hash: self.validation_code.hash(), - para_head: self.para_head_hash.unwrap_or_else(|| self.head_data.hash()), - ..Default::default() - }, - commitments: CandidateCommitments { - head_data: self.head_data, - new_validation_code: self.new_validation_code, - hrmp_watermark: self.hrmp_watermark, - ..Default::default() - }, - } + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); } - } - pub(crate) fn make_vdata_hash(para_id: ParaId) -> Option { - let relay_parent_number = >::block_number() - 1; - let persisted_validation_data = crate::util::make_persisted_validation_data::( - para_id, - relay_parent_number, - Default::default(), - )?; - Some(persisted_validation_data.hash()) - } + // bitfield signed with pending bit signed. + { + let mut bare_bitfield = default_bitfield(); - #[test] - fn collect_pending_cleans_up_pending() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); + assert_eq!(core_lookup(CoreIndex::from(0)), Some(chain_a)); - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - new_test_ext(genesis_config(paras)).execute_with(|| { let default_candidate = TestCandidateBuilder::default().build(); >::insert( chain_a, CandidatePendingAvailability { core: CoreIndex::from(0), hash: default_candidate.hash(), - descriptor: default_candidate.descriptor.clone(), + descriptor: default_candidate.descriptor, availability_votes: default_availability_votes(), relay_parent_number: 0, backed_in_number: 0, @@ -289,1201 +593,622 @@ backing_group: GroupIndex::from(0), }, ); - PendingAvailabilityCommitments::::insert( - chain_a, - default_candidate.commitments.clone(), - ); + PendingAvailabilityCommitments::::insert(chain_a, default_candidate.commitments); + + *bare_bitfield.0.get_mut(0).unwrap() = true; + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); + + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); + + >::remove(chain_a); + PendingAvailabilityCommitments::::remove(chain_a); + } + + // bitfield signed with pending bit signed, but no commitments. + { + let mut bare_bitfield = default_bitfield(); + assert_eq!(core_lookup(CoreIndex::from(0)), Some(chain_a)); + + let default_candidate = TestCandidateBuilder::default().build(); >::insert( - &chain_b, + chain_a, CandidatePendingAvailability { - core: CoreIndex::from(1), + core: CoreIndex::from(0), hash: default_candidate.hash(), descriptor: default_candidate.descriptor, availability_votes: default_availability_votes(), relay_parent_number: 0, backed_in_number: 0, backers: default_backing_bitfield(), - backing_group: GroupIndex::from(1), + backing_group: GroupIndex::from(0), }, ); - PendingAvailabilityCommitments::::insert(chain_b, default_candidate.commitments); - run_to_block(5, |_| None); + *bare_bitfield.0.get_mut(0).unwrap() = true; + let signed = block_on(sign_bitfield( + &keystore, + &validators[0], + ValidatorIndex(0), + bare_bitfield, + &signing_context, + )); - assert!(>::get(&chain_a).is_some()); - assert!(>::get(&chain_b).is_some()); - assert!(>::get(&chain_a).is_some()); - assert!(>::get(&chain_b).is_some()); + // no core is freed + assert!(ParaInclusion::process_bitfields( + expected_bits(), + vec![signed.into()], + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0 + .is_empty()); + } + }); +} + +#[test] +fn supermajority_bitfields_trigger_availability() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); - ParaInclusion::collect_pending(|core, _since| core == CoreIndex::from(0)); + new_test_ext(genesis_config(paras)).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_b).is_some()); - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_b).is_some()); - }); - } + let signing_context = + SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - #[test] - fn bitfield_checks() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); - - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); - } - let validator_public = validator_pubkeys(&validators); + let core_lookup = |core| match core { + core if core == CoreIndex::from(0) => Some(chain_a), + core if core == CoreIndex::from(1) => Some(chain_b), + core if core == CoreIndex::from(2) => Some(thread_a), + _ => panic!("Core out of bounds for 2 parachains and 1 parathread core."), + }; - new_test_ext(genesis_config(paras.clone())).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); + let candidate_a = TestCandidateBuilder { + para_id: chain_a, + head_data: vec![1, 2, 3, 4].into(), + ..Default::default() + } + .build(); - let signing_context = - SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; + >::insert( + chain_a, + CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: candidate_a.hash(), + descriptor: candidate_a.clone().descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 0, + backed_in_number: 0, + backers: backing_bitfield(&[3, 4]), + backing_group: GroupIndex::from(0), + }, + ); + PendingAvailabilityCommitments::::insert(chain_a, candidate_a.clone().commitments); - let core_lookup = |core| match core { - core if core == CoreIndex::from(0) => Some(chain_a), - core if core == CoreIndex::from(1) => Some(chain_b), - core if core == CoreIndex::from(2) => Some(thread_a), - core if core == CoreIndex::from(3) => None, // for the expected_cores() + 1 test below. - _ => panic!("out of bounds for testing"), - }; + let candidate_b = TestCandidateBuilder { + para_id: chain_b, + head_data: vec![5, 6, 7, 8].into(), + ..Default::default() + } + .build(); - // mark all candidates as pending availability - let set_pending_av = || { - for (p_id, _) in paras { - PendingAvailability::::insert( - p_id, - CandidatePendingAvailability { - availability_votes: default_availability_votes(), - ..Default::default() - }, - ) - } - }; + >::insert( + chain_b, + CandidatePendingAvailability { + core: CoreIndex::from(1), + hash: candidate_b.hash(), + descriptor: candidate_b.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 0, + backed_in_number: 0, + backers: backing_bitfield(&[0, 2]), + backing_group: GroupIndex::from(1), + }, + ); + PendingAvailabilityCommitments::::insert(chain_b, candidate_b.commitments); - // too many bits in bitfield - { - let mut bare_bitfield = default_bitfield(); - bare_bitfield.0.push(false); - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); + // this bitfield signals that a and b are available. + let a_and_b_available = { + let mut bare_bitfield = default_bitfield(); + *bare_bitfield.0.get_mut(0).unwrap() = true; + *bare_bitfield.0.get_mut(1).unwrap() = true; - assert_eq!( - ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0, - vec![] - ); - } + bare_bitfield + }; - // not enough bits - { - let bare_bitfield = default_bitfield(); - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); + // this bitfield signals that only a is available. + let a_available = { + let mut bare_bitfield = default_bitfield(); + *bare_bitfield.0.get_mut(0).unwrap() = true; - assert_eq!( - ParaInclusion::process_bitfields( - expected_bits() + 1, - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0, - vec![] - ); - } + bare_bitfield + }; - // duplicate. - { - set_pending_av.clone()(); - let back_core_0_bitfield = { - let mut b = default_bitfield(); - b.0.set(0, true); - b + let threshold = availability_threshold(validators.len()); + + // 4 of 5 first value >= 2/3 + assert_eq!(threshold, 4); + + let signed_bitfields = validators + .iter() + .enumerate() + .filter_map(|(i, key)| { + let to_sign = if i < 3 { + a_and_b_available.clone() + } else if i < 4 { + a_available.clone() + } else { + // sign nothing. + return None }; - let signed: UncheckedSignedAvailabilityBitfield = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - back_core_0_bitfield, - &signing_context, - )) - .into(); - assert_eq!( - >::get(chain_a) - .unwrap() - .availability_votes - .count_ones(), - 0 - ); - - // the threshold to free a core is 4 availability votes, but we only expect 1 valid - // valid bitfield. - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.clone(), signed], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, + Some( + block_on(sign_bitfield( + &keystore, + key, + ValidatorIndex(i as _), + to_sign, + &signing_context, + )) + .into(), ) - .0 - .is_empty()); + }) + .collect(); + + // only chain A's core is freed. + assert_eq!( + ParaInclusion::process_bitfields( + expected_bits(), + signed_bitfields, + DisputedBitfield::zeros(expected_bits()), + &core_lookup, + ) + .0, + vec![(CoreIndex(0), candidate_a.hash())] + ); - assert_eq!( - >::get(chain_a) - .unwrap() - .availability_votes - .count_ones(), - 1 - ); + // chain A had 4 signing off, which is >= threshold. + // chain B has 3 signing off, which is < threshold. + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_b).is_some()); + assert_eq!(>::get(&chain_b).unwrap().availability_votes, { + // check that votes from first 3 were tracked. - // clean up - PendingAvailability::::remove_all(None); - } + let mut votes = default_availability_votes(); + *votes.get_mut(0).unwrap() = true; + *votes.get_mut(1).unwrap() = true; + *votes.get_mut(2).unwrap() = true; - // out of order. - { - set_pending_av.clone()(); - let back_core_0_bitfield = { - let mut b = default_bitfield(); - b.0.set(0, true); - b - }; - let signed_0 = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - back_core_0_bitfield.clone(), - &signing_context, - )) - .into(); - - let signed_1 = block_on(sign_bitfield( - &keystore, - &validators[1], - ValidatorIndex(1), - back_core_0_bitfield, - &signing_context, - )) - .into(); + votes + }); - assert_eq!( - >::get(chain_a) - .unwrap() - .availability_votes - .count_ones(), - 0 - ); + // and check that chain head was enacted. + assert_eq!(Paras::para_head(&chain_a), Some(vec![1, 2, 3, 4].into())); - // the threshold to free a core is 4 availability votes, but we only expect 1 valid - // valid bitfield because `signed_0` will get skipped for being out of order. - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed_1, signed_0], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0 - .is_empty()); + // Check that rewards are applied. + { + let rewards = crate::mock::availability_rewards(); - assert_eq!( - >::get(chain_a) - .unwrap() - .availability_votes - .count_ones(), - 1 - ); + assert_eq!(rewards.len(), 4); + assert_eq!(rewards.get(&ValidatorIndex(0)).unwrap(), &1); + assert_eq!(rewards.get(&ValidatorIndex(1)).unwrap(), &1); + assert_eq!(rewards.get(&ValidatorIndex(2)).unwrap(), &1); + assert_eq!(rewards.get(&ValidatorIndex(3)).unwrap(), &1); + } - PendingAvailability::::remove_all(None); - } + { + let rewards = crate::mock::backing_rewards(); - // non-pending bit set. - { - let mut bare_bitfield = default_bitfield(); - *bare_bitfield.0.get_mut(0).unwrap() = true; - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); - - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0 - .is_empty()); - } + assert_eq!(rewards.len(), 2); + assert_eq!(rewards.get(&ValidatorIndex(3)).unwrap(), &1); + assert_eq!(rewards.get(&ValidatorIndex(4)).unwrap(), &1); + } + }); +} + +#[test] +fn candidate_checks() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); - // empty bitfield signed: always ok, but kind of useless. - { - let bare_bitfield = default_bitfield(); - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); - - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0 - .is_empty()); - } + new_test_ext(genesis_config(paras)).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); - // bitfield signed with pending bit signed. - { - let mut bare_bitfield = default_bitfield(); + run_to_block(5, |_| None); - assert_eq!(core_lookup(CoreIndex::from(0)), Some(chain_a)); + let signing_context = + SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - let default_candidate = TestCandidateBuilder::default().build(); - >::insert( - chain_a, - CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: default_candidate.hash(), - descriptor: default_candidate.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 0, - backed_in_number: 0, - backers: default_backing_bitfield(), - backing_group: GroupIndex::from(0), - }, - ); - PendingAvailabilityCommitments::::insert( - chain_a, - default_candidate.commitments, - ); + let group_validators = |group_index: GroupIndex| { + match group_index { + group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), + group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), + group_index if group_index == GroupIndex::from(2) => Some(vec![4]), + _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), + } + .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) + }; - *bare_bitfield.0.get_mut(0).unwrap() = true; - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); + let thread_collator: CollatorId = Sr25519Keyring::Two.public().into(); - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0 - .is_empty()); + let chain_a_assignment = CoreAssignment { + core: CoreIndex::from(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex::from(0), + }; - >::remove(chain_a); - PendingAvailabilityCommitments::::remove(chain_a); - } + let chain_b_assignment = CoreAssignment { + core: CoreIndex::from(1), + para_id: chain_b, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex::from(1), + }; - // bitfield signed with pending bit signed, but no commitments. - { - let mut bare_bitfield = default_bitfield(); + let thread_a_assignment = CoreAssignment { + core: CoreIndex::from(2), + para_id: thread_a, + kind: AssignmentKind::Parathread(thread_collator.clone(), 0), + group_idx: GroupIndex::from(2), + }; - assert_eq!(core_lookup(CoreIndex::from(0)), Some(chain_a)); + // unscheduled candidate. + { + let mut candidate = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - let default_candidate = TestCandidateBuilder::default().build(); - >::insert( - chain_a, - CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: default_candidate.hash(), - descriptor: default_candidate.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 0, - backed_in_number: 0, - backers: default_backing_bitfield(), - backing_group: GroupIndex::from(0), - }, - ); - - *bare_bitfield.0.get_mut(0).unwrap() = true; - let signed = block_on(sign_bitfield( - &keystore, - &validators[0], - ValidatorIndex(0), - bare_bitfield, - &signing_context, - )); - - // no core is freed - assert!(ParaInclusion::process_bitfields( - expected_bits(), - vec![signed.into()], - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0 - .is_empty()); - } - }); - } + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - #[test] - fn supermajority_bitfields_trigger_availability() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); - - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_b_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::UnscheduledCandidate + ); } - let validator_public = validator_pubkeys(&validators); - new_test_ext(genesis_config(paras)).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); - - let signing_context = - SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - - let core_lookup = |core| match core { - core if core == CoreIndex::from(0) => Some(chain_a), - core if core == CoreIndex::from(1) => Some(chain_b), - core if core == CoreIndex::from(2) => Some(thread_a), - _ => panic!("Core out of bounds for 2 parachains and 1 parathread core."), - }; - - let candidate_a = TestCandidateBuilder { + // candidates out of order. + { + let mut candidate_a = TestCandidateBuilder { para_id: chain_a, - head_data: vec![1, 2, 3, 4].into(), + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() } .build(); - - >::insert( - chain_a, - CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: candidate_a.hash(), - descriptor: candidate_a.clone().descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 0, - backed_in_number: 0, - backers: backing_bitfield(&[3, 4]), - backing_group: GroupIndex::from(0), - }, - ); - PendingAvailabilityCommitments::::insert( - chain_a, - candidate_a.clone().commitments, - ); - - let candidate_b = TestCandidateBuilder { + let mut candidate_b = TestCandidateBuilder { para_id: chain_b, - head_data: vec![5, 6, 7, 8].into(), + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(2), + persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() } .build(); - >::insert( - chain_b, - CandidatePendingAvailability { - core: CoreIndex::from(1), - hash: candidate_b.hash(), - descriptor: candidate_b.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 0, - backed_in_number: 0, - backers: backing_bitfield(&[0, 2]), - backing_group: GroupIndex::from(1), - }, - ); - PendingAvailabilityCommitments::::insert(chain_b, candidate_b.commitments); - - // this bitfield signals that a and b are available. - let a_and_b_available = { - let mut bare_bitfield = default_bitfield(); - *bare_bitfield.0.get_mut(0).unwrap() = true; - *bare_bitfield.0.get_mut(1).unwrap() = true; - - bare_bitfield - }; - - // this bitfield signals that only a is available. - let a_available = { - let mut bare_bitfield = default_bitfield(); - *bare_bitfield.0.get_mut(0).unwrap() = true; - - bare_bitfield - }; + collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); - let threshold = availability_threshold(validators.len()); + collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b); - // 4 of 5 first value >= 2/3 - assert_eq!(threshold, 4); + let backed_a = block_on(back_candidate( + candidate_a, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - let signed_bitfields = validators - .iter() - .enumerate() - .filter_map(|(i, key)| { - let to_sign = if i < 3 { - a_and_b_available.clone() - } else if i < 4 { - a_available.clone() - } else { - // sign nothing. - return None - }; - - Some( - block_on(sign_bitfield( - &keystore, - key, - ValidatorIndex(i as _), - to_sign, - &signing_context, - )) - .into(), - ) - }) - .collect(); + let backed_b = block_on(back_candidate( + candidate_b, + &validators, + group_validators(GroupIndex::from(1)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - // only chain A's core is freed. - assert_eq!( - ParaInclusion::process_bitfields( - expected_bits(), - signed_bitfields, - DisputedBitfield::zeros(expected_bits()), - &core_lookup, - ) - .0, - vec![(CoreIndex(0), candidate_a.hash())] + // out-of-order manifests as unscheduled. + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed_b, backed_a], + vec![chain_a_assignment.clone(), chain_b_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::UnscheduledCandidate ); - - // chain A had 4 signing off, which is >= threshold. - // chain B has 3 signing off, which is < threshold. - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_b).is_some()); - assert_eq!(>::get(&chain_b).unwrap().availability_votes, { - // check that votes from first 3 were tracked. - - let mut votes = default_availability_votes(); - *votes.get_mut(0).unwrap() = true; - *votes.get_mut(1).unwrap() = true; - *votes.get_mut(2).unwrap() = true; - - votes - }); - - // and check that chain head was enacted. - assert_eq!(Paras::para_head(&chain_a), Some(vec![1, 2, 3, 4].into())); - - // Check that rewards are applied. - { - let rewards = crate::mock::availability_rewards(); - - assert_eq!(rewards.len(), 4); - assert_eq!(rewards.get(&ValidatorIndex(0)).unwrap(), &1); - assert_eq!(rewards.get(&ValidatorIndex(1)).unwrap(), &1); - assert_eq!(rewards.get(&ValidatorIndex(2)).unwrap(), &1); - assert_eq!(rewards.get(&ValidatorIndex(3)).unwrap(), &1); - } - - { - let rewards = crate::mock::backing_rewards(); - - assert_eq!(rewards.len(), 2); - assert_eq!(rewards.get(&ValidatorIndex(3)).unwrap(), &1); - assert_eq!(rewards.get(&ValidatorIndex(4)).unwrap(), &1); - } - }); - } - - #[test] - fn candidate_checks() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); - - // The block number of the relay-parent for testing. - const RELAY_PARENT_NUM: BlockNumber = 4; - - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); } - let validator_public = validator_pubkeys(&validators); - - new_test_ext(genesis_config(paras)).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); - - run_to_block(5, |_| None); - - let signing_context = - SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - - let group_validators = |group_index: GroupIndex| { - match group_index { - group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), - group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), - group_index if group_index == GroupIndex::from(2) => Some(vec![4]), - _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), - } - .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) - }; - - let thread_collator: CollatorId = Sr25519Keyring::Two.public().into(); - let chain_a_assignment = CoreAssignment { - core: CoreIndex::from(0), + // candidate not backed. + { + let mut candidate = TestCandidateBuilder { para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex::from(0), - }; - - let chain_b_assignment = CoreAssignment { - core: CoreIndex::from(1), - para_id: chain_b, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex::from(1), - }; - - let thread_a_assignment = CoreAssignment { - core: CoreIndex::from(2), - para_id: thread_a, - kind: AssignmentKind::Parathread(thread_collator.clone(), 0), - group_idx: GroupIndex::from(2), - }; - - // unscheduled candidate. - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_b_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::UnscheduledCandidate - ); + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() } + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - // candidates out of order. - { - let mut candidate_a = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - let mut candidate_b = TestCandidateBuilder { - para_id: chain_b, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(2), - persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); - - collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b); - - let backed_a = block_on(back_candidate( - candidate_a, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - let backed_b = block_on(back_candidate( - candidate_b, - &validators, - group_validators(GroupIndex::from(1)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - // out-of-order manifests as unscheduled. - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed_b, backed_a], - vec![chain_a_assignment.clone(), chain_b_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::UnscheduledCandidate - ); - } + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Lacking, + )); - // candidate not backed. - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Lacking, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::InsufficientBacking - ); - } + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::InsufficientBacking + ); + } - // candidate not in parent context. - { - let wrong_parent_hash = Hash::repeat_byte(222); - assert!(System::parent_hash() != wrong_parent_hash); - - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: wrong_parent_hash, - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - ..Default::default() - } - .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::CandidateNotInParentContext - ); - } + // candidate not in parent context. + { + let wrong_parent_hash = Hash::repeat_byte(222); + assert!(System::parent_hash() != wrong_parent_hash); - // candidate has wrong collator. - { - let mut candidate = TestCandidateBuilder { - para_id: thread_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - assert!(CollatorId::from(Sr25519Keyring::One.public()) != thread_collator); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(2)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![ - chain_a_assignment.clone(), - chain_b_assignment.clone(), - thread_a_assignment.clone(), - ], - &group_validators, - FullCheck::Yes, - ), - Error::::WrongCollator, - ); + let mut candidate = TestCandidateBuilder { + para_id: chain_a, + relay_parent: wrong_parent_hash, + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + ..Default::default() } + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - // candidate not well-signed by collator. - { - let mut candidate = TestCandidateBuilder { - para_id: thread_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - assert_eq!(CollatorId::from(Sr25519Keyring::Two.public()), thread_collator); - collator_sign_candidate(Sr25519Keyring::Two, &mut candidate); - - // change the candidate after signing. - candidate.descriptor.pov_hash = Hash::repeat_byte(2); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(2)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![thread_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::NotCollatorSigned - ); - } + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - // para occupied - reject. - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - let candidate = TestCandidateBuilder::default().build(); - >::insert( - &chain_a, - CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: candidate.hash(), - descriptor: candidate.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 3, - backed_in_number: 4, - backers: default_backing_bitfield(), - backing_group: GroupIndex::from(0), - }, - ); - >::insert(&chain_a, candidate.commitments); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::CandidateScheduledBeforeParaFree - ); + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::CandidateNotInParentContext + ); + } - >::remove(&chain_a); - >::remove(&chain_a); + // candidate has wrong collator. + { + let mut candidate = TestCandidateBuilder { + para_id: thread_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() } + .build(); - // messed up commitments storage - do not panic - reject. - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - // this is not supposed to happen - >::insert( - &chain_a, - candidate.commitments.clone(), - ); + assert!(CollatorId::from(Sr25519Keyring::One.public()) != thread_collator); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::CandidateScheduledBeforeParaFree - ); + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(2)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - >::remove(&chain_a); - } + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![ + chain_a_assignment.clone(), + chain_b_assignment.clone(), + thread_a_assignment.clone(), + ], + &group_validators, + FullCheck::Yes, + ), + Error::::WrongCollator, + ); + } - // interfering code upgrade - reject - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - new_validation_code: Some(vec![5, 6, 7, 8].into()), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - { - let cfg = Configuration::config(); - let expected_at = 10 + cfg.validation_upgrade_delay; - assert_eq!(expected_at, 10); - Paras::schedule_code_upgrade( - chain_a, - vec![1, 2, 3, 4].into(), - expected_at, - &cfg, - ); - - assert_eq!(Paras::last_code_upgrade(chain_a, true), Some(expected_at)); - } - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::PrematureCodeUpgrade - ); + // candidate not well-signed by collator. + { + let mut candidate = TestCandidateBuilder { + para_id: thread_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() } + .build(); - // Bad validation data hash - reject - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: [42u8; 32].into(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); + assert_eq!(CollatorId::from(Sr25519Keyring::Two.public()), thread_collator); + collator_sign_candidate(Sr25519Keyring::Two, &mut candidate); - assert_eq!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Err(Error::::ValidationDataHashMismatch.into()), - ); - } + // change the candidate after signing. + candidate.descriptor.pov_hash = Hash::repeat_byte(2); - // bad validation code hash - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - validation_code: ValidationCode(vec![1]), - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::InvalidValidationCodeHash - ); - } - - // Para head hash in descriptor doesn't match head data - { - let mut candidate = TestCandidateBuilder { - para_id: chain_a, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(1), - persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - para_head_hash: Some(Hash::random()), - ..Default::default() - } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(0)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - - assert_noop!( - ParaInclusion::process_candidates( - Default::default(), - vec![backed], - vec![chain_a_assignment.clone()], - &group_validators, - FullCheck::Yes, - ), - Error::::ParaHeadMismatch - ); - } - }); - } + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(2)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - #[test] - fn backing_works() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); - - // The block number of the relay-parent for testing. - const RELAY_PARENT_NUM: BlockNumber = 4; - - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![thread_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::NotCollatorSigned + ); } - let validator_public = validator_pubkeys(&validators); - new_test_ext(genesis_config(paras)).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); - - run_to_block(5, |_| None); + // para occupied - reject. + { + let mut candidate = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); - let signing_context = - SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - let group_validators = |group_index: GroupIndex| { - match group_index { - group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), - group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), - group_index if group_index == GroupIndex::from(2) => Some(vec![4]), - _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), - } - .map(|vs| vs.into_iter().map(ValidatorIndex).collect::>()) - }; - - let thread_collator: CollatorId = Sr25519Keyring::Two.public().into(); + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - let chain_a_assignment = CoreAssignment { - core: CoreIndex::from(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex::from(0), - }; + let candidate = TestCandidateBuilder::default().build(); + >::insert( + &chain_a, + CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: candidate.hash(), + descriptor: candidate.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 3, + backed_in_number: 4, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(0), + }, + ); + >::insert(&chain_a, candidate.commitments); - let chain_b_assignment = CoreAssignment { - core: CoreIndex::from(1), - para_id: chain_b, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex::from(1), - }; + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::CandidateScheduledBeforeParaFree + ); - let thread_a_assignment = CoreAssignment { - core: CoreIndex::from(2), - para_id: thread_a, - kind: AssignmentKind::Parathread(thread_collator.clone(), 0), - group_idx: GroupIndex::from(2), - }; + >::remove(&chain_a); + >::remove(&chain_a); + } - let mut candidate_a = TestCandidateBuilder { + // messed up commitments storage - do not panic - reject. + { + let mut candidate = TestCandidateBuilder { para_id: chain_a, relay_parent: System::parent_hash(), pov_hash: Hash::repeat_byte(1), @@ -1492,32 +1217,52 @@ ..Default::default() } .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); - let mut candidate_b = TestCandidateBuilder { - para_id: chain_b, - relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(2), - persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() - } - .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); + + // this is not supposed to happen + >::insert(&chain_a, candidate.commitments.clone()); + + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::CandidateScheduledBeforeParaFree + ); - let mut candidate_c = TestCandidateBuilder { - para_id: thread_a, + >::remove(&chain_a); + } + + // interfering code upgrade - reject + { + let mut candidate = TestCandidateBuilder { + para_id: chain_a, relay_parent: System::parent_hash(), - pov_hash: Hash::repeat_byte(3), - persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + pov_hash: Hash::repeat_byte(1), + new_validation_code: Some(vec![5, 6, 7, 8].into()), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), hrmp_watermark: RELAY_PARENT_NUM, ..Default::default() } .build(); - collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_c); - let backed_a = block_on(back_candidate( - candidate_a.clone(), + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); + + let backed = block_on(back_candidate( + candidate, &validators, group_validators(GroupIndex::from(0)).unwrap().as_ref(), &keystore, @@ -1525,234 +1270,115 @@ BackingKind::Threshold, )); - let backed_b = block_on(back_candidate( - candidate_b.clone(), - &validators, - group_validators(GroupIndex::from(1)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); + { + let cfg = Configuration::config(); + let expected_at = 10 + cfg.validation_upgrade_delay; + assert_eq!(expected_at, 10); + Paras::schedule_code_upgrade(chain_a, vec![1, 2, 3, 4].into(), expected_at, &cfg); + + assert_eq!(Paras::last_code_upgrade(chain_a, true), Some(expected_at)); + } + + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::PrematureCodeUpgrade + ); + } + + // Bad validation data hash - reject + { + let mut candidate = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: [42u8; 32].into(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - let backed_c = block_on(back_candidate( - candidate_c.clone(), + let backed = block_on(back_candidate( + candidate, &validators, - group_validators(GroupIndex::from(2)).unwrap().as_ref(), + group_validators(GroupIndex::from(0)).unwrap().as_ref(), &keystore, &signing_context, BackingKind::Threshold, )); - let backed_candidates = vec![backed_a, backed_b, backed_c]; - let get_backing_group_idx = { - // the order defines the group implicitly for this test case - let backed_candidates_with_groups = backed_candidates - .iter() - .enumerate() - .map(|(idx, backed_candidate)| (backed_candidate.hash(), GroupIndex(idx as _))) - .collect::>(); - - move |candidate_hash_x: CandidateHash| -> Option { - backed_candidates_with_groups.iter().find_map(|(candidate_hash, grp)| { - if *candidate_hash == candidate_hash_x { - Some(*grp) - } else { - None - } - }) - } - }; - - let ProcessedCandidates { - core_indices: occupied_cores, - candidate_receipt_with_backing_validator_indices, - } = ParaInclusion::process_candidates( - Default::default(), - backed_candidates.clone(), - vec![ - chain_a_assignment.clone(), - chain_b_assignment.clone(), - thread_a_assignment.clone(), - ], - &group_validators, - FullCheck::Yes, - ) - .expect("candidates scheduled, in order, and backed"); - assert_eq!( - occupied_cores, - vec![CoreIndex::from(0), CoreIndex::from(1), CoreIndex::from(2)] + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Err(Error::::ValidationDataHashMismatch.into()), ); + } - // Transform the votes into the setup we expect - let expected = { - let mut intermediate = std::collections::HashMap::< - CandidateHash, - (CandidateReceipt, Vec<(ValidatorIndex, ValidityAttestation)>), - >::new(); - backed_candidates.into_iter().for_each(|backed_candidate| { - let candidate_receipt_with_backers = intermediate - .entry(backed_candidate.hash()) - .or_insert_with(|| (backed_candidate.receipt(), Vec::new())); - - assert_eq!( - backed_candidate.validity_votes.len(), - backed_candidate.validator_indices.count_ones() - ); - candidate_receipt_with_backers.1.extend( - backed_candidate - .validator_indices - .iter() - .enumerate() - .filter(|(_, signed)| **signed) - .zip(backed_candidate.validity_votes.iter().cloned()) - .filter_map(|((validator_index_within_group, _), attestation)| { - let grp_idx = - get_backing_group_idx(backed_candidate.hash()).unwrap(); - group_validators(grp_idx).map(|validator_indices| { - (validator_indices[validator_index_within_group], attestation) - }) - }), - ); - }); - intermediate.into_values().collect::>() - }; - - // sort, since we use a hashmap above - let assure_candidate_sorting = |mut candidate_receipts_with_backers: Vec<( - CandidateReceipt, - Vec<(ValidatorIndex, ValidityAttestation)>, - )>| { - candidate_receipts_with_backers.sort_by(|(cr1, _), (cr2, _)| { - cr1.descriptor().para_id.cmp(&cr2.descriptor().para_id) - }); - candidate_receipts_with_backers - }; - assert_eq!( - assure_candidate_sorting(expected), - assure_candidate_sorting(candidate_receipt_with_backing_validator_indices) - ); + // bad validation code hash + { + let mut candidate = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + validation_code: ValidationCode(vec![1]), + ..Default::default() + } + .build(); - assert_eq!( - >::get(&chain_a), - Some(CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: candidate_a.hash(), - descriptor: candidate_a.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: System::block_number() - 1, - backed_in_number: System::block_number(), - backers: backing_bitfield(&[0, 1]), - backing_group: GroupIndex::from(0), - }) - ); - assert_eq!( - >::get(&chain_a), - Some(candidate_a.commitments), - ); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - assert_eq!( - >::get(&chain_b), - Some(CandidatePendingAvailability { - core: CoreIndex::from(1), - hash: candidate_b.hash(), - descriptor: candidate_b.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: System::block_number() - 1, - backed_in_number: System::block_number(), - backers: backing_bitfield(&[2, 3]), - backing_group: GroupIndex::from(1), - }) - ); - assert_eq!( - >::get(&chain_b), - Some(candidate_b.commitments), - ); + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - assert_eq!( - >::get(&thread_a), - Some(CandidatePendingAvailability { - core: CoreIndex::from(2), - hash: candidate_c.hash(), - descriptor: candidate_c.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: System::block_number() - 1, - backed_in_number: System::block_number(), - backers: backing_bitfield(&[4]), - backing_group: GroupIndex::from(2), - }) - ); - assert_eq!( - >::get(&thread_a), - Some(candidate_c.commitments), + assert_noop!( + ParaInclusion::process_candidates( + Default::default(), + vec![backed], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ), + Error::::InvalidValidationCodeHash ); - }); - } - - #[test] - fn can_include_candidate_with_ok_code_upgrade() { - let chain_a = ParaId::from(1); - - // The block number of the relay-parent for testing. - const RELAY_PARENT_NUM: BlockNumber = 4; - - let paras = vec![(chain_a, true)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); } - let validator_public = validator_pubkeys(&validators); - - new_test_ext(genesis_config(paras)).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); - - run_to_block(5, |_| None); - - let signing_context = - SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; - - let group_validators = |group_index: GroupIndex| { - match group_index { - group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1, 2, 3, 4]), - _ => panic!("Group index out of bounds for 1 parachain"), - } - .map(|vs| vs.into_iter().map(ValidatorIndex).collect::>()) - }; - - let chain_a_assignment = CoreAssignment { - core: CoreIndex::from(0), - para_id: chain_a, - kind: AssignmentKind::Parachain, - group_idx: GroupIndex::from(0), - }; - let mut candidate_a = TestCandidateBuilder { + // Para head hash in descriptor doesn't match head data + { + let mut candidate = TestCandidateBuilder { para_id: chain_a, relay_parent: System::parent_hash(), pov_hash: Hash::repeat_byte(1), persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), - new_validation_code: Some(vec![1, 2, 3].into()), hrmp_watermark: RELAY_PARENT_NUM, + para_head_hash: Some(Hash::random()), ..Default::default() } .build(); - collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); - let backed_a = block_on(back_candidate( - candidate_a.clone(), + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); + + let backed = block_on(back_candidate( + candidate, &validators, group_validators(GroupIndex::from(0)).unwrap().as_ref(), &keystore, @@ -1760,160 +1386,520 @@ BackingKind::Threshold, )); - let ProcessedCandidates { core_indices: occupied_cores, .. } = + assert_noop!( ParaInclusion::process_candidates( Default::default(), - vec![backed_a], + vec![backed], vec![chain_a_assignment.clone()], &group_validators, FullCheck::Yes, - ) - .expect("candidates scheduled, in order, and backed"); + ), + Error::::ParaHeadMismatch + ); + } + }); +} + +#[test] +fn backing_works() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); - assert_eq!(occupied_cores, vec![CoreIndex::from(0)]); + new_test_ext(genesis_config(paras)).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); - assert_eq!( - >::get(&chain_a), - Some(CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: candidate_a.hash(), - descriptor: candidate_a.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: System::block_number() - 1, - backed_in_number: System::block_number(), - backers: backing_bitfield(&[0, 1, 2]), - backing_group: GroupIndex::from(0), + run_to_block(5, |_| None); + + let signing_context = + SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; + + let group_validators = |group_index: GroupIndex| { + match group_index { + group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), + group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), + group_index if group_index == GroupIndex::from(2) => Some(vec![4]), + _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), + } + .map(|vs| vs.into_iter().map(ValidatorIndex).collect::>()) + }; + + let thread_collator: CollatorId = Sr25519Keyring::Two.public().into(); + + let chain_a_assignment = CoreAssignment { + core: CoreIndex::from(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex::from(0), + }; + + let chain_b_assignment = CoreAssignment { + core: CoreIndex::from(1), + para_id: chain_b, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex::from(1), + }; + + let thread_a_assignment = CoreAssignment { + core: CoreIndex::from(2), + para_id: thread_a, + kind: AssignmentKind::Parathread(thread_collator.clone(), 0), + group_idx: GroupIndex::from(2), + }; + + let mut candidate_a = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); + + let mut candidate_b = TestCandidateBuilder { + para_id: chain_b, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(2), + persisted_validation_data_hash: make_vdata_hash(chain_b).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b); + + let mut candidate_c = TestCandidateBuilder { + para_id: thread_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(3), + persisted_validation_data_hash: make_vdata_hash(thread_a).unwrap(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() + } + .build(); + collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_c); + + let backed_a = block_on(back_candidate( + candidate_a.clone(), + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + + let backed_b = block_on(back_candidate( + candidate_b.clone(), + &validators, + group_validators(GroupIndex::from(1)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + + let backed_c = block_on(back_candidate( + candidate_c.clone(), + &validators, + group_validators(GroupIndex::from(2)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + + let backed_candidates = vec![backed_a, backed_b, backed_c]; + let get_backing_group_idx = { + // the order defines the group implicitly for this test case + let backed_candidates_with_groups = backed_candidates + .iter() + .enumerate() + .map(|(idx, backed_candidate)| (backed_candidate.hash(), GroupIndex(idx as _))) + .collect::>(); + + move |candidate_hash_x: CandidateHash| -> Option { + backed_candidates_with_groups.iter().find_map(|(candidate_hash, grp)| { + if *candidate_hash == candidate_hash_x { + Some(*grp) + } else { + None + } }) - ); - assert_eq!( - >::get(&chain_a), - Some(candidate_a.commitments), - ); - }); + } + }; + + let ProcessedCandidates { + core_indices: occupied_cores, + candidate_receipt_with_backing_validator_indices, + } = ParaInclusion::process_candidates( + Default::default(), + backed_candidates.clone(), + vec![ + chain_a_assignment.clone(), + chain_b_assignment.clone(), + thread_a_assignment.clone(), + ], + &group_validators, + FullCheck::Yes, + ) + .expect("candidates scheduled, in order, and backed"); + + assert_eq!( + occupied_cores, + vec![CoreIndex::from(0), CoreIndex::from(1), CoreIndex::from(2)] + ); + + // Transform the votes into the setup we expect + let expected = { + let mut intermediate = std::collections::HashMap::< + CandidateHash, + (CandidateReceipt, Vec<(ValidatorIndex, ValidityAttestation)>), + >::new(); + backed_candidates.into_iter().for_each(|backed_candidate| { + let candidate_receipt_with_backers = intermediate + .entry(backed_candidate.hash()) + .or_insert_with(|| (backed_candidate.receipt(), Vec::new())); + + assert_eq!( + backed_candidate.validity_votes.len(), + backed_candidate.validator_indices.count_ones() + ); + candidate_receipt_with_backers.1.extend( + backed_candidate + .validator_indices + .iter() + .enumerate() + .filter(|(_, signed)| **signed) + .zip(backed_candidate.validity_votes.iter().cloned()) + .filter_map(|((validator_index_within_group, _), attestation)| { + let grp_idx = get_backing_group_idx(backed_candidate.hash()).unwrap(); + group_validators(grp_idx).map(|validator_indices| { + (validator_indices[validator_index_within_group], attestation) + }) + }), + ); + }); + intermediate.into_values().collect::>() + }; + + // sort, since we use a hashmap above + let assure_candidate_sorting = |mut candidate_receipts_with_backers: Vec<( + CandidateReceipt, + Vec<(ValidatorIndex, ValidityAttestation)>, + )>| { + candidate_receipts_with_backers.sort_by(|(cr1, _), (cr2, _)| { + cr1.descriptor().para_id.cmp(&cr2.descriptor().para_id) + }); + candidate_receipts_with_backers + }; + assert_eq!( + assure_candidate_sorting(expected), + assure_candidate_sorting(candidate_receipt_with_backing_validator_indices) + ); + + assert_eq!( + >::get(&chain_a), + Some(CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: candidate_a.hash(), + descriptor: candidate_a.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: System::block_number() - 1, + backed_in_number: System::block_number(), + backers: backing_bitfield(&[0, 1]), + backing_group: GroupIndex::from(0), + }) + ); + assert_eq!( + >::get(&chain_a), + Some(candidate_a.commitments), + ); + + assert_eq!( + >::get(&chain_b), + Some(CandidatePendingAvailability { + core: CoreIndex::from(1), + hash: candidate_b.hash(), + descriptor: candidate_b.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: System::block_number() - 1, + backed_in_number: System::block_number(), + backers: backing_bitfield(&[2, 3]), + backing_group: GroupIndex::from(1), + }) + ); + assert_eq!( + >::get(&chain_b), + Some(candidate_b.commitments), + ); + + assert_eq!( + >::get(&thread_a), + Some(CandidatePendingAvailability { + core: CoreIndex::from(2), + hash: candidate_c.hash(), + descriptor: candidate_c.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: System::block_number() - 1, + backed_in_number: System::block_number(), + backers: backing_bitfield(&[4]), + backing_group: GroupIndex::from(2), + }) + ); + assert_eq!( + >::get(&thread_a), + Some(candidate_c.commitments), + ); + }); +} + +#[test] +fn can_include_candidate_with_ok_code_upgrade() { + let chain_a = ParaId::from(1); + + // The block number of the relay-parent for testing. + const RELAY_PARENT_NUM: BlockNumber = 4; + + let paras = vec![(chain_a, true)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); } + let validator_public = validator_pubkeys(&validators); - #[test] - fn session_change_wipes() { - let chain_a = ParaId::from(1); - let chain_b = ParaId::from(2); - let thread_a = ParaId::from(3); - - let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; - let validators = vec![ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Ferdie, - ]; - let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); + new_test_ext(genesis_config(paras)).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); + + run_to_block(5, |_| None); + + let signing_context = + SigningContext { parent_hash: System::parent_hash(), session_index: 5 }; + + let group_validators = |group_index: GroupIndex| { + match group_index { + group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1, 2, 3, 4]), + _ => panic!("Group index out of bounds for 1 parachain"), + } + .map(|vs| vs.into_iter().map(ValidatorIndex).collect::>()) + }; + + let chain_a_assignment = CoreAssignment { + core: CoreIndex::from(0), + para_id: chain_a, + kind: AssignmentKind::Parachain, + group_idx: GroupIndex::from(0), + }; + + let mut candidate_a = TestCandidateBuilder { + para_id: chain_a, + relay_parent: System::parent_hash(), + pov_hash: Hash::repeat_byte(1), + persisted_validation_data_hash: make_vdata_hash(chain_a).unwrap(), + new_validation_code: Some(vec![1, 2, 3].into()), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() } - let validator_public = validator_pubkeys(&validators); + .build(); + collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a); - new_test_ext(genesis_config(paras)).execute_with(|| { - shared::Pallet::::set_active_validators_ascending(validator_public.clone()); - shared::Pallet::::set_session_index(5); + let backed_a = block_on(back_candidate( + candidate_a.clone(), + &validators, + group_validators(GroupIndex::from(0)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); - let validators_new = - vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie]; + let ProcessedCandidates { core_indices: occupied_cores, .. } = + ParaInclusion::process_candidates( + Default::default(), + vec![backed_a], + vec![chain_a_assignment.clone()], + &group_validators, + FullCheck::Yes, + ) + .expect("candidates scheduled, in order, and backed"); - let validator_public_new = validator_pubkeys(&validators_new); + assert_eq!(occupied_cores, vec![CoreIndex::from(0)]); - run_to_block(10, |_| None); + assert_eq!( + >::get(&chain_a), + Some(CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: candidate_a.hash(), + descriptor: candidate_a.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: System::block_number() - 1, + backed_in_number: System::block_number(), + backers: backing_bitfield(&[0, 1, 2]), + backing_group: GroupIndex::from(0), + }) + ); + assert_eq!( + >::get(&chain_a), + Some(candidate_a.commitments), + ); + }); +} + +#[test] +fn session_change_wipes() { + let chain_a = ParaId::from(1); + let chain_b = ParaId::from(2); + let thread_a = ParaId::from(3); + + let paras = vec![(chain_a, true), (chain_b, true), (thread_a, false)]; + let validators = vec![ + Sr25519Keyring::Alice, + Sr25519Keyring::Bob, + Sr25519Keyring::Charlie, + Sr25519Keyring::Dave, + Sr25519Keyring::Ferdie, + ]; + let keystore: SyncCryptoStorePtr = Arc::new(LocalKeystore::in_memory()); + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); - >::insert( - &ValidatorIndex(0), - AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, - ); + new_test_ext(genesis_config(paras)).execute_with(|| { + shared::Pallet::::set_active_validators_ascending(validator_public.clone()); + shared::Pallet::::set_session_index(5); - >::insert( - &ValidatorIndex(1), - AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, - ); + let validators_new = + vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie]; - >::insert( - &ValidatorIndex(4), - AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, - ); + let validator_public_new = validator_pubkeys(&validators_new); - let candidate = TestCandidateBuilder::default().build(); - >::insert( - &chain_a, - CandidatePendingAvailability { - core: CoreIndex::from(0), - hash: candidate.hash(), - descriptor: candidate.descriptor.clone(), - availability_votes: default_availability_votes(), - relay_parent_number: 5, - backed_in_number: 6, - backers: default_backing_bitfield(), - backing_group: GroupIndex::from(0), - }, - ); - >::insert(&chain_a, candidate.commitments.clone()); + run_to_block(10, |_| None); - >::insert( - &chain_b, - CandidatePendingAvailability { - core: CoreIndex::from(1), - hash: candidate.hash(), - descriptor: candidate.descriptor, - availability_votes: default_availability_votes(), - relay_parent_number: 6, - backed_in_number: 7, - backers: default_backing_bitfield(), - backing_group: GroupIndex::from(1), - }, - ); - >::insert(&chain_b, candidate.commitments); - - run_to_block(11, |_| None); - - assert_eq!(shared::Pallet::::session_index(), 5); - - assert!(>::get(&ValidatorIndex(0)).is_some()); - assert!(>::get(&ValidatorIndex(1)).is_some()); - assert!(>::get(&ValidatorIndex(4)).is_some()); - - assert!(>::get(&chain_a).is_some()); - assert!(>::get(&chain_b).is_some()); - assert!(>::get(&chain_a).is_some()); - assert!(>::get(&chain_b).is_some()); - - run_to_block(12, |n| match n { - 12 => Some(SessionChangeNotification { - validators: validator_public_new.clone(), - queued: Vec::new(), - prev_config: default_config(), - new_config: default_config(), - random_seed: Default::default(), - session_index: 6, - }), - _ => None, - }); + >::insert( + &ValidatorIndex(0), + AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, + ); - assert_eq!(shared::Pallet::::session_index(), 6); + >::insert( + &ValidatorIndex(1), + AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, + ); - assert!(>::get(&ValidatorIndex(0)).is_none()); - assert!(>::get(&ValidatorIndex(1)).is_none()); - assert!(>::get(&ValidatorIndex(4)).is_none()); + >::insert( + &ValidatorIndex(4), + AvailabilityBitfieldRecord { bitfield: default_bitfield(), submitted_at: 9 }, + ); - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_b).is_none()); - assert!(>::get(&chain_a).is_none()); - assert!(>::get(&chain_b).is_none()); + let candidate = TestCandidateBuilder::default().build(); + >::insert( + &chain_a, + CandidatePendingAvailability { + core: CoreIndex::from(0), + hash: candidate.hash(), + descriptor: candidate.descriptor.clone(), + availability_votes: default_availability_votes(), + relay_parent_number: 5, + backed_in_number: 6, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(0), + }, + ); + >::insert(&chain_a, candidate.commitments.clone()); - assert!(>::iter().collect::>().is_empty()); - assert!(>::iter().collect::>().is_empty()); - assert!(>::iter().collect::>().is_empty()); + >::insert( + &chain_b, + CandidatePendingAvailability { + core: CoreIndex::from(1), + hash: candidate.hash(), + descriptor: candidate.descriptor, + availability_votes: default_availability_votes(), + relay_parent_number: 6, + backed_in_number: 7, + backers: default_backing_bitfield(), + backing_group: GroupIndex::from(1), + }, + ); + >::insert(&chain_b, candidate.commitments); + + run_to_block(11, |_| None); + + assert_eq!(shared::Pallet::::session_index(), 5); + + assert!(>::get(&ValidatorIndex(0)).is_some()); + assert!(>::get(&ValidatorIndex(1)).is_some()); + assert!(>::get(&ValidatorIndex(4)).is_some()); + + assert!(>::get(&chain_a).is_some()); + assert!(>::get(&chain_b).is_some()); + assert!(>::get(&chain_a).is_some()); + assert!(>::get(&chain_b).is_some()); + + run_to_block(12, |n| match n { + 12 => Some(SessionChangeNotification { + validators: validator_public_new.clone(), + queued: Vec::new(), + prev_config: default_config(), + new_config: default_config(), + random_seed: Default::default(), + session_index: 6, + }), + _ => None, }); - } - // TODO [now]: test `collect_disputed` \ No newline at end of file + assert_eq!(shared::Pallet::::session_index(), 6); + + assert!(>::get(&ValidatorIndex(0)).is_none()); + assert!(>::get(&ValidatorIndex(1)).is_none()); + assert!(>::get(&ValidatorIndex(4)).is_none()); + + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_b).is_none()); + assert!(>::get(&chain_a).is_none()); + assert!(>::get(&chain_b).is_none()); + + assert!(>::iter().collect::>().is_empty()); + assert!(>::iter().collect::>().is_empty()); + assert!(>::iter().collect::>().is_empty()); + }); +} + +// TODO [now]: test `collect_disputed` diff --git a/runtime/parachains/src/paras_inherent/mod.rs b/runtime/parachains/src/paras_inherent/mod.rs index b16446c5c57b..d0f3730a970b 100644 --- a/runtime/parachains/src/paras_inherent/mod.rs +++ b/runtime/parachains/src/paras_inherent/mod.rs @@ -22,15 +22,15 @@ //! this module. use crate::{ + configuration, + configuration::HostConfiguration, disputes::DisputesHandler, inclusion, inclusion::{CandidateCheckContext, FullCheck}, initializer, scheduler::{self, CoreAssignment, FreedReason}, shared, ump, - configuration }; -use crate::configuration::HostConfiguration; use bitvec::prelude::BitVec; use frame_support::{ inherent::{InherentData, InherentIdentifier, MakeFatalError, ProvideInherent}, @@ -63,9 +63,9 @@ mod weights; pub use self::{ misc::IndexedRetain, weights::{ - backed_candidate_weight, backed_candidates_weight, dispute_statements_weight, - paras_inherent_total_weight, signed_bitfields_weight, TestWeightInfo, WeightInfo, - enact_candidates_weight, bitfields_count_ones + backed_candidate_weight, backed_candidates_weight, bitfields_count_ones, + dispute_statements_weight, enact_candidates_weight, paras_inherent_total_weight, + signed_bitfields_weight, TestWeightInfo, WeightInfo, }, }; @@ -749,8 +749,7 @@ fn apply_weight_limit( // we include the worst case weight of enacting the candidates voted for by the bitfields let total_bitfields_weight = signed_bitfields_weight::(bitfields.len()); - let total_enact_candidates_weight = - enact_candidates_weight::(cores_with_votes, config); + let total_enact_candidates_weight = enact_candidates_weight::(cores_with_votes, config); let total = total_bitfields_weight .saturating_add(total_candidates_weight) diff --git a/runtime/parachains/src/paras_inherent/tests.rs b/runtime/parachains/src/paras_inherent/tests.rs index f098a369d059..86e411ea9430 100644 --- a/runtime/parachains/src/paras_inherent/tests.rs +++ b/runtime/parachains/src/paras_inherent/tests.rs @@ -1,1119 +1,1091 @@ + +use super::*; + +// In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl +// that uses 0 for all the weights. Because all the weights are 0, the tests that rely on +// weights for limiting data will fail, so we don't run them when using the benchmark feature. +#[cfg(not(feature = "runtime-benchmarks"))] +mod enter { use super::*; + use crate::{ + builder::{Bench, BenchBuilder}, + mock::{new_test_ext, MockGenesisConfig, Test}, + }; + use frame_support::assert_ok; + use sp_std::collections::btree_map::BTreeMap; + + struct TestConfig { + dispute_statements: BTreeMap, + dispute_sessions: Vec, + backed_and_concluding: BTreeMap, + num_validators_per_core: u32, + includes_code_upgrade: Option, + } - // In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl - // that uses 0 for all the weights. Because all the weights are 0, the tests that rely on - // weights for limiting data will fail, so we don't run them when using the benchmark feature. - #[cfg(not(feature = "runtime-benchmarks"))] - mod enter { - use super::*; - use crate::{ - builder::{Bench, BenchBuilder}, - mock::{new_test_ext, MockGenesisConfig, Test}, - }; - use frame_support::assert_ok; - use sp_std::collections::btree_map::BTreeMap; - - struct TestConfig { - dispute_statements: BTreeMap, - dispute_sessions: Vec, - backed_and_concluding: BTreeMap, - num_validators_per_core: u32, - includes_code_upgrade: Option, - } + fn make_inherent_data( + TestConfig { + dispute_statements, + dispute_sessions, + backed_and_concluding, + num_validators_per_core, + includes_code_upgrade, + }: TestConfig, + ) -> Bench { + BenchBuilder::::new() + .set_max_validators((dispute_sessions.len() as u32) * num_validators_per_core) + .set_max_validators_per_core(num_validators_per_core) + .set_dispute_statements(dispute_statements) + .build(backed_and_concluding, dispute_sessions.as_slice(), includes_code_upgrade) + } + + #[test] + // Validate that if we create 2 backed candidates which are assigned to 2 cores that will be freed via + // becoming fully available, the backed candidates will not be filtered out in `create_inherent` and + // will not cause `enter` to early. + fn include_backed_candidates() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + let dispute_statements = BTreeMap::new(); - fn make_inherent_data( - TestConfig { + let mut backed_and_concluding = BTreeMap::new(); + backed_and_concluding.insert(0, 1); + backed_and_concluding.insert(1, 1); + + let scenario = make_inherent_data(TestConfig { dispute_statements, - dispute_sessions, + dispute_sessions: vec![0, 0], backed_and_concluding, - num_validators_per_core, - includes_code_upgrade, - }: TestConfig, - ) -> Bench { - BenchBuilder::::new() - .set_max_validators((dispute_sessions.len() as u32) * num_validators_per_core) - .set_max_validators_per_core(num_validators_per_core) - .set_dispute_statements(dispute_statements) - .build(backed_and_concluding, dispute_sessions.as_slice(), includes_code_upgrade) - } - - #[test] - // Validate that if we create 2 backed candidates which are assigned to 2 cores that will be freed via - // becoming fully available, the backed candidates will not be filtered out in `create_inherent` and - // will not cause `enter` to early. - fn include_backed_candidates() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - backed_and_concluding.insert(0, 1); - backed_and_concluding.insert(1, 1); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0], - backed_and_concluding, - num_validators_per_core: 1, - includes_code_upgrade: None, - }); - - // We expect the scenario to have cores 0 & 1 with pending availability. The backed - // candidates are also created for cores 0 & 1, so once the pending available - // become fully available those cores are marked as free and scheduled for the backed - // candidates. - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (2 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 2); - // * 1 backed candidate per core (2 cores) - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 0 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 0); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - assert_eq!( - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(), - expected_para_inherent_data - ); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 backed candidates - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data - )); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 2 - ); + num_validators_per_core: 1, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that disputes are filtered out if the session is in the future. - fn filter_multi_dispute_data() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![ - 1, 2, 3, /* Session 3 too new, will get filtered out */ - ], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 3 disputes => 3 cores, 15 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 15); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - let multi_dispute_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Dispute for session that lies too far in the future should be filtered out - assert!(multi_dispute_inherent_data != expected_para_inherent_data); - - assert_eq!(multi_dispute_inherent_data.disputes.len(), 2); - - // Assert that the first 2 disputes are included - assert_eq!( - &multi_dispute_inherent_data.disputes[..2], - &expected_para_inherent_data.disputes[..2], - ); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - multi_dispute_inherent_data, - )); + // We expect the scenario to have cores 0 & 1 with pending availability. The backed + // candidates are also created for cores 0 & 1, so once the pending available + // become fully available those cores are marked as free and scheduled for the backed + // candidates. + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (2 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 2); + // * 1 backed candidate per core (2 cores) + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 0 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 0); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know there - // where no backed candidates included - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); - }); - } + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); - #[test] - // Ensure that when dispute data establishes an over weight block that we adequately - // filter out disputes according to our prioritization rule - fn limit_dispute_data() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - // No backed and concluding cores, so all cores will be fileld with disputesw - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes - backed_and_concluding, - num_validators_per_core: 6, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 18); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Expect that inherent data is filtered to include only 2 disputes - assert!(limit_inherent_data != expected_para_inherent_data); - - // Ensure that the included disputes are sorted by session - assert_eq!(limit_inherent_data.disputes.len(), 2); - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); + // Nothing is filtered out (including the backed candidates.) + assert_eq!( + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(), + expected_para_inherent_data + ); - assert_eq!( - // Ensure that our inherent data did not included backed candidates as expected - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); - }); - } + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); - #[test] - // Ensure that when dispute data establishes an over weight block that we abort - // due to an over weight block - fn limit_dispute_data_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - // No backed and concluding cores, so all cores will be fileld with disputesw - let backed_and_concluding = BTreeMap::new(); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes - backed_and_concluding, - num_validators_per_core: 6, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) - assert_eq!(expected_para_inherent_data.bitfields.len(), 18); - // * 0 backed candidate per core - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 backed candidates + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data + )); + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 2 + ); + }); + } + + #[test] + // Ensure that disputes are filtered out if the session is in the future. + fn filter_multi_dispute_data() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![1, 2, 3 /* Session 3 too new, will get filtered out */], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that when a block is over weight due to disputes, but there is still sufficient - // block weight to include a number of signed bitfields, the inherent data is filtered - // as expected - fn limit_dispute_data_ignore_backed_candidates() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 4, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) - assert_eq!(expected_para_inherent_data.bitfields.len(), 20); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - assert!(limit_inherent_data != expected_para_inherent_data); - - // Three disputes is over weight (see previous test), so we expect to only see 2 disputes - assert_eq!(limit_inherent_data.disputes.len(), 2); - // Ensure disputes are filtered as expected - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - // Ensure all bitfields are included as these are still not over weight - assert_eq!( - limit_inherent_data.bitfields.len(), - expected_para_inherent_data.bitfields.len() - ); - // Ensure that all backed candidates are filtered out as either would make the block over weight - assert_eq!(limit_inherent_data.backed_candidates.len(), 0); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 3 disputes => 3 cores, 15 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 15); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + let multi_dispute_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Dispute for session that lies too far in the future should be filtered out + assert!(multi_dispute_inherent_data != expected_para_inherent_data); + + assert_eq!(multi_dispute_inherent_data.disputes.len(), 2); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); + // Assert that the first 2 disputes are included + assert_eq!( + &multi_dispute_inherent_data.disputes[..2], + &expected_para_inherent_data.disputes[..2], + ); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + multi_dispute_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know there + // where no backed candidates included + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0 + ); + }); + } + + #[test] + // Ensure that when dispute data establishes an over weight block that we adequately + // filter out disputes according to our prioritization rule + fn limit_dispute_data() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + // No backed and concluding cores, so all cores will be fileld with disputesw + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes + backed_and_concluding, + num_validators_per_core: 6, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that we abort if we encounter an over weight block for disputes + bitfields - fn limit_dispute_data_ignore_backed_candidates_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let dispute_statements = BTreeMap::new(); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 4, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) - assert_eq!(expected_para_inherent_data.bitfields.len(), 20); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Ensure that calling enter with 3 disputes and 2 candidates is over weight - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 18); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Expect that inherent data is filtered to include only 2 disputes + assert!(limit_inherent_data != expected_para_inherent_data); + + // Ensure that the included disputes are sorted by session + assert_eq!(limit_inherent_data.disputes.len(), 2); + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // Ensure that our inherent data did not included backed candidates as expected + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0 + ); + }); + } + + #[test] + // Ensure that when dispute data establishes an over weight block that we abort + // due to an over weight block + fn limit_dispute_data_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + // No backed and concluding cores, so all cores will be fileld with disputesw + let backed_and_concluding = BTreeMap::new(); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![2, 2, 1], // 3 cores, all disputes + backed_and_concluding, + num_validators_per_core: 6, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, the bitfields are - // filtered to accommodate the block size and no backed candidates are included. - fn limit_bitfields() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Cap the number of statements per dispute to 20 in order to ensure we have enough - // space in the block for some (but not all) bitfields - dispute_statements.insert(2, 20); - dispute_statements.insert(3, 20); - dispute_statements.insert(4, 20); - - let mut backed_and_concluding = BTreeMap::new(); - // Schedule 2 backed candidates - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20), - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates, - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - // Nothing is filtered out (including the backed candidates.) - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - assert!(limit_inherent_data != expected_para_inherent_data); - - // Three disputes is over weight (see previous test), so we expect to only see 2 disputes - assert_eq!(limit_inherent_data.disputes.len(), 2); - // Ensure disputes are filtered as expected - assert_eq!(limit_inherent_data.disputes[0].session, 1); - assert_eq!(limit_inherent_data.disputes[1].session, 2); - // Ensure all bitfields are included as these are still not over weight - assert_eq!(limit_inherent_data.bitfields.len(), 20,); - // Ensure that all backed candidates are filtered out as either would make the block over weight - assert_eq!(limit_inherent_data.backed_candidates.len(), 0); - - // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not - // alter storage, but just double checking for sanity). - assert_eq!(>::scheduled(), vec![]); - - assert_eq!(Pallet::::on_chain_votes(), None); - // Call enter with our 2 disputes - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (6 validators per core, 3 disputes => 18 validators) + assert_eq!(expected_para_inherent_data.bitfields.len(), 18); + // * 0 backed candidate per core + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 0); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes, but there is still sufficient + // block weight to include a number of signed bitfields, the inherent data is filtered + // as expected + fn limit_dispute_data_ignore_backed_candidates() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 4, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_bitfields_overweight() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 20); - dispute_statements.insert(3, 20); - dispute_statements.insert(4, 20); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 2); - backed_and_concluding.insert(1, 2); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) - dispute_sessions: vec![0, 0, 2, 2, 1], - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) + assert_eq!(expected_para_inherent_data.bitfields.len(), 20); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know - // all of our candidates got filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0, - ); + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Nothing is filtered out (including the backed candidates.) + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + assert!(limit_inherent_data != expected_para_inherent_data); + + // Three disputes is over weight (see previous test), so we expect to only see 2 disputes + assert_eq!(limit_inherent_data.disputes.len(), 2); + // Ensure disputes are filtered as expected + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + // Ensure all bitfields are included as these are still not over weight + assert_eq!( + limit_inherent_data.bitfields.len(), + expected_para_inherent_data.bitfields.len() + ); + // Ensure that all backed candidates are filtered out as either would make the block over weight + assert_eq!(limit_inherent_data.backed_candidates.len(), 0); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0, + ); + }); + } + + #[test] + // Ensure that we abort if we encounter an over weight block for disputes + bitfields + fn limit_dispute_data_ignore_backed_candidates_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let dispute_statements = BTreeMap::new(); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 4, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_candidates_over_weight_1() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 17); - dispute_statements.insert(3, 17); - dispute_statements.insert(4, 17); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 16); - backed_and_concluding.insert(1, 25); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - let mut inherent_data = InherentData::new(); - inherent_data - .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) - .unwrap(); - - let limit_inherent_data = - Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); - // Expect that inherent data is filtered to include only 1 backed candidate and 2 disputes - assert!(limit_inherent_data != expected_para_inherent_data); - - // * 1 bitfields - assert_eq!(limit_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(limit_inherent_data.backed_candidates.len(), 1); - // * 3 disputes. - assert_eq!(limit_inherent_data.disputes.len(), 2); - - // The current schedule is empty prior to calling `create_inherent_enter`. - assert_eq!(>::scheduled(), vec![]); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - limit_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (4 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20) + assert_eq!(expected_para_inherent_data.bitfields.len(), 20); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 1 - ); + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Ensure that calling enter with 3 disputes and 2 candidates is over weight + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0, + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, the bitfields are + // filtered to accommodate the block size and no backed candidates are included. + fn limit_bitfields() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Cap the number of statements per dispute to 20 in order to ensure we have enough + // space in the block for some (but not all) bitfields + dispute_statements.insert(2, 20); + dispute_statements.insert(3, 20); + dispute_statements.insert(4, 20); + + let mut backed_and_concluding = BTreeMap::new(); + // Schedule 2 backed candidates + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, }); - } - #[test] - // Ensure that when a block is over weight due to disputes and bitfields, we abort - fn limit_candidates_over_weight_0() { - new_test_ext(MockGenesisConfig::default()).execute_with(|| { - // Create the inherent data for this block - let mut dispute_statements = BTreeMap::new(); - // Control the number of statements per dispute to ensure we have enough space - // in the block for some (but not all) bitfields - dispute_statements.insert(2, 17); - dispute_statements.insert(3, 17); - dispute_statements.insert(4, 17); - - let mut backed_and_concluding = BTreeMap::new(); - // 2 backed candidates shall be scheduled - backed_and_concluding.insert(0, 16); - backed_and_concluding.insert(1, 25); - - let scenario = make_inherent_data(TestConfig { - dispute_statements, - dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively - backed_and_concluding, - num_validators_per_core: 5, - includes_code_upgrade: None, - }); - - let expected_para_inherent_data = scenario.data.clone(); - - // Check the para inherent data is as expected: - // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) - assert_eq!(expected_para_inherent_data.bitfields.len(), 25); - // * 2 backed candidates - assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); - // * 3 disputes. - assert_eq!(expected_para_inherent_data.disputes.len(), 3); - - assert_ok!(Pallet::::enter( - frame_system::RawOrigin::None.into(), - expected_para_inherent_data, - )); + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 4*5 = 20), + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates, + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + // Nothing is filtered out (including the backed candidates.) + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + assert!(limit_inherent_data != expected_para_inherent_data); + + // Three disputes is over weight (see previous test), so we expect to only see 2 disputes + assert_eq!(limit_inherent_data.disputes.len(), 2); + // Ensure disputes are filtered as expected + assert_eq!(limit_inherent_data.disputes[0].session, 1); + assert_eq!(limit_inherent_data.disputes[1].session, 2); + // Ensure all bitfields are included as these are still not over weight + assert_eq!(limit_inherent_data.bitfields.len(), 20,); + // Ensure that all backed candidates are filtered out as either would make the block over weight + assert_eq!(limit_inherent_data.backed_candidates.len(), 0); + + // The schedule is still empty prior to calling `enter`. (`create_inherent_inner` should not + // alter storage, but just double checking for sanity). + assert_eq!(>::scheduled(), vec![]); + + assert_eq!(Pallet::::on_chain_votes(), None); + // Call enter with our 2 disputes + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); - assert_eq!( - // The length of this vec is equal to the number of candidates, so we know our 2 - // backed candidates did not get filtered out - Pallet::::on_chain_votes() - .unwrap() - .backing_validators_per_candidate - .len(), - 0 - ); + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0, + ); + }); + } + + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_bitfields_overweight() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 20); + dispute_statements.insert(3, 20); + dispute_statements.insert(4, 20); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 2); + backed_and_concluding.insert(1, 2); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + // 2 backed candidates + 3 disputes (at sessions 2, 1 and 1) + dispute_sessions: vec![0, 0, 2, 2, 1], + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, }); - } + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know + // all of our candidates got filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0, + ); + }); } - fn default_header() -> primitives::v1::Header { - primitives::v1::Header { - parent_hash: Default::default(), - number: 0, - state_root: Default::default(), - extrinsics_root: Default::default(), - digest: Default::default(), - } + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_candidates_over_weight_1() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 17); + dispute_statements.insert(3, 17); + dispute_statements.insert(4, 17); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 16); + backed_and_concluding.insert(1, 25); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); + + let expected_para_inherent_data = scenario.data.clone(); + + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(PARACHAINS_INHERENT_IDENTIFIER, &expected_para_inherent_data) + .unwrap(); + + let limit_inherent_data = + Pallet::::create_inherent_inner(&inherent_data.clone()).unwrap(); + // Expect that inherent data is filtered to include only 1 backed candidate and 2 disputes + assert!(limit_inherent_data != expected_para_inherent_data); + + // * 1 bitfields + assert_eq!(limit_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(limit_inherent_data.backed_candidates.len(), 1); + // * 3 disputes. + assert_eq!(limit_inherent_data.disputes.len(), 2); + + // The current schedule is empty prior to calling `create_inherent_enter`. + assert_eq!(>::scheduled(), vec![]); + + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + limit_inherent_data, + )); + + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 1 + ); + }); } - mod sanitizers { - use super::*; + #[test] + // Ensure that when a block is over weight due to disputes and bitfields, we abort + fn limit_candidates_over_weight_0() { + new_test_ext(MockGenesisConfig::default()).execute_with(|| { + // Create the inherent data for this block + let mut dispute_statements = BTreeMap::new(); + // Control the number of statements per dispute to ensure we have enough space + // in the block for some (but not all) bitfields + dispute_statements.insert(2, 17); + dispute_statements.insert(3, 17); + dispute_statements.insert(4, 17); + + let mut backed_and_concluding = BTreeMap::new(); + // 2 backed candidates shall be scheduled + backed_and_concluding.insert(0, 16); + backed_and_concluding.insert(1, 25); + + let scenario = make_inherent_data(TestConfig { + dispute_statements, + dispute_sessions: vec![0, 0, 2, 2, 1], // 2 backed candidates, 3 disputes at sessions 2, 1 and 1 respectively + backed_and_concluding, + num_validators_per_core: 5, + includes_code_upgrade: None, + }); - use crate::inclusion::tests::{ - back_candidate, collator_sign_candidate, BackingKind, TestCandidateBuilder, - }; - use bitvec::order::Lsb0; - use primitives::v1::{ - AvailabilityBitfield, GroupIndex, Hash, Id as ParaId, SignedAvailabilityBitfield, - ValidatorIndex, - }; + let expected_para_inherent_data = scenario.data.clone(); - use crate::mock::Test; - use futures::executor::block_on; - use keyring::Sr25519Keyring; - use primitives::v0::PARACHAIN_KEY_TYPE_ID; - use sc_keystore::LocalKeystore; - use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; - use std::sync::Arc; + // Check the para inherent data is as expected: + // * 1 bitfield per validator (5 validators per core, 2 backed candidates, 3 disputes => 5*5 = 25) + assert_eq!(expected_para_inherent_data.bitfields.len(), 25); + // * 2 backed candidates + assert_eq!(expected_para_inherent_data.backed_candidates.len(), 2); + // * 3 disputes. + assert_eq!(expected_para_inherent_data.disputes.len(), 3); - fn validator_pubkeys(val_ids: &[keyring::Sr25519Keyring]) -> Vec { - val_ids.iter().map(|v| v.public().into()).collect() - } + assert_ok!(Pallet::::enter( + frame_system::RawOrigin::None.into(), + expected_para_inherent_data, + )); - #[test] - fn bitfields() { - let header = default_header(); - let parent_hash = header.hash(); - // 2 cores means two bits - let expected_bits = 2; - let session_index = SessionIndex::from(0_u32); - - let crypto_store = LocalKeystore::in_memory(); - let crypto_store = Arc::new(crypto_store) as SyncCryptoStorePtr; - let signing_context = SigningContext { parent_hash, session_index }; - - let validators = vec![ - keyring::Sr25519Keyring::Alice, - keyring::Sr25519Keyring::Bob, - keyring::Sr25519Keyring::Charlie, - keyring::Sr25519Keyring::Dave, - ]; - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*crypto_store, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), - ) - .unwrap(); - } - let validator_public = validator_pubkeys(&validators); - - let unchecked_bitfields = [ - BitVec::::repeat(true, expected_bits), - BitVec::::repeat(true, expected_bits), - { - let mut bv = BitVec::::repeat(false, expected_bits); - bv.set(expected_bits - 1, true); - bv - }, - ] - .iter() - .enumerate() - .map(|(vi, ab)| { - let validator_index = ValidatorIndex::from(vi as u32); - block_on(SignedAvailabilityBitfield::sign( - &crypto_store, - AvailabilityBitfield::from(ab.clone()), - &signing_context, - validator_index, - &validator_public[vi], - )) - .unwrap() - .unwrap() - .into_unchecked() - }) - .collect::>(); + assert_eq!( + // The length of this vec is equal to the number of candidates, so we know our 2 + // backed candidates did not get filtered out + Pallet::::on_chain_votes().unwrap().backing_validators_per_candidate.len(), + 0 + ); + }); + } +} + +fn default_header() -> primitives::v1::Header { + primitives::v1::Header { + parent_hash: Default::default(), + number: 0, + state_root: Default::default(), + extrinsics_root: Default::default(), + digest: Default::default(), + } +} - let disputed_bitfield = DisputedBitfield::zeros(expected_bits); +mod sanitizers { + use super::*; - { - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip, - ), - (unchecked_bitfields.clone(), expected_bits) - ); - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ), - (unchecked_bitfields.clone(), expected_bits) - ); - } + use crate::inclusion::tests::{ + back_candidate, collator_sign_candidate, BackingKind, TestCandidateBuilder, + }; + use bitvec::order::Lsb0; + use primitives::v1::{ + AvailabilityBitfield, GroupIndex, Hash, Id as ParaId, SignedAvailabilityBitfield, + ValidatorIndex, + }; + + use crate::mock::Test; + use futures::executor::block_on; + use keyring::Sr25519Keyring; + use primitives::v0::PARACHAIN_KEY_TYPE_ID; + use sc_keystore::LocalKeystore; + use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr}; + use std::sync::Arc; + + fn validator_pubkeys(val_ids: &[keyring::Sr25519Keyring]) -> Vec { + val_ids.iter().map(|v| v.public().into()).collect() + } - // disputed bitfield is non-zero - { - let mut disputed_bitfield = DisputedBitfield::zeros(expected_bits); - // pretend the first core was freed by either a malicious validator - // or by resolved dispute - disputed_bitfield.0.set(0, true); - - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0 - .len(), - 1 - ); - assert_eq!( - sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0 - .len(), - 1 - ); - } + #[test] + fn bitfields() { + let header = default_header(); + let parent_hash = header.hash(); + // 2 cores means two bits + let expected_bits = 2; + let session_index = SessionIndex::from(0_u32); + + let crypto_store = LocalKeystore::in_memory(); + let crypto_store = Arc::new(crypto_store) as SyncCryptoStorePtr; + let signing_context = SigningContext { parent_hash, session_index }; + + let validators = vec![ + keyring::Sr25519Keyring::Alice, + keyring::Sr25519Keyring::Bob, + keyring::Sr25519Keyring::Charlie, + keyring::Sr25519Keyring::Dave, + ]; + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*crypto_store, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + let validator_public = validator_pubkeys(&validators); - // bitfield size mismatch + let unchecked_bitfields = [ + BitVec::::repeat(true, expected_bits), + BitVec::::repeat(true, expected_bits), { - assert!(sanitize_bitfields::( + let mut bv = BitVec::::repeat(false, expected_bits); + bv.set(expected_bits - 1, true); + bv + }, + ] + .iter() + .enumerate() + .map(|(vi, ab)| { + let validator_index = ValidatorIndex::from(vi as u32); + block_on(SignedAvailabilityBitfield::sign( + &crypto_store, + AvailabilityBitfield::from(ab.clone()), + &signing_context, + validator_index, + &validator_public[vi], + )) + .unwrap() + .unwrap() + .into_unchecked() + }) + .collect::>(); + + let disputed_bitfield = DisputedBitfield::zeros(expected_bits); + + { + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip, + ), + (unchecked_bitfields.clone(), expected_bits) + ); + assert_eq!( + sanitize_bitfields::( unchecked_bitfields.clone(), disputed_bitfield.clone(), - expected_bits + 1, + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ), + (unchecked_bitfields.clone(), expected_bits) + ); + } + + // disputed bitfield is non-zero + { + let mut disputed_bitfield = DisputedBitfield::zeros(expected_bits); + // pretend the first core was freed by either a malicious validator + // or by resolved dispute + disputed_bitfield.0.set(0, true); + + assert_eq!( + sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, parent_hash, session_index, &validator_public[..], FullCheck::Yes ) .0 - .is_empty()); - assert!(sanitize_bitfields::( + .len(), + 1 + ); + assert_eq!( + sanitize_bitfields::( unchecked_bitfields.clone(), disputed_bitfield.clone(), - expected_bits + 1, + expected_bits, parent_hash, session_index, &validator_public[..], FullCheck::Skip ) .0 - .is_empty()); - } + .len(), + 1 + ); + } - // remove the last validator - { - let shortened = validator_public.len() - 2; - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..shortened], - FullCheck::Yes, - ) - .0[..], - &unchecked_bitfields[..shortened] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..shortened], - FullCheck::Skip, - ) - .0[..], - &unchecked_bitfields[..shortened] - ); - } + // bitfield size mismatch + { + assert!(sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits + 1, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0 + .is_empty()); + assert!(sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits + 1, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0 + .is_empty()); + } - // switch ordering of bitfields - { - let mut unchecked_bitfields = unchecked_bitfields.clone(); - let x = unchecked_bitfields.swap_remove(0); - unchecked_bitfields.push(x); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0[..], - &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0[..], - &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] - ); - } + // remove the last validator + { + let shortened = validator_public.len() - 2; + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..shortened], + FullCheck::Yes, + ) + .0[..], + &unchecked_bitfields[..shortened] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..shortened], + FullCheck::Skip, + ) + .0[..], + &unchecked_bitfields[..shortened] + ); + } - // check the validators signature - { - use primitives::v1::ValidatorSignature; - let mut unchecked_bitfields = unchecked_bitfields.clone(); - - // insert a bad signature for the last bitfield - let last_bit_idx = unchecked_bitfields.len() - 1; - unchecked_bitfields - .get_mut(last_bit_idx) - .and_then(|u| Some(u.set_signature(ValidatorSignature::default()))) - .expect("we are accessing a valid index"); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Yes - ) - .0[..], - &unchecked_bitfields[..last_bit_idx] - ); - assert_eq!( - &sanitize_bitfields::( - unchecked_bitfields.clone(), - disputed_bitfield.clone(), - expected_bits, - parent_hash, - session_index, - &validator_public[..], - FullCheck::Skip - ) - .0[..], - &unchecked_bitfields[..] - ); - } + // switch ordering of bitfields + { + let mut unchecked_bitfields = unchecked_bitfields.clone(); + let x = unchecked_bitfields.swap_remove(0); + unchecked_bitfields.push(x); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes + ) + .0[..], + &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0[..], + &unchecked_bitfields[..(unchecked_bitfields.len() - 2)] + ); } - #[test] - fn candidates() { - const RELAY_PARENT_NUM: u32 = 3; - - let header = default_header(); - let relay_parent = header.hash(); - let session_index = SessionIndex::from(0_u32); - - let keystore = LocalKeystore::in_memory(); - let keystore = Arc::new(keystore) as SyncCryptoStorePtr; - let signing_context = SigningContext { parent_hash: relay_parent, session_index }; - - let validators = vec![ - keyring::Sr25519Keyring::Alice, - keyring::Sr25519Keyring::Bob, - keyring::Sr25519Keyring::Charlie, - keyring::Sr25519Keyring::Dave, - ]; - for validator in validators.iter() { - SyncCryptoStore::sr25519_generate_new( - &*keystore, - PARACHAIN_KEY_TYPE_ID, - Some(&validator.to_seed()), + // check the validators signature + { + use primitives::v1::ValidatorSignature; + let mut unchecked_bitfields = unchecked_bitfields.clone(); + + // insert a bad signature for the last bitfield + let last_bit_idx = unchecked_bitfields.len() - 1; + unchecked_bitfields + .get_mut(last_bit_idx) + .and_then(|u| Some(u.set_signature(ValidatorSignature::default()))) + .expect("we are accessing a valid index"); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Yes ) - .unwrap(); + .0[..], + &unchecked_bitfields[..last_bit_idx] + ); + assert_eq!( + &sanitize_bitfields::( + unchecked_bitfields.clone(), + disputed_bitfield.clone(), + expected_bits, + parent_hash, + session_index, + &validator_public[..], + FullCheck::Skip + ) + .0[..], + &unchecked_bitfields[..] + ); + } + } + + #[test] + fn candidates() { + const RELAY_PARENT_NUM: u32 = 3; + + let header = default_header(); + let relay_parent = header.hash(); + let session_index = SessionIndex::from(0_u32); + + let keystore = LocalKeystore::in_memory(); + let keystore = Arc::new(keystore) as SyncCryptoStorePtr; + let signing_context = SigningContext { parent_hash: relay_parent, session_index }; + + let validators = vec![ + keyring::Sr25519Keyring::Alice, + keyring::Sr25519Keyring::Bob, + keyring::Sr25519Keyring::Charlie, + keyring::Sr25519Keyring::Dave, + ]; + for validator in validators.iter() { + SyncCryptoStore::sr25519_generate_new( + &*keystore, + PARACHAIN_KEY_TYPE_ID, + Some(&validator.to_seed()), + ) + .unwrap(); + } + + let has_concluded_invalid = + |_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false }; + + let scheduled = (0_usize..2) + .into_iter() + .map(|idx| { + let ca = CoreAssignment { + kind: scheduler::AssignmentKind::Parachain, + group_idx: GroupIndex::from(idx as u32), + para_id: ParaId::from(1_u32 + idx as u32), + core: CoreIndex::from(idx as u32), + }; + ca + }) + .collect::>(); + let scheduled = &scheduled[..]; + + let group_validators = |group_index: GroupIndex| { + match group_index { + group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), + group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), + _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), } + .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) + }; - let has_concluded_invalid = - |_idx: usize, _backed_candidate: &BackedCandidate| -> bool { false }; - - let scheduled = (0_usize..2) - .into_iter() - .map(|idx| { - let ca = CoreAssignment { - kind: scheduler::AssignmentKind::Parachain, - group_idx: GroupIndex::from(idx as u32), - para_id: ParaId::from(1_u32 + idx as u32), - core: CoreIndex::from(idx as u32), - }; - ca - }) - .collect::>(); - let scheduled = &scheduled[..]; - - let group_validators = |group_index: GroupIndex| { - match group_index { - group_index if group_index == GroupIndex::from(0) => Some(vec![0, 1]), - group_index if group_index == GroupIndex::from(1) => Some(vec![2, 3]), - _ => panic!("Group index out of bounds for 2 parachains and 1 parathread core"), + let backed_candidates = (0_usize..2) + .into_iter() + .map(|idx0| { + let idx1 = idx0 + 1; + let mut candidate = TestCandidateBuilder { + para_id: ParaId::from(idx1), + relay_parent, + pov_hash: Hash::repeat_byte(idx1 as u8), + persisted_validation_data_hash: [42u8; 32].into(), + hrmp_watermark: RELAY_PARENT_NUM, + ..Default::default() } - .map(|m| m.into_iter().map(ValidatorIndex).collect::>()) - }; + .build(); - let backed_candidates = (0_usize..2) - .into_iter() - .map(|idx0| { - let idx1 = idx0 + 1; - let mut candidate = TestCandidateBuilder { - para_id: ParaId::from(idx1), - relay_parent, - pov_hash: Hash::repeat_byte(idx1 as u8), - persisted_validation_data_hash: [42u8; 32].into(), - hrmp_watermark: RELAY_PARENT_NUM, - ..Default::default() + collator_sign_candidate(Sr25519Keyring::One, &mut candidate); + + let backed = block_on(back_candidate( + candidate, + &validators, + group_validators(GroupIndex::from(idx0 as u32)).unwrap().as_ref(), + &keystore, + &signing_context, + BackingKind::Threshold, + )); + backed + }) + .collect::>(); + + // happy path + assert_eq!( + sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ), + backed_candidates + ); + + // nothing is scheduled, so no paraids match, thus all backed candidates are skipped + { + let scheduled = &[][..]; + assert!(sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ) + .is_empty()); + } + + // relay parent mismatch + { + let relay_parent = Hash::repeat_byte(0xFA); + assert!(sanitize_backed_candidates::( + relay_parent, + backed_candidates.clone(), + has_concluded_invalid, + scheduled + ) + .is_empty()); + } + + // candidates that have concluded as invalid are filtered out + { + // mark every second one as concluded invalid + let set = { + let mut set = std::collections::HashSet::new(); + for (idx, backed_candidate) in backed_candidates.iter().enumerate() { + if idx & 0x01 == 0 { + set.insert(backed_candidate.hash().clone()); } - .build(); - - collator_sign_candidate(Sr25519Keyring::One, &mut candidate); - - let backed = block_on(back_candidate( - candidate, - &validators, - group_validators(GroupIndex::from(idx0 as u32)).unwrap().as_ref(), - &keystore, - &signing_context, - BackingKind::Threshold, - )); - backed - }) - .collect::>(); - - // happy path + } + set + }; + let has_concluded_invalid = + |_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash()); assert_eq!( sanitize_backed_candidates::( relay_parent, backed_candidates.clone(), has_concluded_invalid, scheduled - ), - backed_candidates - ); - - // nothing is scheduled, so no paraids match, thus all backed candidates are skipped - { - let scheduled = &[][..]; - assert!(sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ) - .is_empty()); - } - - // relay parent mismatch - { - let relay_parent = Hash::repeat_byte(0xFA); - assert!(sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled ) - .is_empty()); - } - - // candidates that have concluded as invalid are filtered out - { - // mark every second one as concluded invalid - let set = { - let mut set = std::collections::HashSet::new(); - for (idx, backed_candidate) in backed_candidates.iter().enumerate() { - if idx & 0x01 == 0 { - set.insert(backed_candidate.hash().clone()); - } - } - set - }; - let has_concluded_invalid = - |_idx: usize, candidate: &BackedCandidate| set.contains(&candidate.hash()); - assert_eq!( - sanitize_backed_candidates::( - relay_parent, - backed_candidates.clone(), - has_concluded_invalid, - scheduled - ) - .len(), - backed_candidates.len() / 2 - ); - } + .len(), + backed_candidates.len() / 2 + ); } - } \ No newline at end of file + } +} diff --git a/runtime/parachains/src/paras_inherent/weights.rs b/runtime/parachains/src/paras_inherent/weights.rs index edc56b1fb22e..76aca9ff5dea 100644 --- a/runtime/parachains/src/paras_inherent/weights.rs +++ b/runtime/parachains/src/paras_inherent/weights.rs @@ -14,12 +14,14 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . use super::{ - BackedCandidate, Config, DisputeStatementSet, UncheckedSignedAvailabilityBitfield, Weight, - cheap_bitfield_checks + cheap_bitfield_checks, BackedCandidate, Config, DisputeStatementSet, + UncheckedSignedAvailabilityBitfield, Weight, }; -use crate::configuration::HostConfiguration; -use crate::inclusion::{self, FullCheck}; - use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec}; +use crate::{ + configuration::HostConfiguration, + inclusion::{self, FullCheck}, +}; +use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec}; const MAX_UNCHECKED_BITFIELD_ITERATIONS: usize = 1_000; pub trait WeightInfo { @@ -175,4 +177,4 @@ pub fn backed_candidates_weight( .iter() .map(|c| backed_candidate_weight::(c)) .fold(0, |acc, x| acc.saturating_add(x)) -} \ No newline at end of file +} From b97fac22f163baadc8f46a6e0c3b5a27aee4c312 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Wed, 1 Dec 2021 18:34:07 -0800 Subject: [PATCH 24/27] fmt --- runtime/parachains/src/inclusion/tests.rs | 1 - runtime/parachains/src/paras_inherent/tests.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/runtime/parachains/src/inclusion/tests.rs b/runtime/parachains/src/inclusion/tests.rs index 878946060fa1..443879b1dbc0 100644 --- a/runtime/parachains/src/inclusion/tests.rs +++ b/runtime/parachains/src/inclusion/tests.rs @@ -1,4 +1,3 @@ - use super::*; use crate::{ configuration::HostConfiguration, diff --git a/runtime/parachains/src/paras_inherent/tests.rs b/runtime/parachains/src/paras_inherent/tests.rs index 86e411ea9430..acf523af1018 100644 --- a/runtime/parachains/src/paras_inherent/tests.rs +++ b/runtime/parachains/src/paras_inherent/tests.rs @@ -1,4 +1,3 @@ - use super::*; // In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl From a54939745a6cf86a585e01e64add770716741093 Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Mon, 6 Dec 2021 11:38:19 -0800 Subject: [PATCH 25/27] fix diff with file header --- runtime/parachains/src/inclusion/tests.rs | 16 ++++++++++++++++ runtime/parachains/src/paras_inherent/tests.rs | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/runtime/parachains/src/inclusion/tests.rs b/runtime/parachains/src/inclusion/tests.rs index 443879b1dbc0..20d03fdba004 100644 --- a/runtime/parachains/src/inclusion/tests.rs +++ b/runtime/parachains/src/inclusion/tests.rs @@ -1,3 +1,19 @@ +// Copyright 2021 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 . + use super::*; use crate::{ configuration::HostConfiguration, diff --git a/runtime/parachains/src/paras_inherent/tests.rs b/runtime/parachains/src/paras_inherent/tests.rs index acf523af1018..ad6303904042 100644 --- a/runtime/parachains/src/paras_inherent/tests.rs +++ b/runtime/parachains/src/paras_inherent/tests.rs @@ -1,3 +1,19 @@ +// Copyright 2021 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 . + use super::*; // In order to facilitate benchmarks as tests we have a benchmark feature gated `WeightInfo` impl From 9efb72f826ce12721d8575a2e15cb7c7ade131ce Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Mon, 6 Dec 2021 13:53:15 -0800 Subject: [PATCH 26/27] Remove stale TODOs --- runtime/parachains/src/paras_inherent/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/runtime/parachains/src/paras_inherent/mod.rs b/runtime/parachains/src/paras_inherent/mod.rs index d0f3730a970b..d4cab7432d42 100644 --- a/runtime/parachains/src/paras_inherent/mod.rs +++ b/runtime/parachains/src/paras_inherent/mod.rs @@ -624,7 +624,6 @@ impl Pallet { // Assure the maximum block weight is adhered. let max_block_weight = ::BlockWeights::get().max_block; - // TODO subtract enactment weight - return sanitized bitfields from enactment let _consumed_weight = apply_weight_limit::( &mut backed_candidates, &mut bitfields, @@ -829,7 +828,6 @@ fn apply_weight_limit( /// /// `full_check` determines if validator signatures are checked. If `::Yes`, /// bitfields that have an invalid signature will be filtered out. -// TODO also return all bitfields OR'ed together pub(crate) fn sanitize_bitfields( unchecked_bitfields: UncheckedSignedAvailabilityBitfields, disputed_bitfield: DisputedBitfield, From 1c10c27ab267b3a591230b4bfd5248b8c9f1ac2f Mon Sep 17 00:00:00 2001 From: Zeke Mostov Date: Fri, 10 Dec 2021 17:12:01 -0800 Subject: [PATCH 27/27] Update runtime/parachains/src/paras.rs --- runtime/parachains/src/paras.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index f070f01a7dae..09541c58496b 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -990,6 +990,7 @@ impl Pallet { pub(crate) fn note_new_head_weight() -> Weight { T::DbWeight::get().reads_writes(6, 6) } + /// Returns the current lifecycle state of the para. pub fn lifecycle(id: ParaId) -> Option { ParaLifecycles::::get(&id)