diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs
index 36ef8d5719548609af36bcbbe4c4268a6ef7dd94..b841820acfce586763b11bccb40ca3aaf1ba0d46 100644
--- a/cumulus/pallets/parachain-system/src/lib.rs
+++ b/cumulus/pallets/parachain-system/src/lib.rs
@@ -385,6 +385,16 @@ pub mod pallet {
 			)
 			.expect("Invalid relay chain state proof");
 
+			// Deposit a log indicating the relay-parent storage root.
+			// TODO: remove this in favor of the relay-parent's hash after
+			// https://github.com/paritytech/cumulus/issues/303
+			frame_system::Pallet::<T>::deposit_log(
+				cumulus_primitives_core::rpsr_digest::relay_parent_storage_root_item(
+					vfp.relay_parent_storage_root,
+					vfp.relay_parent_number,
+				),
+			);
+
 			// initialization logic: we know that this runs exactly once every block,
 			// which means we can put the initialization logic here to remove the
 			// sequencing problem.
diff --git a/cumulus/pallets/parachain-system/src/tests.rs b/cumulus/pallets/parachain-system/src/tests.rs
index a4b1c275b7a42c95ab758b43d1fba8c1018babc9..cfbe834983c077f99239e04ab4f4c22c83708d75 100755
--- a/cumulus/pallets/parachain-system/src/tests.rs
+++ b/cumulus/pallets/parachain-system/src/tests.rs
@@ -1006,3 +1006,18 @@ fn upgrade_version_checks_should_work() {
 		});
 	}
 }
+
+#[test]
+fn deposits_relay_parent_storage_root() {
+	BlockTests::new().add_with_post_test(
+		123,
+		|| {},
+		|| {
+			let digest = System::digest();
+			assert!(cumulus_primitives_core::rpsr_digest::extract_relay_parent_storage_root(
+				&digest
+			)
+			.is_some());
+		},
+	);
+}
diff --git a/cumulus/primitives/core/src/lib.rs b/cumulus/primitives/core/src/lib.rs
index 52770cdf71698df61c945a615fac973e56d96dde..752e1aee474306b16f8800ee86a7e6e76bc67111 100644
--- a/cumulus/primitives/core/src/lib.rs
+++ b/cumulus/primitives/core/src/lib.rs
@@ -21,7 +21,7 @@
 use codec::{Decode, Encode};
 use polkadot_parachain::primitives::HeadData;
 use scale_info::TypeInfo;
-use sp_runtime::{traits::Block as BlockT, RuntimeDebug};
+use sp_runtime::RuntimeDebug;
 use sp_std::prelude::*;
 
 pub use polkadot_core_primitives::InboundDownwardMessage;
@@ -33,6 +33,12 @@ pub use polkadot_primitives::{
 	AbridgedHostConfiguration, AbridgedHrmpChannel, PersistedValidationData,
 };
 
+pub use sp_runtime::{
+	generic::{Digest, DigestItem},
+	traits::Block as BlockT,
+	ConsensusEngineId,
+};
+
 pub use xcm::latest::prelude::*;
 
 /// A module that re-exports relevant relay chain definitions.
@@ -198,6 +204,88 @@ impl<B: BlockT> ParachainBlockData<B> {
 	}
 }
 
+/// A consensus engine ID indicating that this is a Cumulus Parachain.
+pub const CUMULUS_CONSENSUS_ID: ConsensusEngineId = *b"CMLS";
+
+/// Consensus header digests for Cumulus parachains.
+#[derive(Clone, RuntimeDebug, Decode, Encode, PartialEq)]
+pub enum CumulusDigestItem {
+	/// A digest item indicating the relay-parent a parachain block was built against.
+	#[codec(index = 0)]
+	RelayParent(relay_chain::Hash),
+}
+
+impl CumulusDigestItem {
+	/// Encode this as a Substrate [`DigestItem`].
+	pub fn to_digest_item(&self) -> DigestItem {
+		DigestItem::Consensus(CUMULUS_CONSENSUS_ID, self.encode())
+	}
+}
+
+/// Extract the relay-parent from the provided header digest. Returns `None` if none were found.
+///
+/// If there are multiple valid digests, this returns the value of the first one, although
+/// well-behaving runtimes should not produce headers with more than one.
+pub fn extract_relay_parent(digest: &Digest) -> Option<relay_chain::Hash> {
+	digest.convert_first(|d| match d {
+		DigestItem::Consensus(id, val) if id == &CUMULUS_CONSENSUS_ID =>
+			match CumulusDigestItem::decode(&mut &val[..]) {
+				Ok(CumulusDigestItem::RelayParent(hash)) => Some(hash),
+				_ => None,
+			},
+		_ => None,
+	})
+}
+
+/// Utilities for handling the relay-parent storage root as a digest item.
+///
+/// This is not intended to be part of the public API, as it is a workaround for
+/// <https://github.com/paritytech/cumulus/issues/303> via
+/// <https://github.com/paritytech/polkadot/issues/7191>.
+///
+/// Runtimes using the parachain-system pallet are expected to produce this digest item,
+/// but will stop as soon as they are able to provide the relay-parent hash directly.
+///
+/// The relay-chain storage root is, in practice, a unique identifier of a block
+/// in the absence of equivocations (which are slashable). This assumes that the relay chain
+/// uses BABE or SASSAFRAS, because the slot and the author's VRF randomness are both included
+/// in the relay-chain storage root in both cases.
+///
+/// Therefore, the relay-parent storage root is a suitable identifier of unique relay chain
+/// blocks in low-value scenarios such as performance optimizations.
+#[doc(hidden)]
+pub mod rpsr_digest {
+	use super::{relay_chain, ConsensusEngineId, Decode, Digest, DigestItem, Encode};
+	use codec::Compact;
+
+	/// A consensus engine ID for relay-parent storage root digests.
+	pub const RPSR_CONSENSUS_ID: ConsensusEngineId = *b"RPSR";
+
+	/// Construct a digest item for relay-parent storage roots.
+	pub fn relay_parent_storage_root_item(
+		storage_root: relay_chain::Hash,
+		number: impl Into<Compact<relay_chain::BlockNumber>>,
+	) -> DigestItem {
+		DigestItem::Consensus(RPSR_CONSENSUS_ID, (storage_root, number.into()).encode())
+	}
+
+	/// Extract the relay-parent storage root and number from the provided header digest. Returns `None`
+	/// if none were found.
+	pub fn extract_relay_parent_storage_root(
+		digest: &Digest,
+	) -> Option<(relay_chain::Hash, relay_chain::BlockNumber)> {
+		digest.convert_first(|d| match d {
+			DigestItem::Consensus(id, val) if id == &RPSR_CONSENSUS_ID => {
+				let (h, n): (relay_chain::Hash, Compact<relay_chain::BlockNumber>) =
+					Decode::decode(&mut &val[..]).ok()?;
+
+				Some((h, n.0))
+			},
+			_ => None,
+		})
+	}
+}
+
 /// Information about a collation.
 ///
 /// This was used in version 1 of the [`CollectCollationInfo`] runtime api.