diff --git a/Cargo.lock b/Cargo.lock
index 7634cbc166a4edf979658d36c380e8f99b8d56ed..2d4485c92211b7bdac97e383e71ff82748471812 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14127,6 +14127,7 @@ dependencies = [
  "parity-scale-codec",
  "polkadot-core-primitives",
  "polkadot-parachain-primitives",
+ "polkadot-primitives-test-helpers",
  "scale-info",
  "serde",
  "sp-api",
@@ -14140,6 +14141,7 @@ dependencies = [
  "sp-keystore",
  "sp-runtime",
  "sp-staking",
+ "sp-std 14.0.0",
 ]
 
 [[package]]
diff --git a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
index c796dc5f7c382fb18b5b8f5c9b2d5a69eb43cb6b..629fa728be372b915af2e51248706700ac6909c9 100644
--- a/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
+++ b/cumulus/client/relay-chain-inprocess-interface/src/lib.rs
@@ -137,7 +137,11 @@ impl RelayChainInterface for RelayChainInProcessInterface {
 		hash: PHash,
 		para_id: ParaId,
 	) -> RelayChainResult<Option<CommittedCandidateReceipt>> {
-		Ok(self.full_client.runtime_api().candidate_pending_availability(hash, para_id)?)
+		Ok(self
+			.full_client
+			.runtime_api()
+			.candidate_pending_availability(hash, para_id)?
+			.map(|receipt| receipt.into()))
 	}
 
 	async fn session_index_for_child(&self, hash: PHash) -> RelayChainResult<SessionIndex> {
@@ -260,7 +264,13 @@ impl RelayChainInterface for RelayChainInProcessInterface {
 		&self,
 		relay_parent: PHash,
 	) -> RelayChainResult<Vec<CoreState<PHash, BlockNumber>>> {
-		Ok(self.full_client.runtime_api().availability_cores(relay_parent)?)
+		Ok(self
+			.full_client
+			.runtime_api()
+			.availability_cores(relay_parent)?
+			.into_iter()
+			.map(|core_state| core_state.into())
+			.collect::<Vec<_>>())
 	}
 
 	async fn candidates_pending_availability(
@@ -268,7 +278,13 @@ impl RelayChainInterface for RelayChainInProcessInterface {
 		hash: PHash,
 		para_id: ParaId,
 	) -> RelayChainResult<Vec<CommittedCandidateReceipt>> {
-		Ok(self.full_client.runtime_api().candidates_pending_availability(hash, para_id)?)
+		Ok(self
+			.full_client
+			.runtime_api()
+			.candidates_pending_availability(hash, para_id)?
+			.into_iter()
+			.map(|receipt| receipt.into())
+			.collect::<Vec<_>>())
 	}
 }
 
diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs
index cdef39d5bdf1805d1066fdeba9cb09f0037261b1..1f2efdbbb5b38937611299907a1da712a480c30c 100644
--- a/polkadot/node/service/src/fake_runtime_api.rs
+++ b/polkadot/node/service/src/fake_runtime_api.rs
@@ -21,12 +21,16 @@
 
 use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo};
 use polkadot_primitives::{
-	runtime_api, slashing, AccountId, AuthorityDiscoveryId, Balance, Block, BlockNumber,
-	CandidateCommitments, CandidateEvent, CandidateHash, CommittedCandidateReceipt, CoreState,
-	DisputeState, ExecutorParams, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage,
-	InboundHrmpMessage, Nonce, OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement,
-	ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash,
-	ValidatorId, ValidatorIndex, ValidatorSignature,
+	runtime_api, slashing,
+	vstaging::{
+		CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		ScrapedOnChainVotes,
+	},
+	AccountId, AuthorityDiscoveryId, Balance, Block, BlockNumber, CandidateCommitments,
+	CandidateHash, DisputeState, ExecutorParams, GroupRotationInfo, Hash, Id as ParaId,
+	InboundDownwardMessage, InboundHrmpMessage, Nonce, OccupiedCoreAssumption,
+	PersistedValidationData, PvfCheckStatement, SessionIndex, SessionInfo, ValidationCode,
+	ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature,
 };
 use sp_consensus_beefy::ecdsa_crypto::{AuthorityId as BeefyId, Signature as BeefySignature};
 use sp_consensus_grandpa::AuthorityId as GrandpaId;
diff --git a/polkadot/node/subsystem-types/src/runtime_client.rs b/polkadot/node/subsystem-types/src/runtime_client.rs
index e5e1e4d24ef96e8fa7689a41cd7897832b5aa271..7938223df23b38091832c9716c121bbbc0893006 100644
--- a/polkadot/node/subsystem-types/src/runtime_client.rs
+++ b/polkadot/node/subsystem-types/src/runtime_client.rs
@@ -380,7 +380,10 @@ where
 		&self,
 		at: Hash,
 	) -> Result<Vec<CoreState<Hash, BlockNumber>>, ApiError> {
-		self.client.runtime_api().availability_cores(at)
+		self.client
+			.runtime_api()
+			.availability_cores(at)
+			.map(|cores| cores.into_iter().map(|core| core.into()).collect::<Vec<_>>())
 	}
 
 	async fn persisted_validation_data(
@@ -433,7 +436,10 @@ where
 		at: Hash,
 		para_id: Id,
 	) -> Result<Option<CommittedCandidateReceipt<Hash>>, ApiError> {
-		self.client.runtime_api().candidate_pending_availability(at, para_id)
+		self.client
+			.runtime_api()
+			.candidate_pending_availability(at, para_id)
+			.map(|maybe_candidate| maybe_candidate.map(|candidate| candidate.into()))
 	}
 
 	async fn candidates_pending_availability(
@@ -441,11 +447,19 @@ where
 		at: Hash,
 		para_id: Id,
 	) -> Result<Vec<CommittedCandidateReceipt<Hash>>, ApiError> {
-		self.client.runtime_api().candidates_pending_availability(at, para_id)
+		self.client
+			.runtime_api()
+			.candidates_pending_availability(at, para_id)
+			.map(|candidates| {
+				candidates.into_iter().map(|candidate| candidate.into()).collect::<Vec<_>>()
+			})
 	}
 
 	async fn candidate_events(&self, at: Hash) -> Result<Vec<CandidateEvent<Hash>>, ApiError> {
-		self.client.runtime_api().candidate_events(at)
+		self.client
+			.runtime_api()
+			.candidate_events(at)
+			.map(|events| events.into_iter().map(|event| event.into()).collect::<Vec<_>>())
 	}
 
 	async fn dmq_contents(
@@ -476,7 +490,10 @@ where
 		&self,
 		at: Hash,
 	) -> Result<Option<ScrapedOnChainVotes<Hash>>, ApiError> {
-		self.client.runtime_api().on_chain_votes(at)
+		self.client
+			.runtime_api()
+			.on_chain_votes(at)
+			.map(|maybe_votes| maybe_votes.map(|votes| votes.into()))
 	}
 
 	async fn session_executor_params(
@@ -588,7 +605,12 @@ where
 		at: Hash,
 		para_id: Id,
 	) -> Result<Option<async_backing::BackingState>, ApiError> {
-		self.client.runtime_api().para_backing_state(at, para_id)
+		self.client
+			.runtime_api()
+			.para_backing_state(at, para_id)
+			.map(|maybe_backing_state| {
+				maybe_backing_state.map(|backing_state| backing_state.into())
+			})
 	}
 
 	async fn async_backing_params(
diff --git a/polkadot/node/test/client/src/block_builder.rs b/polkadot/node/test/client/src/block_builder.rs
index 71bcdaffac4e7e69c7614abe4c51a50bd1e8556e..9375aca6ed7340bce68dc8b74da6a01708c24d29 100644
--- a/polkadot/node/test/client/src/block_builder.rs
+++ b/polkadot/node/test/client/src/block_builder.rs
@@ -16,7 +16,7 @@
 
 use crate::Client;
 use codec::{Decode, Encode};
-use polkadot_primitives::{Block, InherentData as ParachainsInherentData};
+use polkadot_primitives::{vstaging::InherentData as ParachainsInherentData, Block};
 use polkadot_test_runtime::UncheckedExtrinsic;
 use polkadot_test_service::GetLastTimestamp;
 use sc_block_builder::{BlockBuilder, BlockBuilderBuilder};
diff --git a/polkadot/primitives/Cargo.toml b/polkadot/primitives/Cargo.toml
index 8f7ec314ecffe6d7ca7f1f943a47971b4a1e9558..a8cd6cb5f4e00c36a3236f58c3ec72922d8b4d43 100644
--- a/polkadot/primitives/Cargo.toml
+++ b/polkadot/primitives/Cargo.toml
@@ -28,10 +28,14 @@ sp-consensus-slots = { features = ["serde"], workspace = true }
 sp-io = { workspace = true }
 sp-keystore = { optional = true, workspace = true }
 sp-staking = { features = ["serde"], workspace = true }
+sp-std = { workspace = true, optional = true }
 
 polkadot-core-primitives = { workspace = true }
 polkadot-parachain-primitives = { workspace = true }
 
+[dev-dependencies]
+polkadot-primitives-test-helpers = { workspace = true }
+
 [features]
 default = ["std"]
 std = [
@@ -54,9 +58,11 @@ std = [
 	"sp-keystore?/std",
 	"sp-runtime/std",
 	"sp-staking/std",
+	"sp-std/std",
 ]
 runtime-benchmarks = [
 	"polkadot-parachain-primitives/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
 	"sp-staking/runtime-benchmarks",
 ]
+test = []
diff --git a/polkadot/primitives/src/runtime_api.rs b/polkadot/primitives/src/runtime_api.rs
index b4816ad15075dcc8d739259b779bfda1a9b5c84a..ddebe99e6214e4393e7ea589ecaa5203f917dcd9 100644
--- a/polkadot/primitives/src/runtime_api.rs
+++ b/polkadot/primitives/src/runtime_api.rs
@@ -114,11 +114,15 @@
 //! separated from the stable primitives.
 
 use crate::{
-	async_backing, slashing, ApprovalVotingParams, AsyncBackingParams, BlockNumber,
-	CandidateCommitments, CandidateEvent, CandidateHash, CommittedCandidateReceipt, CoreIndex,
-	CoreState, DisputeState, ExecutorParams, GroupRotationInfo, Hash, NodeFeatures,
-	OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement, ScrapedOnChainVotes,
-	SessionIndex, SessionInfo, ValidatorId, ValidatorIndex, ValidatorSignature,
+	slashing,
+	vstaging::{
+		self, CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		ScrapedOnChainVotes,
+	},
+	ApprovalVotingParams, AsyncBackingParams, BlockNumber, CandidateCommitments, CandidateHash,
+	CoreIndex, DisputeState, ExecutorParams, GroupRotationInfo, Hash, NodeFeatures,
+	OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement, SessionIndex, SessionInfo,
+	ValidatorId, ValidatorIndex, ValidatorSignature,
 };
 
 use alloc::{
@@ -260,7 +264,7 @@ sp_api::decl_runtime_apis! {
 
 		/// Returns the state of parachain backing for a given para.
 		#[api_version(7)]
-		fn para_backing_state(_: ppp::Id) -> Option<async_backing::BackingState<Hash, BlockNumber>>;
+		fn para_backing_state(_: ppp::Id) -> Option<vstaging::async_backing::BackingState<Hash, BlockNumber>>;
 
 		/// Returns candidate's acceptance limitations for asynchronous backing for a relay parent.
 		#[api_version(7)]
diff --git a/polkadot/primitives/src/v8/mod.rs b/polkadot/primitives/src/v8/mod.rs
index b6928e372064621e64dbac2af346491f6bb10afc..a51ee0bd99bfe9d2c8db17b1db5cdfadce489277 100644
--- a/polkadot/primitives/src/v8/mod.rs
+++ b/polkadot/primitives/src/v8/mod.rs
@@ -15,7 +15,6 @@
 // along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
 
 //! `V7` Primitives.
-
 use alloc::{
 	vec,
 	vec::{IntoIter, Vec},
@@ -1157,7 +1156,7 @@ pub enum OccupiedCoreAssumption {
 	Free,
 }
 
-/// An even concerning a candidate.
+/// An event concerning a candidate.
 #[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
 #[cfg_attr(feature = "std", derive(PartialEq))]
 pub enum CandidateEvent<H = Hash> {
@@ -2130,7 +2129,7 @@ impl<BlockNumber: Default + From<u32>> Default for SchedulerParams<BlockNumber>
 }
 
 #[cfg(test)]
-mod tests {
+pub mod tests {
 	use super::*;
 	use bitvec::bitvec;
 	use sp_core::sr25519;
diff --git a/polkadot/primitives/src/vstaging/async_backing.rs b/polkadot/primitives/src/vstaging/async_backing.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8706214b5a0109b779140c4ffba7d52cbaada62e
--- /dev/null
+++ b/polkadot/primitives/src/vstaging/async_backing.rs
@@ -0,0 +1,76 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+use super::*;
+
+use alloc::vec::Vec;
+use codec::{Decode, Encode};
+use scale_info::TypeInfo;
+use sp_core::RuntimeDebug;
+
+/// A candidate pending availability.
+#[derive(RuntimeDebug, Clone, PartialEq, Encode, Decode, TypeInfo)]
+pub struct CandidatePendingAvailability<H = Hash, N = BlockNumber> {
+	/// The hash of the candidate.
+	pub candidate_hash: CandidateHash,
+	/// The candidate's descriptor.
+	pub descriptor: CandidateDescriptorV2<H>,
+	/// The commitments of the candidate.
+	pub commitments: CandidateCommitments,
+	/// The candidate's relay parent's number.
+	pub relay_parent_number: N,
+	/// The maximum Proof-of-Validity size allowed, in bytes.
+	pub max_pov_size: u32,
+}
+
+impl<H: Copy> From<CandidatePendingAvailability<H>>
+	for crate::v8::async_backing::CandidatePendingAvailability<H>
+{
+	fn from(value: CandidatePendingAvailability<H>) -> Self {
+		Self {
+			candidate_hash: value.candidate_hash,
+			descriptor: value.descriptor.into(),
+			commitments: value.commitments,
+			relay_parent_number: value.relay_parent_number,
+			max_pov_size: value.max_pov_size,
+		}
+	}
+}
+
+/// The per-parachain state of the backing system, including
+/// state-machine constraints and candidates pending availability.
+#[derive(RuntimeDebug, Clone, PartialEq, Encode, Decode, TypeInfo)]
+pub struct BackingState<H = Hash, N = BlockNumber> {
+	/// The state-machine constraints of the parachain.
+	pub constraints: Constraints<N>,
+	/// The candidates pending availability. These should be ordered, i.e. they should form
+	/// a sub-chain, where the first candidate builds on top of the required parent of the
+	/// constraints and each subsequent builds on top of the previous head-data.
+	pub pending_availability: Vec<CandidatePendingAvailability<H, N>>,
+}
+
+impl<H: Copy> From<BackingState<H>> for crate::v8::async_backing::BackingState<H> {
+	fn from(value: BackingState<H>) -> Self {
+		Self {
+			constraints: value.constraints,
+			pending_availability: value
+				.pending_availability
+				.into_iter()
+				.map(|candidate| candidate.into())
+				.collect::<Vec<_>>(),
+		}
+	}
+}
diff --git a/polkadot/primitives/src/vstaging/mod.rs b/polkadot/primitives/src/vstaging/mod.rs
index 1429b0c326aceef4b9088bd4ddef6828f8dcfbd8..57cba85c10d945079618a4ed7344c8066c03a147 100644
--- a/polkadot/primitives/src/vstaging/mod.rs
+++ b/polkadot/primitives/src/vstaging/mod.rs
@@ -15,5 +15,913 @@
 // along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
 
 //! Staging Primitives.
+use crate::{ValidatorIndex, ValidityAttestation};
 
 // Put any primitives used by staging APIs functions here
+use super::{
+	async_backing::Constraints, BlakeTwo256, BlockNumber, CandidateCommitments,
+	CandidateDescriptor, CandidateHash, CollatorId, CollatorSignature, CoreIndex, GroupIndex, Hash,
+	HashT, HeadData, Header, Id, Id as ParaId, MultiDisputeStatementSet, ScheduledCore,
+	UncheckedSignedAvailabilityBitfields, ValidationCodeHash,
+};
+use bitvec::prelude::*;
+use sp_application_crypto::ByteArray;
+
+use alloc::{vec, vec::Vec};
+use codec::{Decode, Encode};
+use scale_info::TypeInfo;
+use sp_core::RuntimeDebug;
+use sp_runtime::traits::Header as HeaderT;
+use sp_staking::SessionIndex;
+/// Async backing primitives
+pub mod async_backing;
+
+/// A type representing the version of the candidate descriptor and internal version number.
+#[derive(PartialEq, Eq, Encode, Decode, Clone, TypeInfo, RuntimeDebug, Copy)]
+#[cfg_attr(feature = "std", derive(Hash))]
+pub struct InternalVersion(pub u8);
+
+/// A type representing the version of the candidate descriptor.
+#[derive(PartialEq, Eq, Clone, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(Hash))]
+pub enum CandidateDescriptorVersion {
+	/// The old candidate descriptor version.
+	V1,
+	/// The new `CandidateDescriptorV2`.
+	V2,
+	/// An unknown version.
+	Unknown,
+}
+
+/// A unique descriptor of the candidate receipt.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(Hash))]
+pub struct CandidateDescriptorV2<H = Hash> {
+	/// The ID of the para this is a candidate for.
+	para_id: ParaId,
+	/// The hash of the relay-chain block this is executed in the context of.
+	relay_parent: H,
+	/// Version field. The raw value here is not exposed, instead it is used
+	/// to determine the `CandidateDescriptorVersion`, see `fn version()`.
+	/// For the current version this field is set to `0` and will be incremented
+	/// by next versions.
+	version: InternalVersion,
+	/// The core index where the candidate is backed.
+	core_index: u16,
+	/// The session index of the candidate relay parent.
+	session_index: SessionIndex,
+	/// Reserved bytes.
+	reserved1: [u8; 25],
+	/// The blake2-256 hash of the persisted validation data. This is extra data derived from
+	/// relay-chain state which may vary based on bitfields included before the candidate.
+	/// Thus it cannot be derived entirely from the relay-parent.
+	persisted_validation_data_hash: Hash,
+	/// The blake2-256 hash of the PoV.
+	pov_hash: Hash,
+	/// The root of a block's erasure encoding Merkle tree.
+	erasure_root: Hash,
+	/// Reserved bytes.
+	reserved2: [u8; 64],
+	/// Hash of the para header that is being generated by this candidate.
+	para_head: Hash,
+	/// The blake2-256 hash of the validation code bytes.
+	validation_code_hash: ValidationCodeHash,
+}
+
+impl<H: Copy> From<CandidateDescriptorV2<H>> for CandidateDescriptor<H> {
+	fn from(value: CandidateDescriptorV2<H>) -> Self {
+		Self {
+			para_id: value.para_id,
+			relay_parent: value.relay_parent,
+			collator: value.rebuild_collator_field(),
+			persisted_validation_data_hash: value.persisted_validation_data_hash,
+			pov_hash: value.pov_hash,
+			erasure_root: value.erasure_root,
+			signature: value.rebuild_signature_field(),
+			para_head: value.para_head,
+			validation_code_hash: value.validation_code_hash,
+		}
+	}
+}
+
+#[cfg(any(feature = "runtime-benchmarks", feature = "test"))]
+impl<H: Encode + Decode + Copy> From<CandidateDescriptor<H>> for CandidateDescriptorV2<H> {
+	fn from(value: CandidateDescriptor<H>) -> Self {
+		Decode::decode(&mut value.encode().as_slice()).unwrap()
+	}
+}
+
+impl<H> CandidateDescriptorV2<H> {
+	/// Constructor
+	pub fn new(
+		para_id: Id,
+		relay_parent: H,
+		core_index: CoreIndex,
+		session_index: SessionIndex,
+		persisted_validation_data_hash: Hash,
+		pov_hash: Hash,
+		erasure_root: Hash,
+		para_head: Hash,
+		validation_code_hash: ValidationCodeHash,
+	) -> Self {
+		Self {
+			para_id,
+			relay_parent,
+			version: InternalVersion(0),
+			core_index: core_index.0 as u16,
+			session_index,
+			reserved1: [0; 25],
+			persisted_validation_data_hash,
+			pov_hash,
+			erasure_root,
+			reserved2: [0; 64],
+			para_head,
+			validation_code_hash,
+		}
+	}
+
+	/// Set the PoV size in the descriptor. Only for tests.
+	#[cfg(feature = "test")]
+	pub fn set_pov_hash(&mut self, pov_hash: Hash) {
+		self.pov_hash = pov_hash;
+	}
+
+	/// Set the version in the descriptor. Only for tests.
+	#[cfg(feature = "test")]
+	pub fn set_version(&mut self, version: InternalVersion) {
+		self.version = version;
+	}
+}
+
+/// A candidate-receipt at version 2.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(Hash))]
+pub struct CandidateReceiptV2<H = Hash> {
+	/// The descriptor of the candidate.
+	pub descriptor: CandidateDescriptorV2<H>,
+	/// The hash of the encoded commitments made as a result of candidate execution.
+	pub commitments_hash: Hash,
+}
+
+/// A candidate-receipt with commitments directly included.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(Hash))]
+pub struct CommittedCandidateReceiptV2<H = Hash> {
+	/// The descriptor of the candidate.
+	pub descriptor: CandidateDescriptorV2<H>,
+	/// The commitments of the candidate receipt.
+	pub commitments: CandidateCommitments,
+}
+
+/// An event concerning a candidate.
+#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(PartialEq))]
+pub enum CandidateEvent<H = Hash> {
+	/// This candidate receipt was backed in the most recent block.
+	/// This includes the core index the candidate is now occupying.
+	#[codec(index = 0)]
+	CandidateBacked(CandidateReceiptV2<H>, HeadData, CoreIndex, GroupIndex),
+	/// This candidate receipt was included and became a parablock at the most recent block.
+	/// This includes the core index the candidate was occupying as well as the group responsible
+	/// for backing the candidate.
+	#[codec(index = 1)]
+	CandidateIncluded(CandidateReceiptV2<H>, HeadData, CoreIndex, GroupIndex),
+	/// This candidate receipt was not made available in time and timed out.
+	/// This includes the core index the candidate was occupying.
+	#[codec(index = 2)]
+	CandidateTimedOut(CandidateReceiptV2<H>, HeadData, CoreIndex),
+}
+
+impl<H: Encode + Copy> From<CandidateEvent<H>> for super::v8::CandidateEvent<H> {
+	fn from(value: CandidateEvent<H>) -> Self {
+		match value {
+			CandidateEvent::CandidateBacked(receipt, head_data, core_index, group_index) =>
+				super::v8::CandidateEvent::CandidateBacked(
+					receipt.into(),
+					head_data,
+					core_index,
+					group_index,
+				),
+			CandidateEvent::CandidateIncluded(receipt, head_data, core_index, group_index) =>
+				super::v8::CandidateEvent::CandidateIncluded(
+					receipt.into(),
+					head_data,
+					core_index,
+					group_index,
+				),
+			CandidateEvent::CandidateTimedOut(receipt, head_data, core_index) =>
+				super::v8::CandidateEvent::CandidateTimedOut(receipt.into(), head_data, core_index),
+		}
+	}
+}
+
+impl<H> CandidateReceiptV2<H> {
+	/// Get a reference to the candidate descriptor.
+	pub fn descriptor(&self) -> &CandidateDescriptorV2<H> {
+		&self.descriptor
+	}
+
+	/// Computes the blake2-256 hash of the receipt.
+	pub fn hash(&self) -> CandidateHash
+	where
+		H: Encode,
+	{
+		CandidateHash(BlakeTwo256::hash_of(self))
+	}
+}
+
+impl<H: Clone> CommittedCandidateReceiptV2<H> {
+	/// Transforms this into a plain `CandidateReceipt`.
+	pub fn to_plain(&self) -> CandidateReceiptV2<H> {
+		CandidateReceiptV2 {
+			descriptor: self.descriptor.clone(),
+			commitments_hash: self.commitments.hash(),
+		}
+	}
+
+	/// Computes the hash of the committed candidate receipt.
+	///
+	/// This computes the canonical hash, not the hash of the directly encoded data.
+	/// Thus this is a shortcut for `candidate.to_plain().hash()`.
+	pub fn hash(&self) -> CandidateHash
+	where
+		H: Encode,
+	{
+		self.to_plain().hash()
+	}
+
+	/// Does this committed candidate receipt corresponds to the given [`CandidateReceiptV2`]?
+	pub fn corresponds_to(&self, receipt: &CandidateReceiptV2<H>) -> bool
+	where
+		H: PartialEq,
+	{
+		receipt.descriptor == self.descriptor && receipt.commitments_hash == self.commitments.hash()
+	}
+}
+
+impl PartialOrd for CommittedCandidateReceiptV2 {
+	fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
+		Some(self.cmp(other))
+	}
+}
+
+impl Ord for CommittedCandidateReceiptV2 {
+	fn cmp(&self, other: &Self) -> core::cmp::Ordering {
+		self.descriptor
+			.para_id
+			.cmp(&other.descriptor.para_id)
+			.then_with(|| self.commitments.head_data.cmp(&other.commitments.head_data))
+	}
+}
+
+impl<H: Copy> From<CommittedCandidateReceiptV2<H>> for super::v8::CommittedCandidateReceipt<H> {
+	fn from(value: CommittedCandidateReceiptV2<H>) -> Self {
+		Self { descriptor: value.descriptor.into(), commitments: value.commitments }
+	}
+}
+
+impl<H: Copy> From<CandidateReceiptV2<H>> for super::v8::CandidateReceipt<H> {
+	fn from(value: CandidateReceiptV2<H>) -> Self {
+		Self { descriptor: value.descriptor.into(), commitments_hash: value.commitments_hash }
+	}
+}
+
+/// A strictly increasing sequence number, typically this would be the least significant byte of the
+/// block number.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+pub struct CoreSelector(pub u8);
+
+/// An offset in the relay chain claim queue.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+pub struct ClaimQueueOffset(pub u8);
+
+/// Signals that a parachain can send to the relay chain via the UMP queue.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+pub enum UMPSignal {
+	/// A message sent by a parachain to select the core the candidate is commited to.
+	/// Relay chain validators, in particular backers, use the `CoreSelector` and
+	/// `ClaimQueueOffset` to compute the index of the core the candidate has commited to.
+	SelectCore(CoreSelector, ClaimQueueOffset),
+}
+/// Separator between `XCM` and `UMPSignal`.
+pub const UMP_SEPARATOR: Vec<u8> = vec![];
+
+impl CandidateCommitments {
+	/// Returns the core selector and claim queue offset the candidate has committed to, if any.
+	pub fn selected_core(&self) -> Option<(CoreSelector, ClaimQueueOffset)> {
+		// We need at least 2 messages for the separator and core selector
+		if self.upward_messages.len() < 2 {
+			return None
+		}
+
+		let separator_pos =
+			self.upward_messages.iter().rposition(|message| message == &UMP_SEPARATOR)?;
+
+		// Use first commitment
+		let message = self.upward_messages.get(separator_pos + 1)?;
+
+		match UMPSignal::decode(&mut message.as_slice()).ok()? {
+			UMPSignal::SelectCore(core_selector, cq_offset) => Some((core_selector, cq_offset)),
+		}
+	}
+}
+
+/// CandidateReceipt construction errors.
+#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+pub enum CandidateReceiptError {
+	/// The specified core index is invalid.
+	InvalidCoreIndex,
+	/// The core index in commitments doesn't match the one in descriptor
+	CoreIndexMismatch,
+	/// The core selector or claim queue offset is invalid.
+	InvalidSelectedCore,
+	/// The parachain is not assigned to any core at specified claim queue offset.
+	NoAssignment,
+	/// No core was selected.
+	NoCoreSelected,
+	/// Unknown version.
+	UnknownVersion(InternalVersion),
+}
+
+macro_rules! impl_getter {
+	($field:ident, $type:ident) => {
+		/// Returns the value of $field field.
+		pub fn $field(&self) -> $type {
+			self.$field
+		}
+	};
+}
+
+impl<H: Copy> CandidateDescriptorV2<H> {
+	impl_getter!(erasure_root, Hash);
+	impl_getter!(para_head, Hash);
+	impl_getter!(relay_parent, H);
+	impl_getter!(para_id, ParaId);
+	impl_getter!(persisted_validation_data_hash, Hash);
+	impl_getter!(pov_hash, Hash);
+	impl_getter!(validation_code_hash, ValidationCodeHash);
+
+	/// Returns the candidate descriptor version.
+	/// The candidate is at version 2 if the reserved fields are zeroed out
+	/// and the internal `version` field is 0.
+	pub fn version(&self) -> CandidateDescriptorVersion {
+		if self.reserved2 != [0u8; 64] || self.reserved1 != [0u8; 25] {
+			return CandidateDescriptorVersion::V1
+		}
+
+		match self.version.0 {
+			0 => CandidateDescriptorVersion::V2,
+			_ => CandidateDescriptorVersion::Unknown,
+		}
+	}
+
+	fn rebuild_collator_field(&self) -> CollatorId {
+		let mut collator_id = Vec::with_capacity(32);
+		let core_index: [u8; 2] = self.core_index.to_ne_bytes();
+		let session_index: [u8; 4] = self.session_index.to_ne_bytes();
+
+		collator_id.push(self.version.0);
+		collator_id.extend_from_slice(core_index.as_slice());
+		collator_id.extend_from_slice(session_index.as_slice());
+		collator_id.extend_from_slice(self.reserved1.as_slice());
+
+		CollatorId::from_slice(&collator_id.as_slice())
+			.expect("Slice size is exactly 32 bytes; qed")
+	}
+
+	/// Returns the collator id if this is a v1 `CandidateDescriptor`
+	pub fn collator(&self) -> Option<CollatorId> {
+		if self.version() == CandidateDescriptorVersion::V1 {
+			Some(self.rebuild_collator_field())
+		} else {
+			None
+		}
+	}
+
+	fn rebuild_signature_field(&self) -> CollatorSignature {
+		CollatorSignature::from_slice(self.reserved2.as_slice())
+			.expect("Slice size is exactly 64 bytes; qed")
+	}
+
+	/// Returns the collator signature of `V1` candidate descriptors, `None` otherwise.
+	pub fn signature(&self) -> Option<CollatorSignature> {
+		if self.version() == CandidateDescriptorVersion::V1 {
+			return Some(self.rebuild_signature_field())
+		}
+
+		None
+	}
+
+	/// Returns the `core_index` of `V2` candidate descriptors, `None` otherwise.
+	pub fn core_index(&self) -> Option<CoreIndex> {
+		if self.version() == CandidateDescriptorVersion::V1 {
+			return None
+		}
+
+		Some(CoreIndex(self.core_index as u32))
+	}
+
+	/// Returns the `core_index` of `V2` candidate descriptors, `None` otherwise.
+	pub fn session_index(&self) -> Option<SessionIndex> {
+		if self.version() == CandidateDescriptorVersion::V1 {
+			return None
+		}
+
+		Some(self.session_index)
+	}
+}
+
+impl<H: Copy> CommittedCandidateReceiptV2<H> {
+	/// Checks if descriptor core index is equal to the commited core index.
+	/// Input `assigned_cores` must contain the sorted cores assigned to the para at
+	/// the committed claim queue offset.
+	pub fn check(&self, assigned_cores: &[CoreIndex]) -> Result<(), CandidateReceiptError> {
+		// Don't check v1 descriptors.
+		if self.descriptor.version() == CandidateDescriptorVersion::V1 {
+			return Ok(())
+		}
+
+		if self.descriptor.version() == CandidateDescriptorVersion::Unknown {
+			return Err(CandidateReceiptError::UnknownVersion(self.descriptor.version))
+		}
+
+		if assigned_cores.is_empty() {
+			return Err(CandidateReceiptError::NoAssignment)
+		}
+
+		let descriptor_core_index = CoreIndex(self.descriptor.core_index as u32);
+
+		let (core_selector, _cq_offset) =
+			self.commitments.selected_core().ok_or(CandidateReceiptError::NoCoreSelected)?;
+
+		let core_index = assigned_cores
+			.get(core_selector.0 as usize % assigned_cores.len())
+			.ok_or(CandidateReceiptError::InvalidCoreIndex)?;
+
+		if *core_index != descriptor_core_index {
+			return Err(CandidateReceiptError::CoreIndexMismatch)
+		}
+
+		Ok(())
+	}
+}
+
+/// A backed (or backable, depending on context) candidate.
+#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
+pub struct BackedCandidate<H = Hash> {
+	/// The candidate referred to.
+	candidate: CommittedCandidateReceiptV2<H>,
+	/// The validity votes themselves, expressed as signatures.
+	validity_votes: Vec<ValidityAttestation>,
+	/// The indices of the validators within the group, expressed as a bitfield. May be extended
+	/// beyond the backing group size to contain the assigned core index, if ElasticScalingMVP is
+	/// enabled.
+	validator_indices: BitVec<u8, bitvec::order::Lsb0>,
+}
+
+/// Parachains inherent-data passed into the runtime by a block author
+#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug, TypeInfo)]
+pub struct InherentData<HDR: HeaderT = Header> {
+	/// Signed bitfields by validators about availability.
+	pub bitfields: UncheckedSignedAvailabilityBitfields,
+	/// Backed candidates for inclusion in the block.
+	pub backed_candidates: Vec<BackedCandidate<HDR::Hash>>,
+	/// Sets of dispute votes for inclusion,
+	pub disputes: MultiDisputeStatementSet,
+	/// The parent block header. Used for checking state proofs.
+	pub parent_header: HDR,
+}
+
+impl<H> BackedCandidate<H> {
+	/// Constructor
+	pub fn new(
+		candidate: CommittedCandidateReceiptV2<H>,
+		validity_votes: Vec<ValidityAttestation>,
+		validator_indices: BitVec<u8, bitvec::order::Lsb0>,
+		core_index: Option<CoreIndex>,
+	) -> Self {
+		let mut instance = Self { candidate, validity_votes, validator_indices };
+		if let Some(core_index) = core_index {
+			instance.inject_core_index(core_index);
+		}
+		instance
+	}
+
+	/// Get a reference to the committed candidate receipt of the candidate.
+	pub fn candidate(&self) -> &CommittedCandidateReceiptV2<H> {
+		&self.candidate
+	}
+
+	/// Get a reference to the descriptor of the candidate.
+	pub fn descriptor(&self) -> &CandidateDescriptorV2<H> {
+		&self.candidate.descriptor
+	}
+
+	/// Get a mutable reference to the descriptor of the candidate. Only for testing.
+	#[cfg(feature = "test")]
+	pub fn descriptor_mut(&mut self) -> &mut CandidateDescriptorV2<H> {
+		&mut self.candidate.descriptor
+	}
+
+	/// Get a reference to the validity votes of the candidate.
+	pub fn validity_votes(&self) -> &[ValidityAttestation] {
+		&self.validity_votes
+	}
+
+	/// Get a mutable reference to validity votes of the para.
+	pub fn validity_votes_mut(&mut self) -> &mut Vec<ValidityAttestation> {
+		&mut self.validity_votes
+	}
+
+	/// Compute this candidate's hash.
+	pub fn hash(&self) -> CandidateHash
+	where
+		H: Clone + Encode,
+	{
+		self.candidate.to_plain().hash()
+	}
+
+	/// Get this candidate's receipt.
+	pub fn receipt(&self) -> CandidateReceiptV2<H>
+	where
+		H: Clone,
+	{
+		self.candidate.to_plain()
+	}
+
+	/// Get a copy of the validator indices and the assumed core index, if any.
+	pub fn validator_indices_and_core_index(
+		&self,
+		core_index_enabled: bool,
+	) -> (&BitSlice<u8, bitvec::order::Lsb0>, Option<CoreIndex>) {
+		// This flag tells us if the block producers must enable Elastic Scaling MVP hack.
+		// It extends `BackedCandidate::validity_indices` to store a 8 bit core index.
+		if core_index_enabled {
+			let core_idx_offset = self.validator_indices.len().saturating_sub(8);
+			if core_idx_offset > 0 {
+				let (validator_indices_slice, core_idx_slice) =
+					self.validator_indices.split_at(core_idx_offset);
+				return (
+					validator_indices_slice,
+					Some(CoreIndex(core_idx_slice.load::<u8>() as u32)),
+				);
+			}
+		}
+
+		(&self.validator_indices, None)
+	}
+
+	/// Inject a core index in the validator_indices bitvec.
+	fn inject_core_index(&mut self, core_index: CoreIndex) {
+		let core_index_to_inject: BitVec<u8, bitvec::order::Lsb0> =
+			BitVec::from_vec(vec![core_index.0 as u8]);
+		self.validator_indices.extend(core_index_to_inject);
+	}
+
+	/// Update the validator indices and core index in the candidate.
+	pub fn set_validator_indices_and_core_index(
+		&mut self,
+		new_indices: BitVec<u8, bitvec::order::Lsb0>,
+		maybe_core_index: Option<CoreIndex>,
+	) {
+		self.validator_indices = new_indices;
+
+		if let Some(core_index) = maybe_core_index {
+			self.inject_core_index(core_index);
+		}
+	}
+}
+
+/// Scraped runtime backing votes and resolved disputes.
+#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
+#[cfg_attr(feature = "std", derive(PartialEq))]
+pub struct ScrapedOnChainVotes<H: Encode + Decode = Hash> {
+	/// The session in which the block was included.
+	pub session: SessionIndex,
+	/// Set of backing validators for each candidate, represented by its candidate
+	/// receipt.
+	pub backing_validators_per_candidate:
+		Vec<(CandidateReceiptV2<H>, Vec<(ValidatorIndex, ValidityAttestation)>)>,
+	/// On-chain-recorded set of disputes.
+	/// Note that the above `backing_validators` are
+	/// unrelated to the backers of the disputes candidates.
+	pub disputes: MultiDisputeStatementSet,
+}
+
+impl<H: Encode + Decode + Copy> From<ScrapedOnChainVotes<H>> for super::v8::ScrapedOnChainVotes<H> {
+	fn from(value: ScrapedOnChainVotes<H>) -> Self {
+		Self {
+			session: value.session,
+			backing_validators_per_candidate: value
+				.backing_validators_per_candidate
+				.into_iter()
+				.map(|(receipt, validators)| (receipt.into(), validators))
+				.collect::<Vec<_>>(),
+			disputes: value.disputes,
+		}
+	}
+}
+
+/// Information about a core which is currently occupied.
+#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(PartialEq))]
+pub struct OccupiedCore<H = Hash, N = BlockNumber> {
+	// NOTE: this has no ParaId as it can be deduced from the candidate descriptor.
+	/// If this core is freed by availability, this is the assignment that is next up on this
+	/// core, if any. None if there is nothing queued for this core.
+	pub next_up_on_available: Option<ScheduledCore>,
+	/// The relay-chain block number this began occupying the core at.
+	pub occupied_since: N,
+	/// The relay-chain block this will time-out at, if any.
+	pub time_out_at: N,
+	/// If this core is freed by being timed-out, this is the assignment that is next up on this
+	/// core. None if there is nothing queued for this core or there is no possibility of timing
+	/// out.
+	pub next_up_on_time_out: Option<ScheduledCore>,
+	/// A bitfield with 1 bit for each validator in the set. `1` bits mean that the corresponding
+	/// validators has attested to availability on-chain. A 2/3+ majority of `1` bits means that
+	/// this will be available.
+	pub availability: BitVec<u8, bitvec::order::Lsb0>,
+	/// The group assigned to distribute availability pieces of this candidate.
+	pub group_responsible: GroupIndex,
+	/// The hash of the candidate occupying the core.
+	pub candidate_hash: CandidateHash,
+	/// The descriptor of the candidate occupying the core.
+	pub candidate_descriptor: CandidateDescriptorV2<H>,
+}
+
+/// The state of a particular availability core.
+#[derive(Clone, Encode, Decode, TypeInfo, RuntimeDebug)]
+#[cfg_attr(feature = "std", derive(PartialEq))]
+pub enum CoreState<H = Hash, N = BlockNumber> {
+	/// The core is currently occupied.
+	#[codec(index = 0)]
+	Occupied(OccupiedCore<H, N>),
+	/// The core is currently free, with a para scheduled and given the opportunity
+	/// to occupy.
+	///
+	/// If a particular Collator is required to author this block, that is also present in this
+	/// variant.
+	#[codec(index = 1)]
+	Scheduled(ScheduledCore),
+	/// The core is currently free and there is nothing scheduled. This can be the case for
+	/// parathread cores when there are no parathread blocks queued. Parachain cores will never be
+	/// left idle.
+	#[codec(index = 2)]
+	Free,
+}
+
+impl<H: Copy> From<OccupiedCore<H>> for super::v8::OccupiedCore<H> {
+	fn from(value: OccupiedCore<H>) -> Self {
+		Self {
+			next_up_on_available: value.next_up_on_available,
+			occupied_since: value.occupied_since,
+			time_out_at: value.time_out_at,
+			next_up_on_time_out: value.next_up_on_time_out,
+			availability: value.availability,
+			group_responsible: value.group_responsible,
+			candidate_hash: value.candidate_hash,
+			candidate_descriptor: value.candidate_descriptor.into(),
+		}
+	}
+}
+
+impl<H: Copy> From<CoreState<H>> for super::v8::CoreState<H> {
+	fn from(value: CoreState<H>) -> Self {
+		match value {
+			CoreState::Free => super::v8::CoreState::Free,
+			CoreState::Scheduled(core) => super::v8::CoreState::Scheduled(core),
+			CoreState::Occupied(occupied_core) =>
+				super::v8::CoreState::Occupied(occupied_core.into()),
+		}
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::{
+		v8::{
+			tests::dummy_committed_candidate_receipt as dummy_old_committed_candidate_receipt,
+			CommittedCandidateReceipt, Hash, HeadData, ValidationCode,
+		},
+		vstaging::{CandidateDescriptorV2, CommittedCandidateReceiptV2},
+	};
+
+	fn dummy_collator_signature() -> CollatorSignature {
+		CollatorSignature::from_slice(&mut (0..64).into_iter().collect::<Vec<_>>().as_slice())
+			.expect("64 bytes; qed")
+	}
+
+	fn dummy_collator_id() -> CollatorId {
+		CollatorId::from_slice(&mut (0..32).into_iter().collect::<Vec<_>>().as_slice())
+			.expect("32 bytes; qed")
+	}
+
+	pub fn dummy_committed_candidate_receipt_v2() -> CommittedCandidateReceiptV2 {
+		let zeros = Hash::zero();
+		let reserved2 = [0; 64];
+
+		CommittedCandidateReceiptV2 {
+			descriptor: CandidateDescriptorV2 {
+				para_id: 0.into(),
+				relay_parent: zeros,
+				version: InternalVersion(0),
+				core_index: 123,
+				session_index: 1,
+				reserved1: Default::default(),
+				persisted_validation_data_hash: zeros,
+				pov_hash: zeros,
+				erasure_root: zeros,
+				reserved2,
+				para_head: zeros,
+				validation_code_hash: ValidationCode(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]).hash(),
+			},
+			commitments: CandidateCommitments {
+				head_data: HeadData(vec![]),
+				upward_messages: vec![].try_into().expect("empty vec fits within bounds"),
+				new_validation_code: None,
+				horizontal_messages: vec![].try_into().expect("empty vec fits within bounds"),
+				processed_downward_messages: 0,
+				hrmp_watermark: 0_u32,
+			},
+		}
+	}
+
+	#[test]
+	fn is_binary_compatibile() {
+		let old_ccr = dummy_old_committed_candidate_receipt();
+		let new_ccr = dummy_committed_candidate_receipt_v2();
+
+		assert_eq!(old_ccr.encoded_size(), new_ccr.encoded_size());
+
+		let encoded_old = old_ccr.encode();
+
+		// Deserialize from old candidate receipt.
+		let new_ccr: CommittedCandidateReceiptV2 =
+			Decode::decode(&mut encoded_old.as_slice()).unwrap();
+
+		// We get same candidate hash.
+		assert_eq!(old_ccr.hash(), new_ccr.hash());
+	}
+
+	#[test]
+	fn invalid_version_descriptor() {
+		let mut new_ccr = dummy_committed_candidate_receipt_v2();
+		assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::V2);
+		// Put some unknown version.
+		new_ccr.descriptor.version = InternalVersion(100);
+
+		// Deserialize as V1.
+		let new_ccr: CommittedCandidateReceiptV2 =
+			Decode::decode(&mut new_ccr.encode().as_slice()).unwrap();
+
+		assert_eq!(new_ccr.descriptor.version(), CandidateDescriptorVersion::Unknown);
+		assert_eq!(
+			new_ccr.check(&vec![].as_slice()),
+			Err(CandidateReceiptError::UnknownVersion(InternalVersion(100)))
+		)
+	}
+
+	#[test]
+	fn test_ump_commitment() {
+		let mut new_ccr = dummy_committed_candidate_receipt_v2();
+		new_ccr.descriptor.core_index = 123;
+		new_ccr.descriptor.para_id = ParaId::new(1000);
+
+		// dummy XCM messages
+		new_ccr.commitments.upward_messages.force_push(vec![0u8; 256]);
+		new_ccr.commitments.upward_messages.force_push(vec![0xff; 256]);
+
+		// separator
+		new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR);
+
+		// CoreIndex commitment
+		new_ccr
+			.commitments
+			.upward_messages
+			.force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode());
+
+		assert_eq!(new_ccr.check(&vec![CoreIndex(123)]), Ok(()));
+	}
+
+	#[test]
+	fn test_invalid_ump_commitment() {
+		let mut new_ccr = dummy_committed_candidate_receipt_v2();
+		new_ccr.descriptor.core_index = 0;
+		new_ccr.descriptor.para_id = ParaId::new(1000);
+
+		new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR);
+		new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR);
+
+		// The check should fail because no `SelectCore` signal was sent.
+		assert_eq!(
+			new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]),
+			Err(CandidateReceiptError::NoCoreSelected)
+		);
+
+		// Garbage message.
+		new_ccr.commitments.upward_messages.force_push(vec![0, 13, 200].encode());
+
+		// No `SelectCore` can be decoded.
+		assert_eq!(new_ccr.commitments.selected_core(), None);
+
+		// Failure is expected.
+		assert_eq!(
+			new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]),
+			Err(CandidateReceiptError::NoCoreSelected)
+		);
+
+		new_ccr.commitments.upward_messages.clear();
+		new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR);
+
+		new_ccr
+			.commitments
+			.upward_messages
+			.force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode());
+
+		// Duplicate
+		new_ccr
+			.commitments
+			.upward_messages
+			.force_push(UMPSignal::SelectCore(CoreSelector(1), ClaimQueueOffset(1)).encode());
+
+		// Duplicate doesn't override first signal.
+		assert_eq!(new_ccr.check(&vec![CoreIndex(0), CoreIndex(100)]), Ok(()));
+	}
+
+	#[test]
+	fn test_version2_receipts_decoded_as_v1() {
+		let mut new_ccr = dummy_committed_candidate_receipt_v2();
+		new_ccr.descriptor.core_index = 123;
+		new_ccr.descriptor.para_id = ParaId::new(1000);
+
+		// dummy XCM messages
+		new_ccr.commitments.upward_messages.force_push(vec![0u8; 256]);
+		new_ccr.commitments.upward_messages.force_push(vec![0xff; 256]);
+
+		// separator
+		new_ccr.commitments.upward_messages.force_push(UMP_SEPARATOR);
+
+		// CoreIndex commitment
+		new_ccr
+			.commitments
+			.upward_messages
+			.force_push(UMPSignal::SelectCore(CoreSelector(0), ClaimQueueOffset(1)).encode());
+
+		let encoded_ccr = new_ccr.encode();
+		let decoded_ccr: CommittedCandidateReceipt =
+			Decode::decode(&mut encoded_ccr.as_slice()).unwrap();
+
+		assert_eq!(decoded_ccr.descriptor.relay_parent, new_ccr.descriptor.relay_parent());
+		assert_eq!(decoded_ccr.descriptor.para_id, new_ccr.descriptor.para_id());
+
+		assert_eq!(new_ccr.hash(), decoded_ccr.hash());
+
+		// Encode v1 and decode as V2
+		let encoded_ccr = new_ccr.encode();
+		let v2_ccr: CommittedCandidateReceiptV2 =
+			Decode::decode(&mut encoded_ccr.as_slice()).unwrap();
+
+		assert_eq!(v2_ccr.descriptor.core_index(), Some(CoreIndex(123)));
+		assert_eq!(new_ccr.check(&vec![CoreIndex(123)]), Ok(()));
+
+		assert_eq!(new_ccr.hash(), v2_ccr.hash());
+	}
+
+	#[test]
+	fn test_core_select_is_mandatory() {
+		// Testing edge case when collators provide zeroed signature and collator id.
+		let mut old_ccr = dummy_old_committed_candidate_receipt();
+		old_ccr.descriptor.para_id = ParaId::new(1000);
+		let encoded_ccr: Vec<u8> = old_ccr.encode();
+
+		let new_ccr: CommittedCandidateReceiptV2 =
+			Decode::decode(&mut encoded_ccr.as_slice()).unwrap();
+
+		// Since collator sig and id are zeroed, it means that the descriptor uses format
+		// version 2.
+		// We expect the check to fail in such case because there will be no `SelectCore`
+		// commitment.
+		assert_eq!(new_ccr.check(&vec![CoreIndex(0)]), Err(CandidateReceiptError::NoCoreSelected));
+
+		// Adding collator signature should make it decode as v1.
+		old_ccr.descriptor.signature = dummy_collator_signature();
+		old_ccr.descriptor.collator = dummy_collator_id();
+
+		let old_ccr_hash = old_ccr.hash();
+
+		let encoded_ccr: Vec<u8> = old_ccr.encode();
+
+		let new_ccr: CommittedCandidateReceiptV2 =
+			Decode::decode(&mut encoded_ccr.as_slice()).unwrap();
+
+		assert_eq!(new_ccr.descriptor.signature(), Some(old_ccr.descriptor.signature));
+		assert_eq!(new_ccr.descriptor.collator(), Some(old_ccr.descriptor.collator));
+
+		assert_eq!(new_ccr.descriptor.core_index(), None);
+		assert_eq!(new_ccr.descriptor.para_id(), ParaId::new(1000));
+
+		assert_eq!(old_ccr_hash, new_ccr.hash());
+	}
+}
diff --git a/polkadot/primitives/test-helpers/src/lib.rs b/polkadot/primitives/test-helpers/src/lib.rs
index d43cf3317e573adffb21b8fdc077eeebf993d40d..b0f78717dd97538758e735eb61f21821c17ed7e6 100644
--- a/polkadot/primitives/test-helpers/src/lib.rs
+++ b/polkadot/primitives/test-helpers/src/lib.rs
@@ -23,9 +23,10 @@
 //! Note that `dummy_` prefixed values are meant to be fillers, that should not matter, and will
 //! contain randomness based data.
 use polkadot_primitives::{
+	vstaging::{CandidateDescriptorV2, CandidateReceiptV2, CommittedCandidateReceiptV2},
 	CandidateCommitments, CandidateDescriptor, CandidateReceipt, CollatorId, CollatorSignature,
-	CommittedCandidateReceipt, Hash, HeadData, Id as ParaId, PersistedValidationData,
-	ValidationCode, ValidationCodeHash, ValidatorId,
+	CommittedCandidateReceipt, CoreIndex, Hash, HeadData, Id as ParaId, PersistedValidationData,
+	SessionIndex, ValidationCode, ValidationCodeHash, ValidatorId,
 };
 pub use rand;
 use sp_application_crypto::sr25519;
@@ -42,6 +43,14 @@ pub fn dummy_candidate_receipt<H: AsRef<[u8]>>(relay_parent: H) -> CandidateRece
 	}
 }
 
+/// Creates a v2 candidate receipt with filler data.
+pub fn dummy_candidate_receipt_v2<H: AsRef<[u8]>>(relay_parent: H) -> CandidateReceiptV2<H> {
+	CandidateReceiptV2::<H> {
+		commitments_hash: dummy_candidate_commitments(dummy_head_data()).hash(),
+		descriptor: dummy_candidate_descriptor_v2(relay_parent),
+	}
+}
+
 /// Creates a committed candidate receipt with filler data.
 pub fn dummy_committed_candidate_receipt<H: AsRef<[u8]>>(
 	relay_parent: H,
@@ -52,6 +61,16 @@ pub fn dummy_committed_candidate_receipt<H: AsRef<[u8]>>(
 	}
 }
 
+/// Creates a v2 committed candidate receipt with filler data.
+pub fn dummy_committed_candidate_receipt_v2<H: AsRef<[u8]>>(
+	relay_parent: H,
+) -> CommittedCandidateReceiptV2<H> {
+	CommittedCandidateReceiptV2 {
+		descriptor: dummy_candidate_descriptor_v2::<H>(relay_parent),
+		commitments: dummy_candidate_commitments(dummy_head_data()),
+	}
+}
+
 /// Create a candidate receipt with a bogus signature and filler data. Optionally set the commitment
 /// hash with the `commitments` arg.
 pub fn dummy_candidate_receipt_bad_sig(
@@ -124,6 +143,23 @@ pub fn dummy_candidate_descriptor<H: AsRef<[u8]>>(relay_parent: H) -> CandidateD
 	descriptor
 }
 
+/// Create a v2 candidate descriptor with filler data.
+pub fn dummy_candidate_descriptor_v2<H: AsRef<[u8]>>(relay_parent: H) -> CandidateDescriptorV2<H> {
+	let invalid = Hash::zero();
+	let descriptor = make_valid_candidate_descriptor_v2(
+		1.into(),
+		relay_parent,
+		CoreIndex(1),
+		1,
+		invalid,
+		invalid,
+		invalid,
+		invalid,
+		invalid,
+	);
+	descriptor
+}
+
 /// Create meaningless validation code.
 pub fn dummy_validation_code() -> ValidationCode {
 	ValidationCode(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])
@@ -134,16 +170,16 @@ pub fn dummy_head_data() -> HeadData {
 	HeadData(vec![])
 }
 
-/// Create a meaningless collator id.
-pub fn dummy_collator() -> CollatorId {
-	CollatorId::from(sr25519::Public::default())
-}
-
 /// Create a meaningless validator id.
 pub fn dummy_validator() -> ValidatorId {
 	ValidatorId::from(sr25519::Public::default())
 }
 
+/// Create a meaningless collator id.
+pub fn dummy_collator() -> CollatorId {
+	CollatorId::from(sr25519::Public::default())
+}
+
 /// Create a meaningless collator signature.
 pub fn dummy_collator_signature() -> CollatorSignature {
 	CollatorSignature::from(sr25519::Signature::default())
@@ -232,6 +268,34 @@ pub fn make_valid_candidate_descriptor<H: AsRef<[u8]>>(
 	descriptor
 }
 
+/// Create a v2 candidate descriptor.
+pub fn make_valid_candidate_descriptor_v2<H: AsRef<[u8]>>(
+	para_id: ParaId,
+	relay_parent: H,
+	core_index: CoreIndex,
+	session_index: SessionIndex,
+	persisted_validation_data_hash: Hash,
+	pov_hash: Hash,
+	validation_code_hash: impl Into<ValidationCodeHash>,
+	para_head: Hash,
+	erasure_root: Hash,
+) -> CandidateDescriptorV2<H> {
+	let validation_code_hash = validation_code_hash.into();
+
+	let descriptor = CandidateDescriptorV2::new(
+		para_id,
+		relay_parent,
+		core_index,
+		session_index,
+		persisted_validation_data_hash,
+		pov_hash,
+		erasure_root,
+		para_head,
+		validation_code_hash,
+	);
+
+	descriptor
+}
 /// After manually modifying the candidate descriptor, resign with a defined collator key.
 pub fn resign_candidate_descriptor_with_collator<H: AsRef<[u8]>>(
 	descriptor: &mut CandidateDescriptor<H>,
diff --git a/polkadot/runtime/parachains/Cargo.toml b/polkadot/runtime/parachains/Cargo.toml
index cfe373e8cba2e6c186ea62d7121aff5f3d4cafa7..a3eec3f9d961ac2119df82c430178855b212cbb0 100644
--- a/polkadot/runtime/parachains/Cargo.toml
+++ b/polkadot/runtime/parachains/Cargo.toml
@@ -59,6 +59,8 @@ polkadot-runtime-metrics = { workspace = true }
 polkadot-core-primitives = { workspace = true }
 
 [dev-dependencies]
+polkadot-primitives = { workspace = true, features = ["test"] }
+
 futures = { workspace = true }
 hex-literal = { workspace = true, default-features = true }
 sp-keyring = { workspace = true, default-features = true }
diff --git a/polkadot/runtime/parachains/src/builder.rs b/polkadot/runtime/parachains/src/builder.rs
index 65e56881c315f141b4b2898617cb1bfa7b1d4841..665737afa6cb51328a2bd944ec2538d44731b793 100644
--- a/polkadot/runtime/parachains/src/builder.rs
+++ b/polkadot/runtime/parachains/src/builder.rs
@@ -30,32 +30,38 @@ use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
 use frame_support::pallet_prelude::*;
 use frame_system::pallet_prelude::*;
 use polkadot_primitives::{
-	node_features::FeatureIndex, AvailabilityBitfield, BackedCandidate, CandidateCommitments,
-	CandidateDescriptor, CandidateHash, CollatorId, CollatorSignature, CommittedCandidateReceipt,
-	CompactStatement, CoreIndex, DisputeStatement, DisputeStatementSet, GroupIndex, HeadData,
-	Id as ParaId, IndexedVec, InherentData as ParachainsInherentData, InvalidDisputeStatementKind,
+	node_features::FeatureIndex,
+	vstaging::{
+		BackedCandidate, CandidateDescriptorV2,
+		CommittedCandidateReceiptV2 as CommittedCandidateReceipt,
+		InherentData as ParachainsInherentData,
+	},
+	AvailabilityBitfield, CandidateCommitments, CandidateDescriptor, CandidateHash, CollatorId,
+	CollatorSignature, CompactStatement, CoreIndex, DisputeStatement, DisputeStatementSet,
+	GroupIndex, HeadData, Id as ParaId, IndexedVec, InvalidDisputeStatementKind,
 	PersistedValidationData, SessionIndex, SigningContext, UncheckedSigned,
 	ValidDisputeStatementKind, ValidationCode, ValidatorId, ValidatorIndex, ValidityAttestation,
 };
-use sp_core::{sr25519, ByteArray, H256};
+use sp_core::{ByteArray, H256};
 use sp_runtime::{
 	generic::Digest,
 	traits::{Header as HeaderT, One, TrailingZeroInput, Zero},
 	RuntimeAppPublic,
 };
-
-/// Create a null collator id.
-pub fn dummy_collator() -> CollatorId {
-	CollatorId::from_slice(&vec![0u8; 32]).expect("32 bytes; qed")
+fn mock_validation_code() -> ValidationCode {
+	ValidationCode(vec![1, 2, 3])
 }
 
-/// Create a null collator signature.
-pub fn dummy_collator_signature() -> CollatorSignature {
-	CollatorSignature::from_slice(&vec![0u8; 64]).expect("64 bytes; qed")
+// Create a dummy collator id suitable to be used in a V1 candidate descriptor.
+fn junk_collator() -> CollatorId {
+	CollatorId::from_slice(&mut (0..32).into_iter().collect::<Vec<_>>().as_slice())
+		.expect("32 bytes; qed")
 }
 
-fn mock_validation_code() -> ValidationCode {
-	ValidationCode(vec![1, 2, 3])
+// Creates a dummy collator signature suitable to be used in a V1 candidate descriptor.
+fn junk_collator_signature() -> CollatorSignature {
+	CollatorSignature::from_slice(&mut (0..64).into_iter().collect::<Vec<_>>().as_slice())
+		.expect("64 bytes; qed")
 }
 
 /// Grab an account, seeded by a name and index.
@@ -136,6 +142,8 @@ pub(crate) struct BenchBuilder<T: paras_inherent::Config> {
 	fill_claimqueue: bool,
 	/// Cores which should not be available when being populated with pending candidates.
 	unavailable_cores: Vec<u32>,
+	/// Use v2 candidate descriptor.
+	candidate_descriptor_v2: bool,
 	_phantom: core::marker::PhantomData<T>,
 }
 
@@ -167,6 +175,7 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
 			code_upgrade: None,
 			fill_claimqueue: true,
 			unavailable_cores: vec![],
+			candidate_descriptor_v2: false,
 			_phantom: core::marker::PhantomData::<T>,
 		}
 	}
@@ -275,6 +284,12 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
 		self
 	}
 
+	/// Toggle usage of v2 candidate descriptors.
+	pub(crate) fn set_candidate_descriptor_v2(mut self, enable: bool) -> Self {
+		self.candidate_descriptor_v2 = enable;
+		self
+	}
+
 	/// Get the maximum number of validators per core.
 	fn max_validators_per_core(&self) -> u32 {
 		self.max_validators_per_core.unwrap_or(Self::fallback_max_validators_per_core())
@@ -310,18 +325,20 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
 		HeadData(vec![0xFF; max_head_size as usize])
 	}
 
-	fn candidate_descriptor_mock() -> CandidateDescriptor<T::Hash> {
+	fn candidate_descriptor_mock() -> CandidateDescriptorV2<T::Hash> {
+		// Use a v1 descriptor.
 		CandidateDescriptor::<T::Hash> {
 			para_id: 0.into(),
 			relay_parent: Default::default(),
-			collator: CollatorId::from(sr25519::Public::from_raw([42u8; 32])),
+			collator: junk_collator(),
 			persisted_validation_data_hash: Default::default(),
 			pov_hash: Default::default(),
 			erasure_root: Default::default(),
-			signature: CollatorSignature::from(sr25519::Signature::from_raw([42u8; 64])),
+			signature: junk_collator_signature(),
 			para_head: Default::default(),
 			validation_code_hash: mock_validation_code().hash(),
 		}
+		.into()
 	}
 
 	/// Create a mock of `CandidatePendingAvailability`.
@@ -632,18 +649,35 @@ impl<T: paras_inherent::Config> BenchBuilder<T> {
 						let group_validators =
 							scheduler::Pallet::<T>::group_validators(group_idx).unwrap();
 
-						let candidate = CommittedCandidateReceipt::<T::Hash> {
-							descriptor: CandidateDescriptor::<T::Hash> {
+						let descriptor = if self.candidate_descriptor_v2 {
+							CandidateDescriptorV2::new(
+								para_id,
+								relay_parent,
+								core_idx,
+								1,
+								persisted_validation_data_hash,
+								pov_hash,
+								Default::default(),
+								head_data.hash(),
+								validation_code_hash,
+							)
+						} else {
+							CandidateDescriptor::<T::Hash> {
 								para_id,
 								relay_parent,
-								collator: dummy_collator(),
+								collator: junk_collator(),
 								persisted_validation_data_hash,
 								pov_hash,
 								erasure_root: Default::default(),
-								signature: dummy_collator_signature(),
+								signature: junk_collator_signature(),
 								para_head: head_data.hash(),
 								validation_code_hash,
-							},
+							}
+							.into()
+						};
+
+						let candidate = CommittedCandidateReceipt::<T::Hash> {
+							descriptor,
 							commitments: CandidateCommitments::<u32> {
 								upward_messages: Default::default(),
 								horizontal_messages: Default::default(),
diff --git a/polkadot/runtime/parachains/src/inclusion/benchmarking.rs b/polkadot/runtime/parachains/src/inclusion/benchmarking.rs
index 978ef718ea4007e142340b0a4088b4c70ad8043d..cb6329bf88eaed8fe7d1de4a94c588f74b8ac010 100644
--- a/polkadot/runtime/parachains/src/inclusion/benchmarking.rs
+++ b/polkadot/runtime/parachains/src/inclusion/benchmarking.rs
@@ -25,10 +25,9 @@ use bitvec::{bitvec, prelude::Lsb0};
 use frame_benchmarking::benchmarks;
 use pallet_message_queue as mq;
 use polkadot_primitives::{
-	CandidateCommitments, CollatorId, CollatorSignature, CommittedCandidateReceipt, HrmpChannelId,
-	OutboundHrmpMessage, SessionIndex,
+	vstaging::CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CandidateCommitments,
+	HrmpChannelId, OutboundHrmpMessage, SessionIndex,
 };
-use sp_core::sr25519;
 
 fn create_candidate_commitments<T: crate::hrmp::pallet::Config>(
 	para_id: ParaId,
@@ -124,17 +123,17 @@ benchmarks! {
 		let core_index = CoreIndex::from(0);
 		let backing_group = GroupIndex::from(0);
 
-		let descriptor = CandidateDescriptor::<T::Hash> {
-			para_id: para,
-			relay_parent: Default::default(),
-			collator: CollatorId::from(sr25519::Public::from_raw([42u8; 32])),
-			persisted_validation_data_hash: Default::default(),
-			pov_hash: Default::default(),
-			erasure_root: Default::default(),
-			signature: CollatorSignature::from(sr25519::Signature::from_raw([42u8; 64])),
-			para_head: Default::default(),
-			validation_code_hash: ValidationCode(vec![1, 2, 3]).hash(),
-		};
+		let descriptor = CandidateDescriptor::<T::Hash>::new(
+			para,
+			Default::default(),
+			CoreIndex(0),
+			1,
+			Default::default(),
+			Default::default(),
+			Default::default(),
+			Default::default(),
+			ValidationCode(vec![1, 2, 3]).hash(),
+		);
 
 		let receipt = CommittedCandidateReceipt::<T::Hash> {
 			descriptor,
diff --git a/polkadot/runtime/parachains/src/inclusion/migration.rs b/polkadot/runtime/parachains/src/inclusion/migration.rs
index 36a810d341c655cf9309b9b8fec87032a8bc7486..2a215d5d595cf44dfdc8d3701ff021e9e8ac1600 100644
--- a/polkadot/runtime/parachains/src/inclusion/migration.rs
+++ b/polkadot/runtime/parachains/src/inclusion/migration.rs
@@ -20,8 +20,8 @@ pub mod v0 {
 	use frame_support::{storage_alias, Twox64Concat};
 	use frame_system::pallet_prelude::BlockNumberFor;
 	use polkadot_primitives::{
-		AvailabilityBitfield, CandidateCommitments, CandidateDescriptor, CandidateHash, CoreIndex,
-		GroupIndex, Id as ParaId, ValidatorIndex,
+		vstaging::CandidateDescriptorV2 as CandidateDescriptor, AvailabilityBitfield,
+		CandidateCommitments, CandidateHash, CoreIndex, GroupIndex, Id as ParaId, ValidatorIndex,
 	};
 	use scale_info::TypeInfo;
 
@@ -219,7 +219,7 @@ mod tests {
 	use frame_support::traits::UncheckedOnRuntimeUpgrade;
 	use polkadot_primitives::{AvailabilityBitfield, Id as ParaId};
 	use polkadot_primitives_test_helpers::{
-		dummy_candidate_commitments, dummy_candidate_descriptor, dummy_hash,
+		dummy_candidate_commitments, dummy_candidate_descriptor_v2, dummy_hash,
 	};
 
 	#[test]
@@ -235,7 +235,7 @@ mod tests {
 			let mut expected = vec![];
 
 			for i in 1..5 {
-				let descriptor = dummy_candidate_descriptor(dummy_hash());
+				let descriptor = dummy_candidate_descriptor_v2(dummy_hash());
 				v0::PendingAvailability::<Test>::insert(
 					ParaId::from(i),
 					v0::CandidatePendingAvailability {
@@ -285,7 +285,7 @@ mod tests {
 				ParaId::from(6),
 				v0::CandidatePendingAvailability {
 					core: CoreIndex(6),
-					descriptor: dummy_candidate_descriptor(dummy_hash()),
+					descriptor: dummy_candidate_descriptor_v2(dummy_hash()),
 					relay_parent_number: 6,
 					hash: CandidateHash(dummy_hash()),
 					availability_votes: Default::default(),
diff --git a/polkadot/runtime/parachains/src/inclusion/mod.rs b/polkadot/runtime/parachains/src/inclusion/mod.rs
index b1d22996fd1228f07ebb74cf5659efdc1eb2e1dc..e014529ea11a00cec30d94d64bbb72be45d6e3ec 100644
--- a/polkadot/runtime/parachains/src/inclusion/mod.rs
+++ b/polkadot/runtime/parachains/src/inclusion/mod.rs
@@ -44,11 +44,15 @@ use frame_support::{
 use frame_system::pallet_prelude::*;
 use pallet_message_queue::OnQueueChanged;
 use polkadot_primitives::{
-	effective_minimum_backing_votes, supermajority_threshold, well_known_keys, BackedCandidate,
-	CandidateCommitments, CandidateDescriptor, CandidateHash, CandidateReceipt,
-	CommittedCandidateReceipt, CoreIndex, GroupIndex, Hash, HeadData, Id as ParaId,
-	SignedAvailabilityBitfields, SigningContext, UpwardMessage, ValidatorId, ValidatorIndex,
-	ValidityAttestation,
+	effective_minimum_backing_votes, supermajority_threshold,
+	vstaging::{
+		BackedCandidate, CandidateDescriptorV2 as CandidateDescriptor,
+		CandidateReceiptV2 as CandidateReceipt,
+		CommittedCandidateReceiptV2 as CommittedCandidateReceipt,
+	},
+	well_known_keys, CandidateCommitments, CandidateHash, CoreIndex, GroupIndex, Hash, HeadData,
+	Id as ParaId, SignedAvailabilityBitfields, SigningContext, UpwardMessage, ValidatorId,
+	ValidatorIndex, ValidityAttestation,
 };
 use scale_info::TypeInfo;
 use sp_runtime::{traits::One, DispatchError, SaturatedConversion, Saturating};
@@ -764,7 +768,7 @@ impl<T: Config> Pallet<T> {
 
 		let mut backers = bitvec::bitvec![u8, BitOrderLsb0; 0; validators.len()];
 		let signing_context = SigningContext {
-			parent_hash: backed_candidate.descriptor().relay_parent,
+			parent_hash: backed_candidate.descriptor().relay_parent(),
 			session_index: shared::CurrentSessionIndex::<T>::get(),
 		};
 
@@ -880,7 +884,7 @@ impl<T: Config> Pallet<T> {
 			let now = frame_system::Pallet::<T>::block_number();
 
 			paras::Pallet::<T>::schedule_code_upgrade(
-				receipt.descriptor.para_id,
+				receipt.descriptor.para_id(),
 				new_code,
 				now,
 				&config,
@@ -890,19 +894,19 @@ impl<T: Config> Pallet<T> {
 
 		// enact the messaging facet of the candidate.
 		dmp::Pallet::<T>::prune_dmq(
-			receipt.descriptor.para_id,
+			receipt.descriptor.para_id(),
 			commitments.processed_downward_messages,
 		);
 		Self::receive_upward_messages(
-			receipt.descriptor.para_id,
+			receipt.descriptor.para_id(),
 			commitments.upward_messages.as_slice(),
 		);
 		hrmp::Pallet::<T>::prune_hrmp(
-			receipt.descriptor.para_id,
+			receipt.descriptor.para_id(),
 			BlockNumberFor::<T>::from(commitments.hrmp_watermark),
 		);
 		hrmp::Pallet::<T>::queue_outbound_hrmp(
-			receipt.descriptor.para_id,
+			receipt.descriptor.para_id(),
 			commitments.horizontal_messages,
 		);
 
@@ -914,7 +918,7 @@ impl<T: Config> Pallet<T> {
 		));
 
 		paras::Pallet::<T>::note_new_head(
-			receipt.descriptor.para_id,
+			receipt.descriptor.para_id(),
 			commitments.head_data,
 			relay_parent_number,
 		);
@@ -1250,8 +1254,8 @@ impl<T: Config> CandidateCheckContext<T> {
 		backed_candidate_receipt: &CommittedCandidateReceipt<<T as frame_system::Config>::Hash>,
 		parent_head_data: HeadData,
 	) -> Result<BlockNumberFor<T>, Error<T>> {
-		let para_id = backed_candidate_receipt.descriptor().para_id;
-		let relay_parent = backed_candidate_receipt.descriptor().relay_parent;
+		let para_id = backed_candidate_receipt.descriptor.para_id();
+		let relay_parent = backed_candidate_receipt.descriptor.relay_parent();
 
 		// Check that the relay-parent is one of the allowed relay-parents.
 		let (relay_parent_storage_root, relay_parent_number) = {
@@ -1271,7 +1275,7 @@ impl<T: Config> CandidateCheckContext<T> {
 			let expected = persisted_validation_data.hash();
 
 			ensure!(
-				expected == backed_candidate_receipt.descriptor().persisted_validation_data_hash,
+				expected == backed_candidate_receipt.descriptor.persisted_validation_data_hash(),
 				Error::<T>::ValidationDataHashMismatch,
 			);
 		}
@@ -1280,12 +1284,12 @@ impl<T: Config> CandidateCheckContext<T> {
 			// A candidate for a parachain without current validation code is not scheduled.
 			.ok_or_else(|| Error::<T>::UnscheduledCandidate)?;
 		ensure!(
-			backed_candidate_receipt.descriptor().validation_code_hash == validation_code_hash,
+			backed_candidate_receipt.descriptor.validation_code_hash() == validation_code_hash,
 			Error::<T>::InvalidValidationCodeHash,
 		);
 
 		ensure!(
-			backed_candidate_receipt.descriptor().para_head ==
+			backed_candidate_receipt.descriptor.para_head() ==
 				backed_candidate_receipt.commitments.head_data.hash(),
 			Error::<T>::ParaHeadMismatch,
 		);
diff --git a/polkadot/runtime/parachains/src/inclusion/tests.rs b/polkadot/runtime/parachains/src/inclusion/tests.rs
index 95fd66bf8e4fbcd139198955fec3e2eff0b42801..59114e28be16486a527503cb74a3b043b8840402 100644
--- a/polkadot/runtime/parachains/src/inclusion/tests.rs
+++ b/polkadot/runtime/parachains/src/inclusion/tests.rs
@@ -26,22 +26,21 @@ use crate::{
 	shared::AllowedRelayParentsTracker,
 };
 use polkadot_primitives::{
-	effective_minimum_backing_votes, AvailabilityBitfield, SignedAvailabilityBitfields,
-	UncheckedSignedAvailabilityBitfields,
+	effective_minimum_backing_votes, AvailabilityBitfield, CandidateDescriptor,
+	SignedAvailabilityBitfields, UncheckedSignedAvailabilityBitfields,
 };
 
 use assert_matches::assert_matches;
 use codec::DecodeAll;
 use frame_support::assert_noop;
 use polkadot_primitives::{
-	BlockNumber, CandidateCommitments, CandidateDescriptor, CollatorId,
+	BlockNumber, CandidateCommitments, CollatorId, CollatorSignature,
 	CompactStatement as Statement, Hash, SignedAvailabilityBitfield, SignedStatement,
 	ValidationCode, ValidatorId, ValidityAttestation, PARACHAIN_KEY_TYPE_ID,
 };
-use polkadot_primitives_test_helpers::{
-	dummy_collator, dummy_collator_signature, dummy_validation_code,
-};
+use polkadot_primitives_test_helpers::dummy_validation_code;
 use sc_keystore::LocalKeystore;
+use sp_core::ByteArray;
 use sp_keyring::Sr25519Keyring;
 use sp_keystore::{Keystore, KeystorePtr};
 use std::sync::Arc;
@@ -96,24 +95,6 @@ pub(crate) enum BackingKind {
 	Lacking,
 }
 
-pub(crate) fn collator_sign_candidate(
-	collator: Sr25519Keyring,
-	candidate: &mut CommittedCandidateReceipt,
-) {
-	candidate.descriptor.collator = collator.public().into();
-
-	let payload = polkadot_primitives::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) fn back_candidate(
 	candidate: CommittedCandidateReceipt,
 	validators: &[Sr25519Keyring],
@@ -311,9 +292,16 @@ impl TestCandidateBuilder {
 				validation_code_hash: self.validation_code.hash(),
 				para_head: self.para_head_hash.unwrap_or_else(|| self.head_data.hash()),
 				erasure_root: Default::default(),
-				signature: dummy_collator_signature(),
-				collator: dummy_collator(),
-			},
+				signature: CollatorSignature::from_slice(
+					&mut (0..64).into_iter().collect::<Vec<_>>().as_slice(),
+				)
+				.expect("64 bytes; qed"),
+				collator: CollatorId::from_slice(
+					&mut (0..32).into_iter().collect::<Vec<_>>().as_slice(),
+				)
+				.expect("32 bytes; qed"),
+			}
+			.into(),
 			commitments: CandidateCommitments {
 				head_data: self.head_data,
 				new_validation_code: self.new_validation_code,
@@ -1244,7 +1232,7 @@ fn candidate_checks() {
 
 		// Check candidate ordering
 		{
-			let mut candidate_a = TestCandidateBuilder {
+			let candidate_a = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1253,7 +1241,7 @@ fn candidate_checks() {
 				..Default::default()
 			}
 			.build();
-			let mut candidate_b_1 = TestCandidateBuilder {
+			let candidate_b_1 = TestCandidateBuilder {
 				para_id: chain_b,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(2),
@@ -1265,7 +1253,7 @@ fn candidate_checks() {
 			.build();
 
 			// Make candidate b2 a child of b1.
-			let mut candidate_b_2 = TestCandidateBuilder {
+			let candidate_b_2 = TestCandidateBuilder {
 				para_id: chain_b,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(3),
@@ -1281,10 +1269,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
-			collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b_1);
-			collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b_2);
-
 			let backed_a = back_candidate(
 				candidate_a,
 				&validators,
@@ -1356,7 +1340,7 @@ fn candidate_checks() {
 
 			// candidate does not build on top of the latest unincluded head
 
-			let mut candidate_b_3 = TestCandidateBuilder {
+			let candidate_b_3 = TestCandidateBuilder {
 				para_id: chain_b,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(4),
@@ -1371,7 +1355,6 @@ fn candidate_checks() {
 				..Default::default()
 			}
 			.build();
-			collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b_3);
 
 			let backed_b_3 = back_candidate(
 				candidate_b_3,
@@ -1396,7 +1379,7 @@ fn candidate_checks() {
 
 		// candidate not backed.
 		{
-			let mut candidate = TestCandidateBuilder {
+			let candidate = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1405,7 +1388,6 @@ fn candidate_checks() {
 				..Default::default()
 			}
 			.build();
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
 
 			// Insufficient backing.
 			let backed = back_candidate(
@@ -1459,7 +1441,7 @@ fn candidate_checks() {
 			let wrong_parent_hash = Hash::repeat_byte(222);
 			assert!(System::parent_hash() != wrong_parent_hash);
 
-			let mut candidate_a = TestCandidateBuilder {
+			let candidate_a = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: wrong_parent_hash,
 				pov_hash: Hash::repeat_byte(1),
@@ -1468,7 +1450,7 @@ fn candidate_checks() {
 			}
 			.build();
 
-			let mut candidate_b = TestCandidateBuilder {
+			let candidate_b = TestCandidateBuilder {
 				para_id: chain_b,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(2),
@@ -1478,10 +1460,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
-
-			collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_b);
-
 			let backed_a = back_candidate(
 				candidate_a,
 				&validators,
@@ -1531,10 +1509,9 @@ fn candidate_checks() {
 			.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);
+			candidate.descriptor.set_pov_hash(Hash::repeat_byte(2));
 
 			let backed = back_candidate(
 				candidate,
@@ -1597,7 +1574,7 @@ fn candidate_checks() {
 
 		// interfering code upgrade - reject
 		{
-			let mut candidate = TestCandidateBuilder {
+			let candidate = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1608,8 +1585,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 			let backed = back_candidate(
 				candidate,
 				&validators,
@@ -1648,7 +1623,7 @@ fn candidate_checks() {
 
 		// Bad validation data hash - reject
 		{
-			let mut candidate = TestCandidateBuilder {
+			let candidate = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1658,8 +1633,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 			let backed = back_candidate(
 				candidate,
 				&validators,
@@ -1685,7 +1658,7 @@ fn candidate_checks() {
 
 		// bad validation code hash
 		{
-			let mut candidate = TestCandidateBuilder {
+			let candidate = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1696,8 +1669,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 			let backed = back_candidate(
 				candidate,
 				&validators,
@@ -1723,7 +1694,7 @@ fn candidate_checks() {
 
 		// Para head hash in descriptor doesn't match head data
 		{
-			let mut candidate = TestCandidateBuilder {
+			let candidate = TestCandidateBuilder {
 				para_id: chain_a,
 				relay_parent: System::parent_hash(),
 				pov_hash: Hash::repeat_byte(1),
@@ -1734,8 +1705,6 @@ fn candidate_checks() {
 			}
 			.build();
 
-			collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 			let backed = back_candidate(
 				candidate,
 				&validators,
@@ -1826,7 +1795,7 @@ fn backing_works() {
 		let chain_b_assignment = (chain_b, CoreIndex::from(1));
 		let thread_a_assignment = (thread_a, CoreIndex::from(2));
 
-		let mut candidate_a = TestCandidateBuilder {
+		let candidate_a = TestCandidateBuilder {
 			para_id: chain_a,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(1),
@@ -1835,9 +1804,8 @@ fn backing_works() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
 
-		let mut candidate_b = TestCandidateBuilder {
+		let candidate_b = TestCandidateBuilder {
 			para_id: chain_b,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(2),
@@ -1846,9 +1814,8 @@ fn backing_works() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b);
 
-		let mut candidate_c = TestCandidateBuilder {
+		let candidate_c = TestCandidateBuilder {
 			para_id: thread_a,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(3),
@@ -1857,7 +1824,6 @@ fn backing_works() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_c);
 
 		let backed_a = back_candidate(
 			candidate_a.clone(),
@@ -1978,7 +1944,7 @@ fn backing_works() {
 			Vec<(ValidatorIndex, ValidityAttestation)>,
 		)>| {
 			candidate_receipts_with_backers.sort_by(|(cr1, _), (cr2, _)| {
-				cr1.descriptor().para_id.cmp(&cr2.descriptor().para_id)
+				cr1.descriptor().para_id().cmp(&cr2.descriptor().para_id())
 			});
 			candidate_receipts_with_backers
 		};
@@ -2121,7 +2087,7 @@ fn backing_works_with_elastic_scaling_mvp() {
 
 		let allowed_relay_parents = default_allowed_relay_parent_tracker();
 
-		let mut candidate_a = TestCandidateBuilder {
+		let candidate_a = TestCandidateBuilder {
 			para_id: chain_a,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(1),
@@ -2130,9 +2096,8 @@ fn backing_works_with_elastic_scaling_mvp() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
 
-		let mut candidate_b_1 = TestCandidateBuilder {
+		let candidate_b_1 = TestCandidateBuilder {
 			para_id: chain_b,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(2),
@@ -2141,10 +2106,9 @@ fn backing_works_with_elastic_scaling_mvp() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b_1);
 
 		// Make candidate b2 a child of b1.
-		let mut candidate_b_2 = TestCandidateBuilder {
+		let candidate_b_2 = TestCandidateBuilder {
 			para_id: chain_b,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(3),
@@ -2158,7 +2122,6 @@ fn backing_works_with_elastic_scaling_mvp() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b_2);
 
 		let backed_a = back_candidate(
 			candidate_a.clone(),
@@ -2396,7 +2359,7 @@ fn can_include_candidate_with_ok_code_upgrade() {
 
 		let allowed_relay_parents = default_allowed_relay_parent_tracker();
 		let chain_a_assignment = (chain_a, CoreIndex::from(0));
-		let mut candidate_a = TestCandidateBuilder {
+		let candidate_a = TestCandidateBuilder {
 			para_id: chain_a,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(1),
@@ -2406,7 +2369,6 @@ fn can_include_candidate_with_ok_code_upgrade() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
 
 		let backed_a = back_candidate(
 			candidate_a.clone(),
@@ -2556,7 +2518,7 @@ fn check_allowed_relay_parents() {
 		let chain_b_assignment = (chain_b, CoreIndex::from(1));
 		let thread_a_assignment = (thread_a, CoreIndex::from(2));
 
-		let mut candidate_a = TestCandidateBuilder {
+		let candidate_a = TestCandidateBuilder {
 			para_id: chain_a,
 			relay_parent: relay_parent_a.1,
 			pov_hash: Hash::repeat_byte(1),
@@ -2569,10 +2531,9 @@ fn check_allowed_relay_parents() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
 		let signing_context_a = SigningContext { parent_hash: relay_parent_a.1, session_index: 5 };
 
-		let mut candidate_b = TestCandidateBuilder {
+		let candidate_b = TestCandidateBuilder {
 			para_id: chain_b,
 			relay_parent: relay_parent_b.1,
 			pov_hash: Hash::repeat_byte(2),
@@ -2585,10 +2546,9 @@ fn check_allowed_relay_parents() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_b);
 		let signing_context_b = SigningContext { parent_hash: relay_parent_b.1, session_index: 5 };
 
-		let mut candidate_c = TestCandidateBuilder {
+		let candidate_c = TestCandidateBuilder {
 			para_id: thread_a,
 			relay_parent: relay_parent_c.1,
 			pov_hash: Hash::repeat_byte(3),
@@ -2601,7 +2561,6 @@ fn check_allowed_relay_parents() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::Two, &mut candidate_c);
 		let signing_context_c = SigningContext { parent_hash: relay_parent_c.1, session_index: 5 };
 
 		let backed_a = back_candidate(
@@ -2823,7 +2782,7 @@ fn para_upgrade_delay_scheduled_from_inclusion() {
 		let allowed_relay_parents = default_allowed_relay_parent_tracker();
 
 		let chain_a_assignment = (chain_a, CoreIndex::from(0));
-		let mut candidate_a = TestCandidateBuilder {
+		let candidate_a = TestCandidateBuilder {
 			para_id: chain_a,
 			relay_parent: System::parent_hash(),
 			pov_hash: Hash::repeat_byte(1),
@@ -2833,7 +2792,6 @@ fn para_upgrade_delay_scheduled_from_inclusion() {
 			..Default::default()
 		}
 		.build();
-		collator_sign_candidate(Sr25519Keyring::One, &mut candidate_a);
 
 		let backed_a = back_candidate(
 			candidate_a.clone(),
diff --git a/polkadot/runtime/parachains/src/paras_inherent/benchmarking.rs b/polkadot/runtime/parachains/src/paras_inherent/benchmarking.rs
index 1742e91276d4b40bdb0b263dc97f87f962ee2592..fa466de11987332ee59e43639e8a9cf43c1263dc 100644
--- a/polkadot/runtime/parachains/src/paras_inherent/benchmarking.rs
+++ b/polkadot/runtime/parachains/src/paras_inherent/benchmarking.rs
@@ -144,8 +144,8 @@ benchmarks! {
 		// Traverse candidates and assert descriptors are as expected
 		for (para_id, backing_validators) in vote.backing_validators_per_candidate.iter().enumerate() {
 			let descriptor = backing_validators.0.descriptor();
-			assert_eq!(ParaId::from(para_id), descriptor.para_id);
-			assert_eq!(header.hash(), descriptor.relay_parent);
+			assert_eq!(ParaId::from(para_id), descriptor.para_id());
+			assert_eq!(header.hash(), descriptor.relay_parent());
 			assert_eq!(backing_validators.1.len(), votes);
 		}
 
@@ -203,8 +203,8 @@ benchmarks! {
 		for (para_id, backing_validators)
 			in vote.backing_validators_per_candidate.iter().enumerate() {
 				let descriptor = backing_validators.0.descriptor();
-				assert_eq!(ParaId::from(para_id), descriptor.para_id);
-				assert_eq!(header.hash(), descriptor.relay_parent);
+				assert_eq!(ParaId::from(para_id), descriptor.para_id());
+				assert_eq!(header.hash(), descriptor.relay_parent());
 				assert_eq!(
 					backing_validators.1.len(),
 					votes,
diff --git a/polkadot/runtime/parachains/src/paras_inherent/mod.rs b/polkadot/runtime/parachains/src/paras_inherent/mod.rs
index bd8d08a842c31ec7db3bba8ad677de79c0f92b97..84d8299cd29cbfc51a58cfa4644cdc5883694c2c 100644
--- a/polkadot/runtime/parachains/src/paras_inherent/mod.rs
+++ b/polkadot/runtime/parachains/src/paras_inherent/mod.rs
@@ -48,12 +48,17 @@ use frame_support::{
 use frame_system::pallet_prelude::*;
 use pallet_babe::{self, ParentBlockRandomness};
 use polkadot_primitives::{
-	effective_minimum_backing_votes, node_features::FeatureIndex, BackedCandidate, CandidateHash,
-	CandidateReceipt, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex,
-	DisputeStatementSet, HeadData, InherentData as ParachainsInherentData,
-	MultiDisputeStatementSet, ScrapedOnChainVotes, SessionIndex, SignedAvailabilityBitfields,
-	SigningContext, UncheckedSignedAvailabilityBitfield, UncheckedSignedAvailabilityBitfields,
-	ValidatorId, ValidatorIndex, ValidityAttestation, PARACHAINS_INHERENT_IDENTIFIER,
+	effective_minimum_backing_votes,
+	node_features::FeatureIndex,
+	vstaging::{
+		BackedCandidate, CandidateDescriptorVersion, CandidateReceiptV2 as CandidateReceipt,
+		InherentData as ParachainsInherentData, ScrapedOnChainVotes,
+	},
+	CandidateHash, CheckedDisputeStatementSet, CheckedMultiDisputeStatementSet, CoreIndex,
+	DisputeStatementSet, HeadData, MultiDisputeStatementSet, SessionIndex,
+	SignedAvailabilityBitfields, SigningContext, UncheckedSignedAvailabilityBitfield,
+	UncheckedSignedAvailabilityBitfields, ValidatorId, ValidatorIndex, ValidityAttestation,
+	PARACHAINS_INHERENT_IDENTIFIER,
 };
 use rand::{seq::SliceRandom, SeedableRng};
 use scale_info::TypeInfo;
@@ -594,6 +599,12 @@ impl<T: Config> Pallet<T> {
 			.map(|b| *b)
 			.unwrap_or(false);
 
+		let allow_v2_receipts = configuration::ActiveConfig::<T>::get()
+			.node_features
+			.get(FeatureIndex::CandidateReceiptV2 as usize)
+			.map(|b| *b)
+			.unwrap_or(false);
+
 		let mut eligible: BTreeMap<ParaId, BTreeSet<CoreIndex>> = BTreeMap::new();
 		let mut total_eligible_cores = 0;
 
@@ -610,6 +621,7 @@ impl<T: Config> Pallet<T> {
 			concluded_invalid_hashes,
 			eligible,
 			core_index_enabled,
+			allow_v2_receipts,
 		);
 		let count = count_backed_candidates(&backed_candidates_with_core);
 
@@ -787,7 +799,7 @@ pub(crate) fn apply_weight_limit<T: Config + inclusion::Config>(
 	let mut current_para_id = None;
 
 	for candidate in core::mem::take(candidates).into_iter() {
-		let candidate_para_id = candidate.descriptor().para_id;
+		let candidate_para_id = candidate.descriptor().para_id();
 		if Some(candidate_para_id) == current_para_id {
 			let chain = chained_candidates
 				.last_mut()
@@ -966,14 +978,15 @@ pub(crate) fn sanitize_bitfields<T: crate::inclusion::Config>(
 /// subsequent candidates after the filtered one.
 ///
 /// Filter out:
-/// 1. any candidates which don't form a chain with the other candidates of the paraid (even if they
+/// 1. Candidates that have v2 descriptors if the node `CandidateReceiptV2` feature is not enabled.
+/// 2. any candidates which don't form a chain with the other candidates of the paraid (even if they
 ///    do form a chain but are not in the right order).
-/// 2. any candidates that have a concluded invalid dispute or who are descendants of a concluded
+/// 3. any candidates that have a concluded invalid dispute or who are descendants of a concluded
 ///    invalid candidate.
-/// 3. any unscheduled candidates, as well as candidates whose paraid has multiple cores assigned
+/// 4. any unscheduled candidates, as well as candidates whose paraid has multiple cores assigned
 ///    but have no injected core index.
-/// 4. all backing votes from disabled validators
-/// 5. any candidates that end up with less than `effective_minimum_backing_votes` backing votes
+/// 5. all backing votes from disabled validators
+/// 6. any candidates that end up with less than `effective_minimum_backing_votes` backing votes
 ///
 /// Returns the scheduled
 /// backed candidates which passed filtering, mapped by para id and in the right dependency order.
@@ -983,13 +996,28 @@ fn sanitize_backed_candidates<T: crate::inclusion::Config>(
 	concluded_invalid_with_descendants: BTreeSet<CandidateHash>,
 	scheduled: BTreeMap<ParaId, BTreeSet<CoreIndex>>,
 	core_index_enabled: bool,
+	allow_v2_receipts: bool,
 ) -> BTreeMap<ParaId, Vec<(BackedCandidate<T::Hash>, CoreIndex)>> {
 	// Map the candidates to the right paraids, while making sure that the order between candidates
 	// of the same para is preserved.
 	let mut candidates_per_para: BTreeMap<ParaId, Vec<_>> = BTreeMap::new();
 	for candidate in backed_candidates {
+		// Drop any v2 candidate receipts if nodes are not allowed to use them.
+		// It is mandatory to filter these before calling `filter_unchained_candidates` to ensure
+		// any v1 descendants of v2 candidates are dropped.
+		if !allow_v2_receipts && candidate.descriptor().version() == CandidateDescriptorVersion::V2
+		{
+			log::debug!(
+				target: LOG_TARGET,
+				"V2 candidate descriptors not allowed. Dropping candidate {:?} for paraid {:?}.",
+				candidate.candidate().hash(),
+				candidate.descriptor().para_id()
+			);
+			continue
+		}
+
 		candidates_per_para
-			.entry(candidate.descriptor().para_id)
+			.entry(candidate.descriptor().para_id())
 			.or_default()
 			.push(candidate);
 	}
@@ -1008,7 +1036,7 @@ fn sanitize_backed_candidates<T: crate::inclusion::Config>(
 				target: LOG_TARGET,
 				"Found backed candidate {:?} which was concluded invalid or is a descendant of a concluded invalid candidate, for paraid {:?}.",
 				candidate.candidate().hash(),
-				candidate.descriptor().para_id
+				candidate.descriptor().para_id()
 			);
 		}
 		keep
@@ -1189,13 +1217,13 @@ fn filter_backed_statements_from_disabled_validators<
 		// Get relay parent block number of the candidate. We need this to get the group index
 		// assigned to this core at this block number
 		let relay_parent_block_number =
-			match allowed_relay_parents.acquire_info(bc.descriptor().relay_parent, None) {
+			match allowed_relay_parents.acquire_info(bc.descriptor().relay_parent(), None) {
 				Some((_, block_num)) => block_num,
 				None => {
 					log::debug!(
 						target: LOG_TARGET,
 						"Relay parent {:?} for candidate is not in the allowed relay parents. Dropping the candidate.",
-						bc.descriptor().relay_parent
+						bc.descriptor().relay_parent()
 					);
 					return false
 				},
@@ -1396,7 +1424,7 @@ fn map_candidates_to_cores<T: configuration::Config + scheduler::Config + inclus
 						log::debug!(
 							target: LOG_TARGET,
 							"Found enough candidates for paraid: {:?}.",
-							candidate.descriptor().para_id
+							candidate.descriptor().para_id()
 						);
 						break;
 					}
@@ -1415,7 +1443,7 @@ fn map_candidates_to_cores<T: configuration::Config + scheduler::Config + inclus
 								"Found a backed candidate {:?} with injected core index {}, which is not scheduled for paraid {:?}.",
 								candidate.candidate().hash(),
 								core_index.0,
-								candidate.descriptor().para_id
+								candidate.descriptor().para_id()
 							);
 
 							break;
@@ -1429,7 +1457,7 @@ fn map_candidates_to_cores<T: configuration::Config + scheduler::Config + inclus
 							target: LOG_TARGET,
 							"Found a backed candidate {:?} with no injected core index, for paraid {:?} which has multiple scheduled cores.",
 							candidate.candidate().hash(),
-							candidate.descriptor().para_id
+							candidate.descriptor().para_id()
 						);
 
 						break;
@@ -1475,13 +1503,13 @@ fn get_injected_core_index<T: configuration::Config + scheduler::Config + inclus
 	let Some(core_idx) = maybe_core_idx else { return None };
 
 	let relay_parent_block_number =
-		match allowed_relay_parents.acquire_info(candidate.descriptor().relay_parent, None) {
+		match allowed_relay_parents.acquire_info(candidate.descriptor().relay_parent(), None) {
 			Some((_, block_num)) => block_num,
 			None => {
 				log::debug!(
 					target: LOG_TARGET,
 					"Relay parent {:?} for candidate {:?} is not in the allowed relay parents.",
-					candidate.descriptor().relay_parent,
+					candidate.descriptor().relay_parent(),
 					candidate.candidate().hash(),
 				);
 				return None
diff --git a/polkadot/runtime/parachains/src/paras_inherent/tests.rs b/polkadot/runtime/parachains/src/paras_inherent/tests.rs
index e34055bfa9f25ce480c78a04e7e22f387db742c1..ac42ac1611df00ebf35b32ad3a134f9536429a4e 100644
--- a/polkadot/runtime/parachains/src/paras_inherent/tests.rs
+++ b/polkadot/runtime/parachains/src/paras_inherent/tests.rs
@@ -58,7 +58,9 @@ mod enter {
 	use core::panic;
 	use frame_support::assert_ok;
 	use frame_system::limits;
-	use polkadot_primitives::{AvailabilityBitfield, SchedulerParams, UncheckedSigned};
+	use polkadot_primitives::{
+		vstaging::InternalVersion, AvailabilityBitfield, SchedulerParams, UncheckedSigned,
+	};
 	use sp_runtime::Perbill;
 
 	struct TestConfig {
@@ -70,6 +72,7 @@ mod enter {
 		fill_claimqueue: bool,
 		elastic_paras: BTreeMap<u32, u8>,
 		unavailable_cores: Vec<u32>,
+		v2_descriptor: bool,
 	}
 
 	fn make_inherent_data(
@@ -82,6 +85,7 @@ mod enter {
 			fill_claimqueue,
 			elastic_paras,
 			unavailable_cores,
+			v2_descriptor,
 		}: TestConfig,
 	) -> Bench<Test> {
 		let extra_cores = elastic_paras
@@ -99,7 +103,8 @@ mod enter {
 			.set_backed_and_concluding_paras(backed_and_concluding.clone())
 			.set_dispute_sessions(&dispute_sessions[..])
 			.set_fill_claimqueue(fill_claimqueue)
-			.set_unavailable_cores(unavailable_cores);
+			.set_unavailable_cores(unavailable_cores)
+			.set_candidate_descriptor_v2(v2_descriptor);
 
 		// Setup some assignments as needed:
 		mock_assigner::Pallet::<Test>::set_core_count(builder.max_cores());
@@ -145,6 +150,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			// We expect the scenario to have cores 0 & 1 with pending availability. The backed
@@ -240,6 +246,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: [(2, 3)].into_iter().collect(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -344,6 +351,7 @@ mod enter {
 				fill_claimqueue: true,
 				elastic_paras: [(2, 4)].into_iter().collect(),
 				unavailable_cores: unavailable_cores.clone(),
+				v2_descriptor: false,
 			});
 
 			let mut expected_para_inherent_data = scenario.data.clone();
@@ -600,6 +608,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -673,6 +682,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -744,6 +754,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -831,6 +842,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -918,6 +930,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -977,6 +990,7 @@ mod enter {
 				fill_claimqueue: true,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1063,6 +1077,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1170,6 +1185,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1238,6 +1254,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1304,6 +1321,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1407,6 +1425,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let mut para_inherent_data = scenario.data.clone();
@@ -1440,7 +1459,7 @@ mod enter {
 
 			// The chained candidates are not picked, instead a single other candidate is picked
 			assert_eq!(backed_candidates.len(), 1);
-			assert_ne!(backed_candidates[0].descriptor().para_id, ParaId::from(1000));
+			assert_ne!(backed_candidates[0].descriptor().para_id(), ParaId::from(1000));
 
 			// All bitfields are kept.
 			assert_eq!(bitfields.len(), 150);
@@ -1461,9 +1480,9 @@ mod enter {
 			// Only the chained candidates should pass filter.
 			assert_eq!(backed_candidates.len(), 3);
 			// Check the actual candidates
-			assert_eq!(backed_candidates[0].descriptor().para_id, ParaId::from(1000));
-			assert_eq!(backed_candidates[1].descriptor().para_id, ParaId::from(1000));
-			assert_eq!(backed_candidates[2].descriptor().para_id, ParaId::from(1000));
+			assert_eq!(backed_candidates[0].descriptor().para_id(), ParaId::from(1000));
+			assert_eq!(backed_candidates[1].descriptor().para_id(), ParaId::from(1000));
+			assert_eq!(backed_candidates[2].descriptor().para_id(), ParaId::from(1000));
 
 			// All bitfields are kept.
 			assert_eq!(bitfields.len(), 150);
@@ -1496,6 +1515,7 @@ mod enter {
 				fill_claimqueue: false,
 				elastic_paras: BTreeMap::new(),
 				unavailable_cores: vec![],
+				v2_descriptor: false,
 			});
 
 			let expected_para_inherent_data = scenario.data.clone();
@@ -1524,6 +1544,76 @@ mod enter {
 			assert_eq!(dispatch_error, Error::<Test>::InherentOverweight.into());
 		});
 	}
+
+	#[test]
+	fn v2_descriptors_are_filtered() {
+		let config = default_config();
+		assert!(config.configuration.config.scheduler_params.lookahead > 0);
+		new_test_ext(config).execute_with(|| {
+			// Set the elastic scaling MVP feature.
+			configuration::Pallet::<Test>::set_node_feature(
+				RuntimeOrigin::root(),
+				FeatureIndex::ElasticScalingMVP as u8,
+				true,
+			)
+			.unwrap();
+
+			let mut backed_and_concluding = BTreeMap::new();
+			backed_and_concluding.insert(0, 1);
+			backed_and_concluding.insert(1, 1);
+			backed_and_concluding.insert(2, 1);
+
+			let unavailable_cores = vec![];
+
+			let scenario = make_inherent_data(TestConfig {
+				dispute_statements: BTreeMap::new(),
+				dispute_sessions: vec![], // No disputes
+				backed_and_concluding,
+				num_validators_per_core: 5,
+				code_upgrade: None,
+				fill_claimqueue: true,
+				// 8 cores !
+				elastic_paras: [(2, 8)].into_iter().collect(),
+				unavailable_cores: unavailable_cores.clone(),
+				v2_descriptor: true,
+			});
+
+			let mut unfiltered_para_inherent_data = scenario.data.clone();
+
+			// Check the para inherent data is as expected:
+			// * 1 bitfield per validator (5 validators per core, 10 backed candidates)
+			assert_eq!(unfiltered_para_inherent_data.bitfields.len(), 50);
+			// * 10 v2 candidate descriptors.
+			assert_eq!(unfiltered_para_inherent_data.backed_candidates.len(), 10);
+
+			// Make the last candidate look like v1, by using an unknown version.
+			unfiltered_para_inherent_data.backed_candidates[9]
+				.descriptor_mut()
+				.set_version(InternalVersion(123));
+
+			let mut inherent_data = InherentData::new();
+			inherent_data
+				.put_data(PARACHAINS_INHERENT_IDENTIFIER, &unfiltered_para_inherent_data)
+				.unwrap();
+
+			// We expect all backed candidates to be filtered out.
+			let filtered_para_inherend_data =
+				Pallet::<Test>::create_inherent_inner(&inherent_data).unwrap();
+
+			assert_eq!(filtered_para_inherend_data.backed_candidates.len(), 0);
+
+			let dispatch_error = Pallet::<Test>::enter(
+				frame_system::RawOrigin::None.into(),
+				unfiltered_para_inherent_data,
+			)
+			.unwrap_err()
+			.error;
+
+			// We expect `enter` to fail because the inherent data contains backed candidates with
+			// v2 descriptors.
+			assert_eq!(dispatch_error, Error::<Test>::CandidatesFilteredDuringExecution.into());
+		});
+	}
 }
 
 fn default_header() -> polkadot_primitives::Header {
@@ -1540,9 +1630,7 @@ mod sanitizers {
 	use super::*;
 
 	use crate::{
-		inclusion::tests::{
-			back_candidate, collator_sign_candidate, BackingKind, TestCandidateBuilder,
-		},
+		inclusion::tests::{back_candidate, BackingKind, TestCandidateBuilder},
 		mock::new_test_ext,
 	};
 	use bitvec::order::Lsb0;
@@ -1556,7 +1644,6 @@ mod sanitizers {
 	use crate::mock::Test;
 	use polkadot_primitives::PARACHAIN_KEY_TYPE_ID;
 	use sc_keystore::LocalKeystore;
-	use sp_keyring::Sr25519Keyring;
 	use sp_keystore::{Keystore, KeystorePtr};
 	use std::sync::Arc;
 
@@ -1940,7 +2027,7 @@ mod sanitizers {
 				.into_iter()
 				.map(|idx0| {
 					let idx1 = idx0 + 1;
-					let mut candidate = TestCandidateBuilder {
+					let candidate = TestCandidateBuilder {
 						para_id: ParaId::from(idx1),
 						relay_parent,
 						pov_hash: Hash::repeat_byte(idx1 as u8),
@@ -1957,8 +2044,6 @@ mod sanitizers {
 					}
 					.build();
 
-					collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 					let backed = back_candidate(
 						candidate,
 						&validators,
@@ -1991,7 +2076,7 @@ mod sanitizers {
 			let mut expected_backed_candidates_with_core = BTreeMap::new();
 
 			for candidate in backed_candidates.iter() {
-				let para_id = candidate.descriptor().para_id;
+				let para_id = candidate.descriptor().para_id();
 
 				expected_backed_candidates_with_core.entry(para_id).or_insert(vec![]).push((
 					candidate.clone(),
@@ -2177,7 +2262,7 @@ mod sanitizers {
 
 			// Para 1
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(1 as u8),
@@ -2195,8 +2280,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed: BackedCandidate = back_candidate(
 					candidate,
@@ -2215,7 +2298,7 @@ mod sanitizers {
 						.push((backed, CoreIndex(0)));
 				}
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(2 as u8),
@@ -2233,8 +2316,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2255,7 +2336,7 @@ mod sanitizers {
 
 			// Para 2
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(3 as u8),
@@ -2272,8 +2353,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2294,7 +2373,7 @@ mod sanitizers {
 
 			// Para 3
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(3),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(4 as u8),
@@ -2311,8 +2390,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2331,7 +2408,7 @@ mod sanitizers {
 
 			// Para 4
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(4),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(5 as u8),
@@ -2348,8 +2425,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed = back_candidate(
 					candidate,
@@ -2366,7 +2441,7 @@ mod sanitizers {
 					.or_insert(vec![])
 					.push((backed, CoreIndex(5)));
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(4),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(6 as u8),
@@ -2384,8 +2459,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2402,7 +2475,7 @@ mod sanitizers {
 
 			// Para 6.
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(6),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(3 as u8),
@@ -2419,8 +2492,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2435,7 +2506,7 @@ mod sanitizers {
 
 			// Para 7.
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(7),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(3 as u8),
@@ -2452,8 +2523,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2468,7 +2537,7 @@ mod sanitizers {
 
 			// Para 8.
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(8),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(3 as u8),
@@ -2485,8 +2554,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2707,7 +2774,7 @@ mod sanitizers {
 
 			// Para 1
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(1 as u8),
@@ -2725,8 +2792,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let prev_backed: BackedCandidate = back_candidate(
 					candidate,
@@ -2738,7 +2803,7 @@ mod sanitizers {
 					core_index_enabled.then_some(CoreIndex(0 as u32)),
 				);
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(2 as u8),
@@ -2756,8 +2821,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2773,7 +2836,7 @@ mod sanitizers {
 
 			// Para 2.
 			{
-				let mut candidate_1 = TestCandidateBuilder {
+				let candidate_1 = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(3 as u8),
@@ -2791,8 +2854,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate_1);
-
 				let backed_1: BackedCandidate = back_candidate(
 					candidate_1,
 					&validators,
@@ -2811,7 +2872,7 @@ mod sanitizers {
 						.push((backed_1, CoreIndex(2)));
 				}
 
-				let mut candidate_2 = TestCandidateBuilder {
+				let candidate_2 = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(4 as u8),
@@ -2829,8 +2890,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate_2);
-
 				let backed_2 = back_candidate(
 					candidate_2.clone(),
 					&validators,
@@ -2842,7 +2901,7 @@ mod sanitizers {
 				);
 				backed_candidates.push(backed_2.clone());
 
-				let mut candidate_3 = TestCandidateBuilder {
+				let candidate_3 = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(5 as u8),
@@ -2860,8 +2919,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate_3);
-
 				let backed_3 = back_candidate(
 					candidate_3,
 					&validators,
@@ -2876,7 +2933,7 @@ mod sanitizers {
 
 			// Para 3
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(3),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(6 as u8),
@@ -2894,8 +2951,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed: BackedCandidate = back_candidate(
 					candidate,
@@ -2914,7 +2969,7 @@ mod sanitizers {
 						.push((backed, CoreIndex(5)));
 				}
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(3),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(6 as u8),
@@ -2932,8 +2987,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -2954,7 +3007,7 @@ mod sanitizers {
 
 			// Para 4
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(4),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(8 as u8),
@@ -2972,8 +3025,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed: BackedCandidate = back_candidate(
 					candidate.clone(),
 					&validators,
@@ -3210,7 +3261,7 @@ mod sanitizers {
 
 			// Para 1
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(1 as u8),
@@ -3228,8 +3279,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed: BackedCandidate = back_candidate(
 					candidate,
@@ -3248,7 +3297,7 @@ mod sanitizers {
 						.push((backed, CoreIndex(0)));
 				}
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent: prev_relay_parent,
 					pov_hash: Hash::repeat_byte(1 as u8),
@@ -3267,8 +3316,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed = back_candidate(
 					candidate,
@@ -3281,7 +3328,7 @@ mod sanitizers {
 				);
 				backed_candidates.push(backed.clone());
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(1),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(1 as u8),
@@ -3300,8 +3347,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -3316,7 +3361,7 @@ mod sanitizers {
 
 			// Para 2
 			{
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent: prev_relay_parent,
 					pov_hash: Hash::repeat_byte(2 as u8),
@@ -3334,8 +3379,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed: BackedCandidate = back_candidate(
 					candidate,
@@ -3354,7 +3397,7 @@ mod sanitizers {
 						.push((backed, CoreIndex(3)));
 				}
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(2 as u8),
@@ -3373,8 +3416,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let prev_candidate = candidate.clone();
 				let backed = back_candidate(
 					candidate,
@@ -3393,7 +3434,7 @@ mod sanitizers {
 						.push((backed, CoreIndex(4)));
 				}
 
-				let mut candidate = TestCandidateBuilder {
+				let candidate = TestCandidateBuilder {
 					para_id: ParaId::from(2),
 					relay_parent,
 					pov_hash: Hash::repeat_byte(2 as u8),
@@ -3412,8 +3453,6 @@ mod sanitizers {
 				}
 				.build();
 
-				collator_sign_candidate(Sr25519Keyring::One, &mut candidate);
-
 				let backed = back_candidate(
 					candidate,
 					&validators,
@@ -3486,7 +3525,8 @@ mod sanitizers {
 						&shared::AllowedRelayParents::<Test>::get(),
 						BTreeSet::new(),
 						scheduled,
-						core_index_enabled
+						core_index_enabled,
+						false,
 					),
 					expected_backed_candidates_with_core,
 				);
@@ -3510,7 +3550,8 @@ mod sanitizers {
 						&shared::AllowedRelayParents::<Test>::get(),
 						BTreeSet::new(),
 						scheduled,
-						core_index_enabled
+						core_index_enabled,
+						false,
 					),
 					expected_backed_candidates_with_core,
 				);
@@ -3535,6 +3576,7 @@ mod sanitizers {
 						BTreeSet::new(),
 						scheduled,
 						core_index_enabled,
+						false,
 					),
 					expected_backed_candidates_with_core
 				);
@@ -3567,6 +3609,7 @@ mod sanitizers {
 						BTreeSet::new(),
 						scheduled,
 						core_index_enabled,
+						false,
 					),
 					expected_backed_candidates_with_core
 				);
@@ -3607,6 +3650,7 @@ mod sanitizers {
 					BTreeSet::new(),
 					scheduled,
 					core_index_enabled,
+					false,
 				);
 
 				if core_index_enabled {
@@ -3677,6 +3721,7 @@ mod sanitizers {
 					BTreeSet::new(),
 					scheduled,
 					core_index_enabled,
+					false,
 				);
 
 				if core_index_enabled {
@@ -3715,6 +3760,7 @@ mod sanitizers {
 					BTreeSet::new(),
 					scheduled,
 					core_index_enabled,
+					false,
 				);
 
 				assert!(sanitized_backed_candidates.is_empty());
@@ -3751,6 +3797,7 @@ mod sanitizers {
 					set,
 					scheduled,
 					core_index_enabled,
+					false,
 				);
 
 				assert_eq!(sanitized_backed_candidates.len(), backed_candidates.len() / 2);
@@ -3773,9 +3820,9 @@ mod sanitizers {
 				let mut invalid_set = std::collections::BTreeSet::new();
 
 				for (idx, backed_candidate) in backed_candidates.iter().enumerate() {
-					if backed_candidate.descriptor().para_id == ParaId::from(1) && idx == 0 {
+					if backed_candidate.descriptor().para_id() == ParaId::from(1) && idx == 0 {
 						invalid_set.insert(backed_candidate.hash());
-					} else if backed_candidate.descriptor().para_id == ParaId::from(3) {
+					} else if backed_candidate.descriptor().para_id() == ParaId::from(3) {
 						invalid_set.insert(backed_candidate.hash());
 					}
 				}
@@ -3788,6 +3835,7 @@ mod sanitizers {
 					invalid_set,
 					scheduled,
 					true,
+					false,
 				);
 
 				// We'll be left with candidates from paraid 2 and 4.
@@ -3811,7 +3859,7 @@ mod sanitizers {
 				let mut invalid_set = std::collections::BTreeSet::new();
 
 				for (idx, backed_candidate) in backed_candidates.iter().enumerate() {
-					if backed_candidate.descriptor().para_id == ParaId::from(1) && idx == 1 {
+					if backed_candidate.descriptor().para_id() == ParaId::from(1) && idx == 1 {
 						invalid_set.insert(backed_candidate.hash());
 					}
 				}
@@ -3824,6 +3872,7 @@ mod sanitizers {
 					invalid_set,
 					scheduled,
 					true,
+					false,
 				);
 
 				// Only the second candidate of paraid 1 should be removed.
diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/v10.rs b/polkadot/runtime/parachains/src/runtime_api_impl/v10.rs
index 697890232211315af749a365c95fa8cc23425450..ead825b38f079e9789c65eebb1ff7171c4aa2847 100644
--- a/polkadot/runtime/parachains/src/runtime_api_impl/v10.rs
+++ b/polkadot/runtime/parachains/src/runtime_api_impl/v10.rs
@@ -27,15 +27,19 @@ use frame_support::traits::{GetStorageVersion, StorageVersion};
 use frame_system::pallet_prelude::*;
 use polkadot_primitives::{
 	async_backing::{
-		AsyncBackingParams, BackingState, CandidatePendingAvailability, Constraints,
-		InboundHrmpLimitations, OutboundHrmpChannelLimitations,
+		AsyncBackingParams, Constraints, InboundHrmpLimitations, OutboundHrmpChannelLimitations,
 	},
-	slashing, ApprovalVotingParams, AuthorityDiscoveryId, CandidateEvent, CandidateHash,
-	CommittedCandidateReceipt, CoreIndex, CoreState, DisputeState, ExecutorParams, GroupIndex,
-	GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage,
-	NodeFeatures, OccupiedCore, OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement,
-	ScrapedOnChainVotes, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash,
-	ValidatorId, ValidatorIndex, ValidatorSignature,
+	slashing,
+	vstaging::{
+		async_backing::{BackingState, CandidatePendingAvailability},
+		CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		OccupiedCore, ScrapedOnChainVotes,
+	},
+	ApprovalVotingParams, AuthorityDiscoveryId, CandidateHash, CoreIndex, DisputeState,
+	ExecutorParams, GroupIndex, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage,
+	InboundHrmpMessage, NodeFeatures, OccupiedCoreAssumption, PersistedValidationData,
+	PvfCheckStatement, SessionIndex, SessionInfo, ValidationCode, ValidationCodeHash, ValidatorId,
+	ValidatorIndex, ValidatorSignature,
 };
 use sp_runtime::traits::One;
 
diff --git a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs
index 4aa381e33b1bc2246abd3c16044cc56290597b12..a3440f686e94306399659f844ef31c720e77cad8 100644
--- a/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs
+++ b/polkadot/runtime/parachains/src/runtime_api_impl/vstaging.rs
@@ -21,7 +21,9 @@ use alloc::{
 	collections::{btree_map::BTreeMap, vec_deque::VecDeque},
 	vec::Vec,
 };
-use polkadot_primitives::{CommittedCandidateReceipt, CoreIndex, Id as ParaId};
+use polkadot_primitives::{
+	vstaging::CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreIndex, Id as ParaId,
+};
 use sp_runtime::traits::One;
 
 /// Returns the claimqueue from the scheduler
diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs
index dfc41b15bb1dc4468ecab0e0cceb4c09d3f6efd8..6b046e1908309971e276af2391472bc19d5a22d0 100644
--- a/polkadot/runtime/rococo/src/lib.rs
+++ b/polkadot/runtime/rococo/src/lib.rs
@@ -35,12 +35,16 @@ use frame_support::{
 };
 use pallet_nis::WithMaximumOf;
 use polkadot_primitives::{
-	slashing, AccountId, AccountIndex, ApprovalVotingParams, Balance, BlockNumber, CandidateEvent,
-	CandidateHash, CommittedCandidateReceipt, CoreIndex, CoreState, DisputeState, ExecutorParams,
-	GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, Moment,
-	NodeFeatures, Nonce, OccupiedCoreAssumption, PersistedValidationData, ScrapedOnChainVotes,
-	SessionInfo, Signature, ValidationCode, ValidationCodeHash, ValidatorId, ValidatorIndex,
-	PARACHAIN_KEY_TYPE_ID,
+	slashing,
+	vstaging::{
+		CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		ScrapedOnChainVotes,
+	},
+	AccountId, AccountIndex, ApprovalVotingParams, Balance, BlockNumber, CandidateHash, CoreIndex,
+	DisputeState, ExecutorParams, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage,
+	InboundHrmpMessage, Moment, NodeFeatures, Nonce, OccupiedCoreAssumption,
+	PersistedValidationData, SessionInfo, Signature, ValidationCode, ValidationCodeHash,
+	ValidatorId, ValidatorIndex, PARACHAIN_KEY_TYPE_ID,
 };
 use polkadot_runtime_common::{
 	assigned_slots, auctions, claims, crowdloan, identity_migrator, impl_runtime_weights,
@@ -2032,7 +2036,7 @@ sp_api::impl_runtime_apis! {
 			parachains_runtime_api_impl::minimum_backing_votes::<Runtime>()
 		}
 
-		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::async_backing::BackingState> {
+		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::vstaging::async_backing::BackingState> {
 			parachains_runtime_api_impl::backing_state::<Runtime>(para_id)
 		}
 
diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs
index 8e34320d38f2f26edc800343cf37217977e8bad1..72d024e9a8787b349a2091ae6326215bb6805d3d 100644
--- a/polkadot/runtime/test-runtime/src/lib.rs
+++ b/polkadot/runtime/test-runtime/src/lib.rs
@@ -59,10 +59,14 @@ use pallet_session::historical as session_historical;
 use pallet_timestamp::Now;
 use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo};
 use polkadot_primitives::{
-	slashing, AccountId, AccountIndex, Balance, BlockNumber, CandidateEvent, CandidateHash,
-	CommittedCandidateReceipt, CoreIndex, CoreState, DisputeState, ExecutorParams,
-	GroupRotationInfo, Hash as HashT, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage,
-	Moment, Nonce, OccupiedCoreAssumption, PersistedValidationData, ScrapedOnChainVotes,
+	slashing,
+	vstaging::{
+		CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		ScrapedOnChainVotes,
+	},
+	AccountId, AccountIndex, Balance, BlockNumber, CandidateHash, CoreIndex, DisputeState,
+	ExecutorParams, GroupRotationInfo, Hash as HashT, Id as ParaId, InboundDownwardMessage,
+	InboundHrmpMessage, Moment, Nonce, OccupiedCoreAssumption, PersistedValidationData,
 	SessionInfo as SessionInfoData, Signature, ValidationCode, ValidationCodeHash, ValidatorId,
 	ValidatorIndex, PARACHAIN_KEY_TYPE_ID,
 };
@@ -978,7 +982,7 @@ sp_api::impl_runtime_apis! {
 			runtime_impl::minimum_backing_votes::<Runtime>()
 		}
 
-		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::async_backing::BackingState> {
+		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::vstaging::async_backing::BackingState> {
 			runtime_impl::backing_state::<Runtime>(para_id)
 		}
 
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index e8fe11615d743a914aceaaa3445d9c6f1940054d..b02c2d8c671ea14ed186881ae271868f71012093 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -49,12 +49,16 @@ use pallet_identity::legacy::IdentityInfo;
 use pallet_session::historical as session_historical;
 use pallet_transaction_payment::{FeeDetails, FungibleAdapter, RuntimeDispatchInfo};
 use polkadot_primitives::{
-	slashing, AccountId, AccountIndex, ApprovalVotingParams, Balance, BlockNumber, CandidateEvent,
-	CandidateHash, CommittedCandidateReceipt, CoreIndex, CoreState, DisputeState, ExecutorParams,
-	GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage, InboundHrmpMessage, Moment,
-	NodeFeatures, Nonce, OccupiedCoreAssumption, PersistedValidationData, PvfCheckStatement,
-	ScrapedOnChainVotes, SessionInfo, Signature, ValidationCode, ValidationCodeHash, ValidatorId,
-	ValidatorIndex, ValidatorSignature, PARACHAIN_KEY_TYPE_ID,
+	slashing,
+	vstaging::{
+		CandidateEvent, CommittedCandidateReceiptV2 as CommittedCandidateReceipt, CoreState,
+		ScrapedOnChainVotes,
+	},
+	AccountId, AccountIndex, ApprovalVotingParams, Balance, BlockNumber, CandidateHash, CoreIndex,
+	DisputeState, ExecutorParams, GroupRotationInfo, Hash, Id as ParaId, InboundDownwardMessage,
+	InboundHrmpMessage, Moment, NodeFeatures, Nonce, OccupiedCoreAssumption,
+	PersistedValidationData, PvfCheckStatement, SessionInfo, Signature, ValidationCode,
+	ValidationCodeHash, ValidatorId, ValidatorIndex, ValidatorSignature, PARACHAIN_KEY_TYPE_ID,
 };
 use polkadot_runtime_common::{
 	assigned_slots, auctions, crowdloan,
@@ -2064,7 +2068,7 @@ sp_api::impl_runtime_apis! {
 			parachains_runtime_api_impl::minimum_backing_votes::<Runtime>()
 		}
 
-		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::async_backing::BackingState> {
+		fn para_backing_state(para_id: ParaId) -> Option<polkadot_primitives::vstaging::async_backing::BackingState> {
 			parachains_runtime_api_impl::backing_state::<Runtime>(para_id)
 		}
 
diff --git a/prdoc/pr_5322.prdoc b/prdoc/pr_5322.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..b4cf261f33a4d7cabea2b5e22529451aa79ca856
--- /dev/null
+++ b/prdoc/pr_5322.prdoc
@@ -0,0 +1,30 @@
+title: Elastic scaling - introduce new candidate receipt primitive
+
+doc:
+  - audience: [Runtime Dev, Node Dev]
+    description: |
+      Introduces `CandidateDescriptorV2` primitive as described in [RFC 103](https://github.com/polkadot-fellows/RFCs/pull/103).
+      Updates parachains runtime, Westend, Rococo and test runtimes to use the new primitives. 
+      This change does not implement the functionality of the new candidate receipts.
+
+crates: 
+- name: polkadot-primitives
+  bump: minor
+- name: polkadot-primitives-test-helpers
+  bump: minor
+- name: polkadot-runtime-parachains
+  bump: major
+- name: rococo-runtime
+  bump: major
+- name: westend-runtime
+  bump: major
+- name: polkadot-test-runtime
+  bump: major
+- name: polkadot-service
+  bump: patch
+- name: polkadot-node-subsystem-types
+  bump: patch
+- name: polkadot-test-client
+  bump: major
+- name: cumulus-relay-chain-inprocess-interface
+  bump: patch