diff --git a/bridges/bin/millau/runtime/src/lib.rs b/bridges/bin/millau/runtime/src/lib.rs
index 4fd9a098fc0443e7df71aa42157bf0c57070376e..9abe5ce69c096636c0e61ae69a848799f563d20c 100644
--- a/bridges/bin/millau/runtime/src/lib.rs
+++ b/bridges/bin/millau/runtime/src/lib.rs
@@ -88,8 +88,8 @@ pub use pallet_xcm::Call as XcmCall;
 use bridge_runtime_common::{
 	generate_bridge_reject_obsolete_headers_and_messages,
 	refund_relayer_extension::{
-		ActualFeeRefund, RefundBridgedParachainMessages, RefundableMessagesLane,
-		RefundableParachain,
+		ActualFeeRefund, RefundBridgedParachainMessages, RefundSignedExtensionAdapter,
+		RefundableMessagesLane, RefundableParachain,
 	},
 };
 #[cfg(any(feature = "std", test))]
@@ -625,13 +625,15 @@ generate_bridge_reject_obsolete_headers_and_messages! {
 bp_runtime::generate_static_str_provider!(BridgeRefundRialtoPara2000Lane0Msgs);
 /// Signed extension that refunds relayers that are delivering messages from the Rialto parachain.
 pub type PriorityBoostPerMessage = ConstU64<324_316_715>;
-pub type BridgeRefundRialtoParachainMessages = RefundBridgedParachainMessages<
-	Runtime,
-	RefundableParachain<WithRialtoParachainsInstance, bp_rialto_parachain::RialtoParachain>,
-	RefundableMessagesLane<WithRialtoParachainMessagesInstance, RialtoParachainMessagesLane>,
-	ActualFeeRefund<Runtime>,
-	PriorityBoostPerMessage,
-	StrBridgeRefundRialtoPara2000Lane0Msgs,
+pub type BridgeRefundRialtoParachainMessages = RefundSignedExtensionAdapter<
+	RefundBridgedParachainMessages<
+		Runtime,
+		RefundableParachain<WithRialtoParachainsInstance, bp_rialto_parachain::RialtoParachain>,
+		RefundableMessagesLane<WithRialtoParachainMessagesInstance, RialtoParachainMessagesLane>,
+		ActualFeeRefund<Runtime>,
+		PriorityBoostPerMessage,
+		StrBridgeRefundRialtoPara2000Lane0Msgs,
+	>,
 >;
 
 /// The address format for describing accounts.
diff --git a/bridges/bin/runtime-common/src/refund_relayer_extension.rs b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
index f0c2cbf44509b3340028927a5b374461c2e94d12..55a4d17036e22d50fa59d8623851353605fa934a 100644
--- a/bridges/bin/runtime-common/src/refund_relayer_extension.rs
+++ b/bridges/bin/runtime-common/src/refund_relayer_extension.rs
@@ -24,8 +24,8 @@ use crate::messages_call_ext::{
 };
 use bp_messages::{LaneId, MessageNonce};
 use bp_relayers::{RewardsAccountOwner, RewardsAccountParams};
-use bp_runtime::{Parachain, ParachainIdOf, RangeInclusiveExt, StaticStrProvider};
-use codec::{Decode, Encode};
+use bp_runtime::{Chain, Parachain, ParachainIdOf, RangeInclusiveExt, StaticStrProvider};
+use codec::{Codec, Decode, Encode};
 use frame_support::{
 	dispatch::{CallableCallFor, DispatchInfo, PostDispatchInfo},
 	traits::IsSubType,
@@ -33,7 +33,8 @@ use frame_support::{
 	CloneNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
 };
 use pallet_bridge_grandpa::{
-	CallSubType as GrandpaCallSubType, SubmitFinalityProofHelper, SubmitFinalityProofInfo,
+	CallSubType as GrandpaCallSubType, Config as GrandpaConfig, SubmitFinalityProofHelper,
+	SubmitFinalityProofInfo,
 };
 use pallet_bridge_messages::Config as MessagesConfig;
 use pallet_bridge_parachains::{
@@ -96,7 +97,7 @@ where
 /// coming from this lane.
 pub trait RefundableMessagesLaneId {
 	/// The instance of the bridge messages pallet.
-	type Instance;
+	type Instance: 'static;
 	/// The messages lane id.
 	type Id: Get<LaneId>;
 }
@@ -106,6 +107,7 @@ pub struct RefundableMessagesLane<Instance, Id>(PhantomData<(Instance, Id)>);
 
 impl<Instance, Id> RefundableMessagesLaneId for RefundableMessagesLane<Instance, Id>
 where
+	Instance: 'static,
 	Id: Get<LaneId>,
 {
 	type Instance = Instance;
@@ -165,7 +167,11 @@ pub enum CallInfo {
 		SubmitParachainHeadsInfo,
 		MessagesCallInfo,
 	),
+	/// Relay chain finality + message delivery/confirmation calls.
+	RelayFinalityAndMsgs(SubmitFinalityProofInfo<RelayBlockNumber>, MessagesCallInfo),
 	/// Parachain finality + message delivery/confirmation calls.
+	///
+	/// This variant is used only when bridging with parachain.
 	ParachainFinalityAndMsgs(SubmitParachainHeadsInfo, MessagesCallInfo),
 	/// Standalone message delivery/confirmation call.
 	Msgs(MessagesCallInfo),
@@ -184,6 +190,7 @@ impl CallInfo {
 	fn submit_finality_proof_info(&self) -> Option<SubmitFinalityProofInfo<RelayBlockNumber>> {
 		match *self {
 			Self::AllFinalityAndMsgs(info, _, _) => Some(info),
+			Self::RelayFinalityAndMsgs(info, _) => Some(info),
 			_ => None,
 		}
 	}
@@ -201,6 +208,7 @@ impl CallInfo {
 	fn messages_call_info(&self) -> &MessagesCallInfo {
 		match self {
 			Self::AllFinalityAndMsgs(_, _, info) => info,
+			Self::RelayFinalityAndMsgs(_, info) => info,
 			Self::ParachainFinalityAndMsgs(_, info) => info,
 			Self::Msgs(info) => info,
 		}
@@ -209,7 +217,7 @@ impl CallInfo {
 
 /// The actions on relayer account that need to be performed because of his actions.
 #[derive(RuntimeDebug, PartialEq)]
-enum RelayerAccountAction<AccountId, Reward> {
+pub enum RelayerAccountAction<AccountId, Reward> {
 	/// Do nothing with relayer account.
 	None,
 	/// Reward the relayer.
@@ -218,121 +226,60 @@ enum RelayerAccountAction<AccountId, Reward> {
 	Slash(AccountId, RewardsAccountParams),
 }
 
-/// Signed extension that refunds a relayer for new messages coming from a parachain.
-///
-/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`)
-/// with message delivery transaction. Batch may deliver either both relay chain header and
-/// parachain head, or just parachain head. Corresponding headers must be used in messages
-/// proof verification.
-///
-/// Extension does not refund transaction tip due to security reasons.
-#[derive(
-	DefaultNoBound,
-	CloneNoBound,
-	Decode,
-	Encode,
-	EqNoBound,
-	PartialEqNoBound,
-	RuntimeDebugNoBound,
-	TypeInfo,
-)]
-#[scale_info(skip_type_params(Runtime, Para, Msgs, Refund, Priority, Id))]
-pub struct RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>(
-	PhantomData<(
-		// runtime with `frame-utility`, `pallet-bridge-grandpa`, `pallet-bridge-parachains`,
-		// `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed
-		Runtime,
-		// implementation of `RefundableParachainId` trait, which specifies the instance of
-		// the used `pallet-bridge-parachains` pallet and the bridged parachain id
-		Para,
-		// implementation of `RefundableMessagesLaneId` trait, which specifies the instance of
-		// the used `pallet-bridge-messages` pallet and the lane within this pallet
-		Msgs,
-		// implementation of the `RefundCalculator` trait, that is used to compute refund that
-		// we give to relayer for his transaction
-		Refund,
-		// getter for per-message `TransactionPriority` boost that we give to message
-		// delivery transactions
-		Priority,
-		// the runtime-unique identifier of this signed extension
-		Id,
-	)>,
-);
-
-impl<Runtime, Para, Msgs, Refund, Priority, Id>
-	RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>
+/// Everything common among our refund signed extensions.
+pub trait RefundSignedExtension:
+	'static + Clone + Codec + sp_std::fmt::Debug + Default + Eq + PartialEq + Send + Sync + TypeInfo
 where
-	Self: 'static + Send + Sync,
-	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
-		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsConfig<Para::Instance>
-		+ MessagesConfig<Msgs::Instance>
-		+ RelayersConfig,
-	Para: RefundableParachainId,
-	Msgs: RefundableMessagesLaneId,
-	Refund: RefundCalculator<Balance = Runtime::Reward>,
-	Priority: Get<TransactionPriority>,
-	Id: StaticStrProvider,
-	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
-		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
-		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsCallSubType<Runtime, Para::Instance>
-		+ MessagesCallSubType<Runtime, Msgs::Instance>,
+	<Self::Runtime as GrandpaConfig<Self::GrandpaInstance>>::BridgedChain:
+		Chain<BlockNumber = RelayBlockNumber>,
 {
-	fn expand_call<'a>(&self, call: &'a CallOf<Runtime>) -> Vec<&'a CallOf<Runtime>> {
-		match call.is_sub_type() {
-			Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 3 =>
-				calls.iter().collect(),
-			Some(_) => vec![],
-			None => vec![call],
-		}
-	}
-
+	/// This chain runtime.
+	type Runtime: UtilityConfig<RuntimeCall = CallOf<Self::Runtime>>
+		+ GrandpaConfig<Self::GrandpaInstance>
+		+ MessagesConfig<<Self::Msgs as RefundableMessagesLaneId>::Instance>
+		+ RelayersConfig;
+	/// Grandpa pallet reference.
+	type GrandpaInstance: 'static;
+	/// Messages pallet and lane reference.
+	type Msgs: RefundableMessagesLaneId;
+	/// Refund amount calculator.
+	type Refund: RefundCalculator<Balance = <Self::Runtime as RelayersConfig>::Reward>;
+	/// Priority boost calculator.
+	type Priority: Get<TransactionPriority>;
+	/// Signed extension unique identifier.
+	type Id: StaticStrProvider;
+
+	/// Unpack batch runtime call.
+	fn expand_call(call: &CallOf<Self::Runtime>) -> Vec<&CallOf<Self::Runtime>>;
+
+	/// Given runtime call, check if it has supported format. Additionally, check if any of
+	/// (optionally batched) calls are obsolete and we shall reject the transaction.
 	fn parse_and_check_for_obsolete_call(
-		&self,
-		call: &CallOf<Runtime>,
-	) -> Result<Option<CallInfo>, TransactionValidityError> {
-		let calls = self.expand_call(call);
-		let total_calls = calls.len();
-		let mut calls = calls.into_iter().map(Self::check_obsolete_call).rev();
-
-		let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get()));
-		let para_finality_call = calls
-			.next()
-			.transpose()?
-			.and_then(|c| c.submit_parachain_heads_info_for(Para::Id::get()));
-		let relay_finality_call =
-			calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
+		call: &CallOf<Self::Runtime>,
+	) -> Result<Option<CallInfo>, TransactionValidityError>;
 
-		Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) {
-			(3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) => Some(
-				CallInfo::AllFinalityAndMsgs(relay_finality_call, para_finality_call, msgs_call),
-			),
-			(2, None, Some(para_finality_call), Some(msgs_call)) =>
-				Some(CallInfo::ParachainFinalityAndMsgs(para_finality_call, msgs_call)),
-			(1, None, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)),
-			_ => None,
-		})
-	}
+	/// Check if parsed call is already obsolete.
+	fn check_obsolete_parsed_call(
+		call: &CallOf<Self::Runtime>,
+	) -> Result<&CallOf<Self::Runtime>, TransactionValidityError>;
 
-	fn check_obsolete_call(
-		call: &CallOf<Runtime>,
-	) -> Result<&CallOf<Runtime>, TransactionValidityError> {
-		call.check_obsolete_submit_finality_proof()?;
-		call.check_obsolete_submit_parachain_heads()?;
-		call.check_obsolete_call()?;
-		Ok(call)
-	}
+	/// Called from post-dispatch and shall perform additional checks (apart from relay
+	/// chain finality and messages transaction finality) of given call result.
+	fn additional_call_result_check(
+		relayer: &AccountIdOf<Self::Runtime>,
+		call_info: &CallInfo,
+	) -> bool;
 
 	/// Given post-dispatch information, analyze the outcome of relayer call and return
 	/// actions that need to be performed on relayer account.
 	fn analyze_call_result(
-		pre: Option<Option<PreDispatchData<Runtime::AccountId>>>,
+		pre: Option<Option<PreDispatchData<AccountIdOf<Self::Runtime>>>>,
 		info: &DispatchInfo,
 		post_info: &PostDispatchInfo,
 		len: usize,
 		result: &DispatchResult,
-	) -> RelayerAccountAction<AccountIdOf<Runtime>, Runtime::Reward> {
+	) -> RelayerAccountAction<AccountIdOf<Self::Runtime>, <Self::Runtime as RelayersConfig>::Reward>
+	{
 		let mut extra_weight = Weight::zero();
 		let mut extra_size = 0;
 
@@ -344,15 +291,18 @@ where
 
 		// now we know that the relayer either needs to be rewarded, or slashed
 		// => let's prepare the correspondent account that pays reward/receives slashed amount
-		let reward_account_params = RewardsAccountParams::new(
-			Msgs::Id::get(),
-			Runtime::BridgedChainId::get(),
-			if call_info.is_receive_messages_proof_call() {
-				RewardsAccountOwner::ThisChain
-			} else {
-				RewardsAccountOwner::BridgedChain
-			},
-		);
+		let reward_account_params =
+			RewardsAccountParams::new(
+				<Self::Msgs as RefundableMessagesLaneId>::Id::get(),
+				<Self::Runtime as MessagesConfig<
+					<Self::Msgs as RefundableMessagesLaneId>::Instance,
+				>>::BridgedChainId::get(),
+				if call_info.is_receive_messages_proof_call() {
+					RewardsAccountOwner::ThisChain
+				} else {
+					RewardsAccountOwner::BridgedChain
+				},
+			);
 
 		// prepare return value for the case if the call has failed or it has not caused
 		// expected side effects (e.g. not all messages have been accepted)
@@ -376,10 +326,9 @@ where
 		if let Err(e) = result {
 			log::trace!(
 				target: "runtime::bridge",
-				"{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}",
-				Self::IDENTIFIER,
-				Para::Id::get(),
-				Msgs::Id::get(),
+				"{} via {:?}: relayer {:?} has submitted invalid messages transaction: {:?}",
+				Self::Id::STR,
+				<Self::Msgs as RefundableMessagesLaneId>::Id::get(),
 				relayer,
 				e,
 			);
@@ -388,19 +337,18 @@ where
 
 		// check if relay chain state has been updated
 		if let Some(finality_proof_info) = call_info.submit_finality_proof_info() {
-			if !SubmitFinalityProofHelper::<Runtime, Runtime::BridgesGrandpaPalletInstance>::was_successful(
+			if !SubmitFinalityProofHelper::<Self::Runtime, Self::GrandpaInstance>::was_successful(
 				finality_proof_info.block_number,
 			) {
 				// we only refund relayer if all calls have updated chain state
 				log::trace!(
 					target: "runtime::bridge",
-					"{} from parachain {} via {:?}: relayer {:?} has submitted invalid relay chain finality proof",
-					Self::IDENTIFIER,
-					Para::Id::get(),
-					Msgs::Id::get(),
+					"{} via {:?}: relayer {:?} has submitted invalid relay chain finality proof",
+					Self::Id::STR,
+					<Self::Msgs as RefundableMessagesLaneId>::Id::get(),
 					relayer,
 				);
-				return slash_relayer_if_delivery_result;
+				return slash_relayer_if_delivery_result
 			}
 
 			// there's a conflict between how bridge GRANDPA pallet works and a `utility.batchAll`
@@ -416,39 +364,25 @@ where
 			extra_size = finality_proof_info.extra_size;
 		}
 
-		// check if parachain state has been updated
-		if let Some(para_proof_info) = call_info.submit_parachain_heads_info() {
-			if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful(
-				para_proof_info,
-			) {
-				// we only refund relayer if all calls have updated chain state
-				log::trace!(
-					target: "runtime::bridge",
-					"{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof",
-					Self::IDENTIFIER,
-					Para::Id::get(),
-					Msgs::Id::get(),
-					relayer,
-				);
-				return slash_relayer_if_delivery_result
-			}
-		}
-
 		// Check if the `ReceiveMessagesProof` call delivered at least some of the messages that
 		// it contained. If this happens, we consider the transaction "helpful" and refund it.
 		let msgs_call_info = call_info.messages_call_info();
-		if !MessagesCallHelper::<Runtime, Msgs::Instance>::was_successful(msgs_call_info) {
+		if !MessagesCallHelper::<Self::Runtime, <Self::Msgs as RefundableMessagesLaneId>::Instance>::was_successful(msgs_call_info) {
 			log::trace!(
 				target: "runtime::bridge",
-				"{} from parachain {} via {:?}: relayer {:?} has submitted invalid messages call",
-				Self::IDENTIFIER,
-				Para::Id::get(),
-				Msgs::Id::get(),
+				"{} via {:?}: relayer {:?} has submitted invalid messages call",
+				Self::Id::STR,
+				<Self::Msgs as RefundableMessagesLaneId>::Id::get(),
 				relayer,
 			);
 			return slash_relayer_if_delivery_result
 		}
 
+		// do additional check
+		if !Self::additional_call_result_check(&relayer, &call_info) {
+			return slash_relayer_if_delivery_result
+		}
+
 		// regarding the tip - refund that happens here (at this side of the bridge) isn't the whole
 		// relayer compensation. He'll receive some amount at the other side of the bridge. It shall
 		// (in theory) cover the tip there. Otherwise, if we'll be compensating tip here, some
@@ -464,14 +398,14 @@ where
 		// let's also replace the weight of slashing relayer with the weight of rewarding relayer
 		if call_info.is_receive_messages_proof_call() {
 			post_info_weight = post_info_weight.saturating_sub(
-				<Runtime as RelayersConfig>::WeightInfo::extra_weight_of_successful_receive_messages_proof_call(),
+				<Self::Runtime as RelayersConfig>::WeightInfo::extra_weight_of_successful_receive_messages_proof_call(),
 			);
 		}
 
 		// compute the relayer refund
 		let mut post_info = *post_info;
 		post_info.actual_weight = Some(post_info_weight);
-		let refund = Refund::compute_refund(info, &post_info, post_info_len, tip);
+		let refund = Self::Refund::compute_refund(info, &post_info, post_info_len, tip);
 
 		// we can finally reward relayer
 		RelayerAccountAction::Reward(relayer, reward_account_params, refund)
@@ -497,7 +431,11 @@ where
 		let bundled_messages = parsed_call.messages_call_info().bundled_messages().saturating_len();
 
 		// a quick check to avoid invalid high-priority transactions
-		if bundled_messages > Runtime::MaxUnconfirmedMessagesAtInboundLane::get() {
+		let max_unconfirmed_messages_in_confirmation_tx = <Self::Runtime as MessagesConfig<
+			<Self::Msgs as RefundableMessagesLaneId>::Instance,
+		>>::MaxUnconfirmedMessagesAtInboundLane::get(
+		);
+		if bundled_messages > max_unconfirmed_messages_in_confirmation_tx {
 			return None
 		}
 
@@ -505,31 +443,37 @@ where
 	}
 }
 
-impl<Runtime, Para, Msgs, Refund, Priority, Id> SignedExtension
-	for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>
+/// Adapter that allow implementing `sp_runtime::traits::SignedExtension` for any
+/// `RefundSignedExtension`.
+#[derive(
+	DefaultNoBound,
+	CloneNoBound,
+	Decode,
+	Encode,
+	EqNoBound,
+	PartialEqNoBound,
+	RuntimeDebugNoBound,
+	TypeInfo,
+)]
+pub struct RefundSignedExtensionAdapter<T: RefundSignedExtension>(T)
 where
-	Self: 'static + Send + Sync,
-	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
-		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsConfig<Para::Instance>
-		+ MessagesConfig<Msgs::Instance>
-		+ RelayersConfig,
-	Para: RefundableParachainId,
-	Msgs: RefundableMessagesLaneId,
-	Refund: RefundCalculator<Balance = Runtime::Reward>,
-	Priority: Get<TransactionPriority>,
-	Id: StaticStrProvider,
-	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
-		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
-		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
-		+ ParachainsCallSubType<Runtime, Para::Instance>
-		+ MessagesCallSubType<Runtime, Msgs::Instance>,
+	<T::Runtime as GrandpaConfig<T::GrandpaInstance>>::BridgedChain:
+		Chain<BlockNumber = RelayBlockNumber>;
+
+impl<T: RefundSignedExtension> SignedExtension for RefundSignedExtensionAdapter<T>
+where
+	<T::Runtime as GrandpaConfig<T::GrandpaInstance>>::BridgedChain:
+		Chain<BlockNumber = RelayBlockNumber>,
+	CallOf<T::Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+		+ IsSubType<CallableCallFor<UtilityPallet<T::Runtime>, T::Runtime>>
+		+ GrandpaCallSubType<T::Runtime, T::GrandpaInstance>
+		+ MessagesCallSubType<T::Runtime, <T::Msgs as RefundableMessagesLaneId>::Instance>,
 {
-	const IDENTIFIER: &'static str = Id::STR;
-	type AccountId = Runtime::AccountId;
-	type Call = CallOf<Runtime>;
+	const IDENTIFIER: &'static str = T::Id::STR;
+	type AccountId = AccountIdOf<T::Runtime>;
+	type Call = CallOf<T::Runtime>;
 	type AdditionalSigned = ();
-	type Pre = Option<PreDispatchData<Runtime::AccountId>>;
+	type Pre = Option<PreDispatchData<AccountIdOf<T::Runtime>>>;
 
 	fn additional_signed(&self) -> Result<(), TransactionValidityError> {
 		Ok(())
@@ -547,34 +491,32 @@ where
 		// we're not calling `validate` from `pre_dispatch` directly because of performance
 		// reasons, so if you're adding some code that may fail here, please check if it needs
 		// to be added to the `pre_dispatch` as well
-		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+		let parsed_call = T::parse_and_check_for_obsolete_call(call)?;
 
 		// the following code just plays with transaction priority and never returns an error
 
 		// we only boost priority of presumably correct message delivery transactions
-		let bundled_messages = match Self::bundled_messages_for_priority_boost(parsed_call.as_ref())
-		{
+		let bundled_messages = match T::bundled_messages_for_priority_boost(parsed_call.as_ref()) {
 			Some(bundled_messages) => bundled_messages,
 			None => return Ok(Default::default()),
 		};
 
 		// we only boost priority if relayer has staked required balance
-		if !RelayersPallet::<Runtime>::is_registration_active(who) {
+		if !RelayersPallet::<T::Runtime>::is_registration_active(who) {
 			return Ok(Default::default())
 		}
 
 		// compute priority boost
 		let priority_boost =
-			crate::priority_calculator::compute_priority_boost::<Priority>(bundled_messages);
+			crate::priority_calculator::compute_priority_boost::<T::Priority>(bundled_messages);
 		let valid_transaction = ValidTransactionBuilder::default().priority(priority_boost);
 
 		log::trace!(
 			target: "runtime::bridge",
-			"{} from parachain {} via {:?} has boosted priority of message delivery transaction \
+			"{} via {:?} has boosted priority of message delivery transaction \
 			of relayer {:?}: {} messages -> {} priority",
 			Self::IDENTIFIER,
-			Para::Id::get(),
-			Msgs::Id::get(),
+			<T::Msgs as RefundableMessagesLaneId>::Id::get(),
 			who,
 			bundled_messages,
 			priority_boost,
@@ -591,54 +533,294 @@ where
 		_len: usize,
 	) -> Result<Self::Pre, TransactionValidityError> {
 		// this is a relevant piece of `validate` that we need here (in `pre_dispatch`)
-		let parsed_call = self.parse_and_check_for_obsolete_call(call)?;
+		let parsed_call = T::parse_and_check_for_obsolete_call(call)?;
+
+		Ok(parsed_call.map(|call_info| {
+			log::trace!(
+				target: "runtime::bridge",
+				"{} via {:?} parsed bridge transaction in pre-dispatch: {:?}",
+				Self::IDENTIFIER,
+				<T::Msgs as RefundableMessagesLaneId>::Id::get(),
+				call_info,
+			);
+			PreDispatchData { relayer: who.clone(), call_info }
+		}))
+	}
+
+	fn post_dispatch(
+		pre: Option<Self::Pre>,
+		info: &DispatchInfoOf<Self::Call>,
+		post_info: &PostDispatchInfoOf<Self::Call>,
+		len: usize,
+		result: &DispatchResult,
+	) -> Result<(), TransactionValidityError> {
+		let call_result = T::analyze_call_result(pre, info, post_info, len, result);
+
+		match call_result {
+			RelayerAccountAction::None => (),
+			RelayerAccountAction::Reward(relayer, reward_account, reward) => {
+				RelayersPallet::<T::Runtime>::register_relayer_reward(
+					reward_account,
+					&relayer,
+					reward,
+				);
+
+				log::trace!(
+					target: "runtime::bridge",
+					"{} via {:?} has registered reward: {:?} for {:?}",
+					Self::IDENTIFIER,
+					<T::Msgs as RefundableMessagesLaneId>::Id::get(),
+					reward,
+					relayer,
+				);
+			},
+			RelayerAccountAction::Slash(relayer, slash_account) =>
+				RelayersPallet::<T::Runtime>::slash_and_deregister(&relayer, slash_account),
+		}
+
+		Ok(())
+	}
+}
+
+/// Signed extension that refunds a relayer for new messages coming from a parachain.
+///
+/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`)
+/// with message delivery transaction. Batch may deliver either both relay chain header and
+/// parachain head, or just parachain head. Corresponding headers must be used in messages
+/// proof verification.
+///
+/// Extension does not refund transaction tip due to security reasons.
+#[derive(
+	DefaultNoBound,
+	CloneNoBound,
+	Decode,
+	Encode,
+	EqNoBound,
+	PartialEqNoBound,
+	RuntimeDebugNoBound,
+	TypeInfo,
+)]
+#[scale_info(skip_type_params(Runtime, Para, Msgs, Refund, Priority, Id))]
+pub struct RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>(
+	PhantomData<(
+		// runtime with `frame-utility`, `pallet-bridge-grandpa`, `pallet-bridge-parachains`,
+		// `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed
+		Runtime,
+		// implementation of `RefundableParachainId` trait, which specifies the instance of
+		// the used `pallet-bridge-parachains` pallet and the bridged parachain id
+		Para,
+		// implementation of `RefundableMessagesLaneId` trait, which specifies the instance of
+		// the used `pallet-bridge-messages` pallet and the lane within this pallet
+		Msgs,
+		// implementation of the `RefundCalculator` trait, that is used to compute refund that
+		// we give to relayer for his transaction
+		Refund,
+		// getter for per-message `TransactionPriority` boost that we give to message
+		// delivery transactions
+		Priority,
+		// the runtime-unique identifier of this signed extension
+		Id,
+	)>,
+);
+
+impl<Runtime, Para, Msgs, Refund, Priority, Id> RefundSignedExtension
+	for RefundBridgedParachainMessages<Runtime, Para, Msgs, Refund, Priority, Id>
+where
+	Self: 'static + Send + Sync,
+	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
+		+ BoundedBridgeGrandpaConfig<Runtime::BridgesGrandpaPalletInstance>
+		+ ParachainsConfig<Para::Instance>
+		+ MessagesConfig<Msgs::Instance>
+		+ RelayersConfig,
+	Para: RefundableParachainId,
+	Msgs: RefundableMessagesLaneId,
+	Refund: RefundCalculator<Balance = Runtime::Reward>,
+	Priority: Get<TransactionPriority>,
+	Id: StaticStrProvider,
+	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
+		+ GrandpaCallSubType<Runtime, Runtime::BridgesGrandpaPalletInstance>
+		+ ParachainsCallSubType<Runtime, Para::Instance>
+		+ MessagesCallSubType<Runtime, Msgs::Instance>,
+{
+	type Runtime = Runtime;
+	type GrandpaInstance = Runtime::BridgesGrandpaPalletInstance;
+	type Msgs = Msgs;
+	type Refund = Refund;
+	type Priority = Priority;
+	type Id = Id;
+
+	fn expand_call(call: &CallOf<Runtime>) -> Vec<&CallOf<Runtime>> {
+		match call.is_sub_type() {
+			Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 3 =>
+				calls.iter().collect(),
+			Some(_) => vec![],
+			None => vec![call],
+		}
+	}
+
+	fn parse_and_check_for_obsolete_call(
+		call: &CallOf<Runtime>,
+	) -> Result<Option<CallInfo>, TransactionValidityError> {
+		let calls = Self::expand_call(call);
+		let total_calls = calls.len();
+		let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
+
+		let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get()));
+		let para_finality_call = calls
+			.next()
+			.transpose()?
+			.and_then(|c| c.submit_parachain_heads_info_for(Para::Id::get()));
+		let relay_finality_call =
+			calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
+
+		Ok(match (total_calls, relay_finality_call, para_finality_call, msgs_call) {
+			(3, Some(relay_finality_call), Some(para_finality_call), Some(msgs_call)) => Some(
+				CallInfo::AllFinalityAndMsgs(relay_finality_call, para_finality_call, msgs_call),
+			),
+			(2, None, Some(para_finality_call), Some(msgs_call)) =>
+				Some(CallInfo::ParachainFinalityAndMsgs(para_finality_call, msgs_call)),
+			(1, None, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)),
+			_ => None,
+		})
+	}
+
+	fn check_obsolete_parsed_call(
+		call: &CallOf<Runtime>,
+	) -> Result<&CallOf<Runtime>, TransactionValidityError> {
+		call.check_obsolete_submit_finality_proof()?;
+		call.check_obsolete_submit_parachain_heads()?;
+		call.check_obsolete_call()?;
+		Ok(call)
+	}
+
+	fn additional_call_result_check(relayer: &Runtime::AccountId, call_info: &CallInfo) -> bool {
+		// check if parachain state has been updated
+		if let Some(para_proof_info) = call_info.submit_parachain_heads_info() {
+			if !SubmitParachainHeadsHelper::<Runtime, Para::Instance>::was_successful(
+				para_proof_info,
+			) {
+				// we only refund relayer if all calls have updated chain state
+				log::trace!(
+					target: "runtime::bridge",
+					"{} from parachain {} via {:?}: relayer {:?} has submitted invalid parachain finality proof",
+					Id::STR,
+					Para::Id::get(),
+					Msgs::Id::get(),
+					relayer,
+				);
+				return false
+			}
+		}
+
+		true
+	}
+}
+
+/// Signed extension that refunds a relayer for new messages coming from a standalone (GRANDPA)
+/// chain.
+///
+/// Also refunds relayer for successful finality delivery if it comes in batch (`utility.batchAll`)
+/// with message delivery transaction. Batch may deliver either both relay chain header and
+/// parachain head, or just parachain head. Corresponding headers must be used in messages
+/// proof verification.
+///
+/// Extension does not refund transaction tip due to security reasons.
+#[derive(
+	DefaultNoBound,
+	CloneNoBound,
+	Decode,
+	Encode,
+	EqNoBound,
+	PartialEqNoBound,
+	RuntimeDebugNoBound,
+	TypeInfo,
+)]
+#[scale_info(skip_type_params(Runtime, GrandpaInstance, Msgs, Refund, Priority, Id))]
+pub struct RefundBridgedGrandpaMessages<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id>(
+	PhantomData<(
+		// runtime with `frame-utility`, `pallet-bridge-grandpa`,
+		// `pallet-bridge-messages` and `pallet-bridge-relayers` pallets deployed
+		Runtime,
+		// bridge GRANDPA pallet instance, used to track bridged chain state
+		GrandpaInstance,
+		// implementation of `RefundableMessagesLaneId` trait, which specifies the instance of
+		// the used `pallet-bridge-messages` pallet and the lane within this pallet
+		Msgs,
+		// implementation of the `RefundCalculator` trait, that is used to compute refund that
+		// we give to relayer for his transaction
+		Refund,
+		// getter for per-message `TransactionPriority` boost that we give to message
+		// delivery transactions
+		Priority,
+		// the runtime-unique identifier of this signed extension
+		Id,
+	)>,
+);
+
+impl<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id> RefundSignedExtension
+	for RefundBridgedGrandpaMessages<Runtime, GrandpaInstance, Msgs, Refund, Priority, Id>
+where
+	Self: 'static + Send + Sync,
+	Runtime: UtilityConfig<RuntimeCall = CallOf<Runtime>>
+		+ BoundedBridgeGrandpaConfig<GrandpaInstance>
+		+ MessagesConfig<Msgs::Instance>
+		+ RelayersConfig,
+	GrandpaInstance: 'static,
+	Msgs: RefundableMessagesLaneId,
+	Refund: RefundCalculator<Balance = Runtime::Reward>,
+	Priority: Get<TransactionPriority>,
+	Id: StaticStrProvider,
+	CallOf<Runtime>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
+		+ IsSubType<CallableCallFor<UtilityPallet<Runtime>, Runtime>>
+		+ GrandpaCallSubType<Runtime, GrandpaInstance>
+		+ MessagesCallSubType<Runtime, Msgs::Instance>,
+{
+	type Runtime = Runtime;
+	type GrandpaInstance = GrandpaInstance;
+	type Msgs = Msgs;
+	type Refund = Refund;
+	type Priority = Priority;
+	type Id = Id;
 
-		Ok(parsed_call.map(|call_info| {
-			log::trace!(
-				target: "runtime::bridge",
-				"{} from parachain {} via {:?} parsed bridge transaction in pre-dispatch: {:?}",
-				Self::IDENTIFIER,
-				Para::Id::get(),
-				Msgs::Id::get(),
-				call_info,
-			);
-			PreDispatchData { relayer: who.clone(), call_info }
-		}))
+	fn expand_call(call: &CallOf<Runtime>) -> Vec<&CallOf<Runtime>> {
+		match call.is_sub_type() {
+			Some(UtilityCall::<Runtime>::batch_all { ref calls }) if calls.len() <= 2 =>
+				calls.iter().collect(),
+			Some(_) => vec![],
+			None => vec![call],
+		}
 	}
 
-	fn post_dispatch(
-		pre: Option<Self::Pre>,
-		info: &DispatchInfoOf<Self::Call>,
-		post_info: &PostDispatchInfoOf<Self::Call>,
-		len: usize,
-		result: &DispatchResult,
-	) -> Result<(), TransactionValidityError> {
-		let call_result = Self::analyze_call_result(pre, info, post_info, len, result);
+	fn parse_and_check_for_obsolete_call(
+		call: &CallOf<Runtime>,
+	) -> Result<Option<CallInfo>, TransactionValidityError> {
+		let calls = Self::expand_call(call);
+		let total_calls = calls.len();
+		let mut calls = calls.into_iter().map(Self::check_obsolete_parsed_call).rev();
 
-		match call_result {
-			RelayerAccountAction::None => (),
-			RelayerAccountAction::Reward(relayer, reward_account, reward) => {
-				RelayersPallet::<Runtime>::register_relayer_reward(
-					reward_account,
-					&relayer,
-					reward,
-				);
+		let msgs_call = calls.next().transpose()?.and_then(|c| c.call_info_for(Msgs::Id::get()));
+		let relay_finality_call =
+			calls.next().transpose()?.and_then(|c| c.submit_finality_proof_info());
 
-				log::trace!(
-					target: "runtime::bridge",
-					"{} from parachain {} via {:?} has registered reward: {:?} for {:?}",
-					Self::IDENTIFIER,
-					Para::Id::get(),
-					Msgs::Id::get(),
-					reward,
-					relayer,
-				);
-			},
-			RelayerAccountAction::Slash(relayer, slash_account) =>
-				RelayersPallet::<Runtime>::slash_and_deregister(&relayer, slash_account),
-		}
+		Ok(match (total_calls, relay_finality_call, msgs_call) {
+			(2, Some(relay_finality_call), Some(msgs_call)) =>
+				Some(CallInfo::RelayFinalityAndMsgs(relay_finality_call, msgs_call)),
+			(1, None, Some(msgs_call)) => Some(CallInfo::Msgs(msgs_call)),
+			_ => None,
+		})
+	}
 
-		Ok(())
+	fn check_obsolete_parsed_call(
+		call: &CallOf<Runtime>,
+	) -> Result<&CallOf<Runtime>, TransactionValidityError> {
+		call.check_obsolete_submit_finality_proof()?;
+		call.check_obsolete_call()?;
+		Ok(call)
+	}
+
+	fn additional_call_result_check(_relayer: &Runtime::AccountId, _call_info: &CallInfo) -> bool {
+		true
 	}
 }
 
@@ -690,7 +872,17 @@ mod tests {
 	}
 
 	bp_runtime::generate_static_str_provider!(TestExtension);
-	type TestExtension = RefundBridgedParachainMessages<
+
+	type TestGrandpaExtensionProvider = RefundBridgedGrandpaMessages<
+		TestRuntime,
+		(),
+		RefundableMessagesLane<(), TestLaneId>,
+		ActualFeeRefund<TestRuntime>,
+		ConstU64<1>,
+		StrTestExtension,
+	>;
+	type TestGrandpaExtension = RefundSignedExtensionAdapter<TestGrandpaExtensionProvider>;
+	type TestExtensionProvider = RefundBridgedParachainMessages<
 		TestRuntime,
 		DefaultRefundableParachainId<(), TestParachain>,
 		RefundableMessagesLane<(), TestLaneId>,
@@ -698,6 +890,7 @@ mod tests {
 		ConstU64<1>,
 		StrTestExtension,
 	>;
+	type TestExtension = RefundSignedExtensionAdapter<TestExtensionProvider>;
 
 	fn initial_balance_of_relayer_account_at_this_chain() -> ThisChainBalance {
 		let test_stake: ThisChainBalance = TestStake::get();
@@ -849,6 +1042,30 @@ mod tests {
 		})
 	}
 
+	fn relay_finality_and_delivery_batch_call(
+		relay_header_number: RelayBlockNumber,
+		best_message: MessageNonce,
+	) -> RuntimeCall {
+		RuntimeCall::Utility(UtilityCall::batch_all {
+			calls: vec![
+				submit_relay_header_call(relay_header_number),
+				message_delivery_call(best_message),
+			],
+		})
+	}
+
+	fn relay_finality_and_confirmation_batch_call(
+		relay_header_number: RelayBlockNumber,
+		best_message: MessageNonce,
+	) -> RuntimeCall {
+		RuntimeCall::Utility(UtilityCall::batch_all {
+			calls: vec![
+				submit_relay_header_call(relay_header_number),
+				message_confirmation_call(best_message),
+			],
+		})
+	}
+
 	fn all_finality_and_delivery_batch_call(
 		relay_header_number: RelayBlockNumber,
 		parachain_head_at_relay_header_number: RelayBlockNumber,
@@ -931,6 +1148,50 @@ mod tests {
 		}
 	}
 
+	fn relay_finality_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> {
+		PreDispatchData {
+			relayer: relayer_account_at_this_chain(),
+			call_info: CallInfo::RelayFinalityAndMsgs(
+				SubmitFinalityProofInfo {
+					block_number: 200,
+					extra_weight: Weight::zero(),
+					extra_size: 0,
+				},
+				MessagesCallInfo::ReceiveMessagesProof(ReceiveMessagesProofInfo {
+					base: BaseMessagesProofInfo {
+						lane_id: TEST_LANE_ID,
+						bundled_range: 101..=200,
+						best_stored_nonce: 100,
+					},
+					unrewarded_relayers: UnrewardedRelayerOccupation {
+						free_relayer_slots: MaxUnrewardedRelayerEntriesAtInboundLane::get(),
+						free_message_slots: MaxUnconfirmedMessagesAtInboundLane::get(),
+					},
+				}),
+			),
+		}
+	}
+
+	fn relay_finality_confirmation_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> {
+		PreDispatchData {
+			relayer: relayer_account_at_this_chain(),
+			call_info: CallInfo::RelayFinalityAndMsgs(
+				SubmitFinalityProofInfo {
+					block_number: 200,
+					extra_weight: Weight::zero(),
+					extra_size: 0,
+				},
+				MessagesCallInfo::ReceiveMessagesDeliveryProof(ReceiveMessagesDeliveryProofInfo(
+					BaseMessagesProofInfo {
+						lane_id: TEST_LANE_ID,
+						bundled_range: 101..=200,
+						best_stored_nonce: 100,
+					},
+				)),
+			),
+		}
+	}
+
 	fn parachain_finality_pre_dispatch_data() -> PreDispatchData<ThisChainAccountId> {
 		PreDispatchData {
 			relayer: relayer_account_at_this_chain(),
@@ -1013,6 +1274,7 @@ mod tests {
 	) -> PreDispatchData<ThisChainAccountId> {
 		let msg_info = match pre_dispatch_data.call_info {
 			CallInfo::AllFinalityAndMsgs(_, _, ref mut info) => info,
+			CallInfo::RelayFinalityAndMsgs(_, ref mut info) => info,
 			CallInfo::ParachainFinalityAndMsgs(_, ref mut info) => info,
 			CallInfo::Msgs(ref mut info) => info,
 		};
@@ -1025,7 +1287,14 @@ mod tests {
 	}
 
 	fn run_validate(call: RuntimeCall) -> TransactionValidity {
-		let extension: TestExtension = RefundBridgedParachainMessages(PhantomData);
+		let extension: TestExtension =
+			RefundSignedExtensionAdapter(RefundBridgedParachainMessages(PhantomData));
+		extension.validate(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0)
+	}
+
+	fn run_grandpa_validate(call: RuntimeCall) -> TransactionValidity {
+		let extension: TestGrandpaExtension =
+			RefundSignedExtensionAdapter(RefundBridgedGrandpaMessages(PhantomData));
 		extension.validate(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0)
 	}
 
@@ -1039,7 +1308,16 @@ mod tests {
 	fn run_pre_dispatch(
 		call: RuntimeCall,
 	) -> Result<Option<PreDispatchData<ThisChainAccountId>>, TransactionValidityError> {
-		let extension: TestExtension = RefundBridgedParachainMessages(PhantomData);
+		let extension: TestExtension =
+			RefundSignedExtensionAdapter(RefundBridgedParachainMessages(PhantomData));
+		extension.pre_dispatch(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0)
+	}
+
+	fn run_grandpa_pre_dispatch(
+		call: RuntimeCall,
+	) -> Result<Option<PreDispatchData<ThisChainAccountId>>, TransactionValidityError> {
+		let extension: TestGrandpaExtension =
+			RefundSignedExtensionAdapter(RefundBridgedGrandpaMessages(PhantomData));
 		extension.pre_dispatch(&relayer_account_at_this_chain(), &call, &DispatchInfo::default(), 0)
 	}
 
@@ -1674,7 +1952,7 @@ mod tests {
 		pre_dispatch_data: PreDispatchData<ThisChainAccountId>,
 		dispatch_result: DispatchResult,
 	) -> RelayerAccountAction<ThisChainAccountId, ThisChainBalance> {
-		TestExtension::analyze_call_result(
+		TestExtensionProvider::analyze_call_result(
 			Some(Some(pre_dispatch_data)),
 			&dispatch_info(),
 			&post_dispatch_info(),
@@ -1737,4 +2015,175 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn grandpa_ext_only_parses_valid_batches() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+
+			// relay + parachain + message delivery calls batch is ignored
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&all_finality_and_delivery_batch_call(200, 200, 200)
+				),
+				Ok(None),
+			);
+
+			// relay + parachain + message confirmation calls batch is ignored
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&all_finality_and_confirmation_batch_call(200, 200, 200)
+				),
+				Ok(None),
+			);
+
+			// parachain + message delivery call batch is ignored
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&parachain_finality_and_delivery_batch_call(200, 200)
+				),
+				Ok(None),
+			);
+
+			// parachain + message confirmation call batch is ignored
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&parachain_finality_and_confirmation_batch_call(200, 200)
+				),
+				Ok(None),
+			);
+
+			// relay + message delivery call batch is accepted
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&relay_finality_and_delivery_batch_call(200, 200)
+				),
+				Ok(Some(relay_finality_pre_dispatch_data().call_info)),
+			);
+
+			// relay + message confirmation call batch is accepted
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&relay_finality_and_confirmation_batch_call(200, 200)
+				),
+				Ok(Some(relay_finality_confirmation_pre_dispatch_data().call_info)),
+			);
+
+			// message delivery call batch is accepted
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&message_delivery_call(200)
+				),
+				Ok(Some(delivery_pre_dispatch_data().call_info)),
+			);
+
+			// message confirmation call batch is accepted
+			assert_eq!(
+				TestGrandpaExtensionProvider::parse_and_check_for_obsolete_call(
+					&message_confirmation_call(200)
+				),
+				Ok(Some(confirmation_pre_dispatch_data().call_info)),
+			);
+		});
+	}
+
+	#[test]
+	fn grandpa_ext_rejects_batch_with_obsolete_relay_chain_header() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+
+			assert_eq!(
+				run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(100, 200)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+
+			assert_eq!(
+				run_grandpa_validate(relay_finality_and_delivery_batch_call(100, 200)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+		});
+	}
+
+	#[test]
+	fn grandpa_ext_rejects_calls_with_obsolete_messages() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+
+			assert_eq!(
+				run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(200, 100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+			assert_eq!(
+				run_grandpa_pre_dispatch(relay_finality_and_confirmation_batch_call(200, 100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+
+			assert_eq!(
+				run_grandpa_validate(relay_finality_and_delivery_batch_call(200, 100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+			assert_eq!(
+				run_grandpa_validate(relay_finality_and_confirmation_batch_call(200, 100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+
+			assert_eq!(
+				run_grandpa_pre_dispatch(message_delivery_call(100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+			assert_eq!(
+				run_grandpa_pre_dispatch(message_confirmation_call(100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+
+			assert_eq!(
+				run_grandpa_validate(message_delivery_call(100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+			assert_eq!(
+				run_grandpa_validate(message_confirmation_call(100)),
+				Err(TransactionValidityError::Invalid(InvalidTransaction::Stale)),
+			);
+		});
+	}
+
+	#[test]
+	fn grandpa_ext_accepts_calls_with_new_messages() {
+		run_test(|| {
+			initialize_environment(100, 100, 100);
+
+			assert_eq!(
+				run_grandpa_pre_dispatch(relay_finality_and_delivery_batch_call(200, 200)),
+				Ok(Some(relay_finality_pre_dispatch_data()),)
+			);
+			assert_eq!(
+				run_grandpa_pre_dispatch(relay_finality_and_confirmation_batch_call(200, 200)),
+				Ok(Some(relay_finality_confirmation_pre_dispatch_data())),
+			);
+
+			assert_eq!(
+				run_grandpa_validate(relay_finality_and_delivery_batch_call(200, 200)),
+				Ok(Default::default()),
+			);
+			assert_eq!(
+				run_grandpa_validate(relay_finality_and_confirmation_batch_call(200, 200)),
+				Ok(Default::default()),
+			);
+
+			assert_eq!(
+				run_grandpa_pre_dispatch(message_delivery_call(200)),
+				Ok(Some(delivery_pre_dispatch_data())),
+			);
+			assert_eq!(
+				run_grandpa_pre_dispatch(message_confirmation_call(200)),
+				Ok(Some(confirmation_pre_dispatch_data())),
+			);
+
+			assert_eq!(run_grandpa_validate(message_delivery_call(200)), Ok(Default::default()),);
+			assert_eq!(
+				run_grandpa_validate(message_confirmation_call(200)),
+				Ok(Default::default()),
+			);
+		});
+	}
 }
diff --git a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs
index 3a919648df47f091b76f0e16b9cd98b22f1970ff..66e0dad05895c7df72cda7158c39e589b78ada73 100644
--- a/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs
+++ b/bridges/primitives/chain-bridge-hub-kusama/src/lib.rs
@@ -29,7 +29,6 @@ use frame_support::{
 	sp_runtime::{MultiAddress, MultiSigner},
 };
 use sp_runtime::RuntimeDebug;
-use sp_std::prelude::Vec;
 
 /// BridgeHubKusama parachain.
 #[derive(RuntimeDebug)]
diff --git a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs
index bf8d8e07c3a61c83a8f1f0cc253c45557dd1b703..c3661c1adcada619bb319ae8853c20623565c0c9 100644
--- a/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs
+++ b/bridges/primitives/chain-bridge-hub-polkadot/src/lib.rs
@@ -26,7 +26,6 @@ use bp_runtime::{
 };
 use frame_support::dispatch::DispatchClass;
 use sp_runtime::RuntimeDebug;
-use sp_std::prelude::Vec;
 
 /// BridgeHubPolkadot parachain.
 #[derive(RuntimeDebug)]
diff --git a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs
index b726c62ac42b3270d72c7eebc6d3f5faff54c166..a50bda23ac8d358259d725e0c93be31983caba0d 100644
--- a/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs
+++ b/bridges/primitives/chain-bridge-hub-rococo/src/lib.rs
@@ -26,7 +26,7 @@ use bp_runtime::{
 };
 use frame_support::dispatch::DispatchClass;
 use sp_runtime::{MultiAddress, MultiSigner, RuntimeDebug};
-use sp_std::prelude::Vec;
+
 /// BridgeHubRococo parachain.
 #[derive(RuntimeDebug)]
 pub struct BridgeHubRococo;
diff --git a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs
index 5e4758645d9ea63fedbd17b39c88ee208e725f96..ce4600f5ff35d674afb1f6a6bdd24a044990fe78 100644
--- a/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs
+++ b/bridges/primitives/chain-bridge-hub-wococo/src/lib.rs
@@ -26,7 +26,6 @@ use bp_runtime::{
 };
 use frame_support::dispatch::DispatchClass;
 use sp_runtime::RuntimeDebug;
-use sp_std::prelude::Vec;
 
 /// BridgeHubWococo parachain.
 #[derive(RuntimeDebug)]
diff --git a/bridges/primitives/chain-kusama/src/lib.rs b/bridges/primitives/chain-kusama/src/lib.rs
index 8c3fbd9c203e5e81ebfc248259cd823255d826f2..d5748aa132cea6caddeabd102b62345bbdc6153f 100644
--- a/bridges/primitives/chain-kusama/src/lib.rs
+++ b/bridges/primitives/chain-kusama/src/lib.rs
@@ -23,7 +23,6 @@ pub use bp_polkadot_core::*;
 use bp_header_chain::ChainWithGrandpa;
 use bp_runtime::{decl_bridge_finality_runtime_apis, Chain};
 use frame_support::weights::Weight;
-use sp_std::prelude::Vec;
 
 /// Kusama Chain
 pub struct Kusama;
diff --git a/bridges/primitives/chain-millau/src/lib.rs b/bridges/primitives/chain-millau/src/lib.rs
index ae14f7f362953093f55550196055bb2b73da8d70..0c312871666616ad231ee77fb49e215391cf2a72 100644
--- a/bridges/primitives/chain-millau/src/lib.rs
+++ b/bridges/primitives/chain-millau/src/lib.rs
@@ -22,9 +22,7 @@ mod millau_hash;
 
 use bp_beefy::ChainWithBeefy;
 use bp_header_chain::ChainWithGrandpa;
-use bp_messages::{
-	InboundMessageDetails, LaneId, MessageNonce, MessagePayload, OutboundMessageDetails,
-};
+use bp_messages::MessageNonce;
 use bp_runtime::{decl_bridge_finality_runtime_apis, decl_bridge_runtime_apis, Chain};
 use frame_support::{
 	dispatch::DispatchClass,
diff --git a/bridges/primitives/chain-polkadot-bulletin/Cargo.toml b/bridges/primitives/chain-polkadot-bulletin/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..4ad1bb497c67edfee60e957c541130fc813f5451
--- /dev/null
+++ b/bridges/primitives/chain-polkadot-bulletin/Cargo.toml
@@ -0,0 +1,41 @@
+[package]
+name = "bp-polkadot-bulletin"
+description = "Primitives of Polkadot Bulletin chain runtime."
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", default-features = false, features = ["derive"] }
+scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
+
+# Bridge Dependencies
+
+bp-header-chain = { path = "../header-chain", default-features = false }
+bp-messages = { path = "../messages", default-features = false }
+bp-polkadot-core = { path = "../polkadot-core", default-features = false }
+bp-runtime = { path = "../runtime", default-features = false }
+
+# Substrate Based Dependencies
+
+frame-support = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master", default-features = false }
+
+[features]
+default = ["std"]
+std = [
+	"bp-header-chain/std",
+	"bp-messages/std",
+	"bp-polkadot-core/std",
+	"bp-runtime/std",
+	"codec/std",
+	"frame-support/std",
+	"frame-system/std",
+	"sp-api/std",
+	"sp-runtime/std",
+	"sp-std/std",
+]
diff --git a/bridges/primitives/chain-polkadot-bulletin/src/lib.rs b/bridges/primitives/chain-polkadot-bulletin/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fcc6e90eb1b298e703b7c4b1a83c914f0a018031
--- /dev/null
+++ b/bridges/primitives/chain-polkadot-bulletin/src/lib.rs
@@ -0,0 +1,215 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Polkadot Bulletin Chain primitives.
+
+#![warn(missing_docs)]
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use bp_header_chain::ChainWithGrandpa;
+use bp_messages::MessageNonce;
+use bp_runtime::{
+	decl_bridge_finality_runtime_apis, decl_bridge_messages_runtime_apis,
+	extensions::{
+		CheckEra, CheckGenesis, CheckNonZeroSender, CheckNonce, CheckSpecVersion, CheckTxVersion,
+		CheckWeight, GenericSignedExtension, GenericSignedExtensionSchema,
+	},
+	Chain, TransactionEra,
+};
+use codec::{Decode, Encode};
+use frame_support::{
+	dispatch::DispatchClass,
+	parameter_types,
+	weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight},
+};
+use frame_system::limits;
+use scale_info::TypeInfo;
+use sp_runtime::{traits::DispatchInfoOf, transaction_validity::TransactionValidityError, Perbill};
+
+// This chain reuses most of Polkadot primitives.
+pub use bp_polkadot_core::{
+	AccountAddress, AccountId, Balance, Block, BlockNumber, Hash, Hasher, Header, Nonce, Signature,
+	SignedBlock, UncheckedExtrinsic, AVERAGE_HEADER_SIZE_IN_JUSTIFICATION,
+	EXTRA_STORAGE_PROOF_SIZE, MAX_HEADER_SIZE, REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY,
+};
+
+/// Maximal number of GRANDPA authorities at Polkadot Bulletin chain.
+pub const MAX_AUTHORITIES_COUNT: u32 = 100;
+
+/// Name of the With-Polkadot Bulletin chain GRANDPA pallet instance that is deployed at bridged
+/// chains.
+pub const WITH_POLKADOT_BULLETIN_GRANDPA_PALLET_NAME: &str = "BridgePolkadotBulletinGrandpa";
+/// Name of the With-Polkadot Bulletin chain messages pallet instance that is deployed at bridged
+/// chains.
+pub const WITH_POLKADOT_BULLETIN_MESSAGES_PALLET_NAME: &str = "BridgePolkadotBulletinMessages";
+
+// There are fewer system operations on this chain (e.g. staking, governance, etc.). Use a higher
+// percentage of the block for data storage.
+const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(90);
+
+// Re following constants - we are using the same values at Cumulus parachains. They are limited
+// by the maximal transaction weight/size. Since block limits at Bulletin Chain are larger than
+// at the Cumulus Bridgeg Hubs, we could reuse the same values.
+
+/// Maximal number of unrewarded relayer entries at inbound lane for Cumulus-based parachains.
+pub const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 1024;
+
+/// Maximal number of unconfirmed messages at inbound lane for Cumulus-based parachains.
+pub const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 4096;
+
+/// This signed extension is used to ensure that the chain transactions are signed by proper
+pub type ValidateSigned = GenericSignedExtensionSchema<(), ()>;
+
+/// Signed extension schema, used by Polkadot Bulletin.
+pub type SignedExtensionSchema = GenericSignedExtension<(
+	(
+		CheckNonZeroSender,
+		CheckSpecVersion,
+		CheckTxVersion,
+		CheckGenesis<Hash>,
+		CheckEra<Hash>,
+		CheckNonce<Nonce>,
+		CheckWeight,
+	),
+	ValidateSigned,
+)>;
+
+/// Signed extension, used by Polkadot Bulletin.
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+pub struct SignedExtension(SignedExtensionSchema);
+
+impl sp_runtime::traits::SignedExtension for SignedExtension {
+	const IDENTIFIER: &'static str = "Not needed.";
+	type AccountId = ();
+	type Call = ();
+	type AdditionalSigned =
+		<SignedExtensionSchema as sp_runtime::traits::SignedExtension>::AdditionalSigned;
+	type Pre = ();
+
+	fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
+		self.0.additional_signed()
+	}
+
+	fn pre_dispatch(
+		self,
+		_who: &Self::AccountId,
+		_call: &Self::Call,
+		_info: &DispatchInfoOf<Self::Call>,
+		_len: usize,
+	) -> Result<Self::Pre, TransactionValidityError> {
+		Ok(())
+	}
+}
+
+impl SignedExtension {
+	/// Create signed extension from its components.
+	pub fn from_params(
+		spec_version: u32,
+		transaction_version: u32,
+		era: TransactionEra<BlockNumber, Hash>,
+		genesis_hash: Hash,
+		nonce: Nonce,
+	) -> Self {
+		Self(GenericSignedExtension::new(
+			(
+				(
+					(),              // non-zero sender
+					(),              // spec version
+					(),              // tx version
+					(),              // genesis
+					era.frame_era(), // era
+					nonce.into(),    // nonce (compact encoding)
+					(),              // Check weight
+				),
+				(),
+			),
+			Some((
+				(
+					(),
+					spec_version,
+					transaction_version,
+					genesis_hash,
+					era.signed_payload(genesis_hash),
+					(),
+					(),
+				),
+				(),
+			)),
+		))
+	}
+
+	/// Return transaction nonce.
+	pub fn nonce(&self) -> Nonce {
+		let common_payload = self.0.payload.0;
+		common_payload.5 .0
+	}
+}
+
+parameter_types! {
+	/// We allow for 2 seconds of compute with a 6 second average block time.
+	pub BlockWeights: limits::BlockWeights = limits::BlockWeights::with_sensible_defaults(
+			Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX),
+			NORMAL_DISPATCH_RATIO,
+		);
+	// Note: Max transaction size is 8 MB. Set max block size to 10 MB to facilitate data storage.
+	// This is double the "normal" Relay Chain block length limit.
+	/// Maximal block length at Polkadot Bulletin chain.
+	pub BlockLength: limits::BlockLength = limits::BlockLength::max_with_normal_ratio(
+		10 * 1024 * 1024,
+		NORMAL_DISPATCH_RATIO,
+	);
+}
+
+/// Polkadot Bulletin Chain declaration.
+pub struct PolkadotBulletin;
+
+impl Chain for PolkadotBulletin {
+	type BlockNumber = BlockNumber;
+	type Hash = Hash;
+	type Hasher = Hasher;
+	type Header = Header;
+
+	type AccountId = AccountId;
+	// The Bulletin Chain is a permissioned blockchain without any balances. Our `Chain` trait
+	// requires balance type, which is then used by various bridge infrastructure code. However
+	// this code is optional and we are not planning to use it in our bridge.
+	type Balance = Balance;
+	type Nonce = Nonce;
+	type Signature = Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		*BlockLength::get().max.get(DispatchClass::Normal)
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		BlockWeights::get()
+			.get(DispatchClass::Normal)
+			.max_extrinsic
+			.unwrap_or(Weight::MAX)
+	}
+}
+
+impl ChainWithGrandpa for PolkadotBulletin {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = WITH_POLKADOT_BULLETIN_GRANDPA_PALLET_NAME;
+	const MAX_AUTHORITIES_COUNT: u32 = MAX_AUTHORITIES_COUNT;
+	const REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY: u32 =
+		REASONABLE_HEADERS_IN_JUSTIFICATON_ANCESTRY;
+	const MAX_HEADER_SIZE: u32 = MAX_HEADER_SIZE;
+	const AVERAGE_HEADER_SIZE_IN_JUSTIFICATION: u32 = AVERAGE_HEADER_SIZE_IN_JUSTIFICATION;
+}
+
+decl_bridge_finality_runtime_apis!(polkadot_bulletin, grandpa);
+decl_bridge_messages_runtime_apis!(polkadot_bulletin);
diff --git a/bridges/primitives/chain-polkadot/src/lib.rs b/bridges/primitives/chain-polkadot/src/lib.rs
index d1d6f74543121afb47c5fdc423fd29aae5e2615e..61c8ca927d807ac6c169bb30d324f9720118e010 100644
--- a/bridges/primitives/chain-polkadot/src/lib.rs
+++ b/bridges/primitives/chain-polkadot/src/lib.rs
@@ -23,7 +23,6 @@ pub use bp_polkadot_core::*;
 use bp_header_chain::ChainWithGrandpa;
 use bp_runtime::{decl_bridge_finality_runtime_apis, extensions::PrevalidateAttests, Chain};
 use frame_support::weights::Weight;
-use sp_std::prelude::Vec;
 
 /// Polkadot Chain
 pub struct Polkadot;
diff --git a/bridges/primitives/chain-rialto-parachain/src/lib.rs b/bridges/primitives/chain-rialto-parachain/src/lib.rs
index c30372b5a07884753f97999c8c230cb3f7f2895c..18655503edddf07354353c263de276bc2a1c547c 100644
--- a/bridges/primitives/chain-rialto-parachain/src/lib.rs
+++ b/bridges/primitives/chain-rialto-parachain/src/lib.rs
@@ -18,9 +18,7 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{
-	InboundMessageDetails, LaneId, MessageNonce, MessagePayload, OutboundMessageDetails,
-};
+use bp_messages::MessageNonce;
 use bp_runtime::{decl_bridge_runtime_apis, Chain, Parachain};
 use frame_support::{
 	dispatch::DispatchClass,
@@ -32,7 +30,6 @@ use sp_runtime::{
 	traits::{BlakeTwo256, IdentifyAccount, Verify},
 	MultiSignature, MultiSigner, Perbill, RuntimeDebug,
 };
-use sp_std::vec::Vec;
 
 /// Identifier of RialtoParachain in the Rialto relay chain.
 ///
diff --git a/bridges/primitives/chain-rialto/src/lib.rs b/bridges/primitives/chain-rialto/src/lib.rs
index 34d7c8f7476680c68ff9626b655494079555b422..9d5215c10292a41d7dc1acc26874cb19019285c0 100644
--- a/bridges/primitives/chain-rialto/src/lib.rs
+++ b/bridges/primitives/chain-rialto/src/lib.rs
@@ -19,9 +19,7 @@
 #![allow(clippy::too_many_arguments)]
 
 use bp_header_chain::ChainWithGrandpa;
-use bp_messages::{
-	InboundMessageDetails, LaneId, MessageNonce, MessagePayload, OutboundMessageDetails,
-};
+use bp_messages::MessageNonce;
 use bp_runtime::{decl_bridge_finality_runtime_apis, decl_bridge_runtime_apis, Chain};
 use frame_support::{
 	dispatch::DispatchClass,
@@ -33,7 +31,6 @@ use sp_runtime::{
 	traits::{BlakeTwo256, IdentifyAccount, Verify},
 	MultiSignature, MultiSigner, Perbill, RuntimeDebug,
 };
-use sp_std::prelude::*;
 
 /// Number of extra bytes (excluding size of storage value itself) of storage proof, built at
 /// Rialto chain. This mostly depends on number of entries (and their density) in the storage trie.
diff --git a/bridges/primitives/chain-rococo/src/lib.rs b/bridges/primitives/chain-rococo/src/lib.rs
index 1589d14ea5143da0bc0244b23f502a2b99cf9233..5436ad846468cda632aab9ffcf46748f4c1546f8 100644
--- a/bridges/primitives/chain-rococo/src/lib.rs
+++ b/bridges/primitives/chain-rococo/src/lib.rs
@@ -23,7 +23,6 @@ pub use bp_polkadot_core::*;
 use bp_header_chain::ChainWithGrandpa;
 use bp_runtime::{decl_bridge_finality_runtime_apis, Chain};
 use frame_support::{parameter_types, weights::Weight};
-use sp_std::prelude::Vec;
 
 /// Rococo Chain
 pub struct Rococo;
diff --git a/bridges/primitives/chain-wococo/src/lib.rs b/bridges/primitives/chain-wococo/src/lib.rs
index 5b5bde82690442c3a2c26b424163eeb58b5892e9..b1df65630beffdaad80ce8f22ba357dbf766ea0f 100644
--- a/bridges/primitives/chain-wococo/src/lib.rs
+++ b/bridges/primitives/chain-wococo/src/lib.rs
@@ -26,7 +26,6 @@ pub use bp_rococo::{
 use bp_header_chain::ChainWithGrandpa;
 use bp_runtime::{decl_bridge_finality_runtime_apis, Chain};
 use frame_support::weights::Weight;
-use sp_std::prelude::Vec;
 
 /// Wococo Chain
 pub struct Wococo;
diff --git a/bridges/primitives/runtime/src/chain.rs b/bridges/primitives/runtime/src/chain.rs
index 5caaebd42babc32124ec724f10129f84f39423f7..e1809e145248f1cb37022ed4c2bb29d1547b2b2a 100644
--- a/bridges/primitives/runtime/src/chain.rs
+++ b/bridges/primitives/runtime/src/chain.rs
@@ -311,7 +311,7 @@ macro_rules! decl_bridge_finality_runtime_apis {
 						$(
 							/// Returns the justifications accepted in the current block.
 							fn [<synced_headers_ $consensus:lower _info>](
-							) -> Vec<$justification_type>;
+							) -> sp_std::vec::Vec<$justification_type>;
 						)?
 					}
 				}
@@ -360,10 +360,10 @@ macro_rules! decl_bridge_messages_runtime_apis {
 						/// If some (or all) messages are missing from the storage, they'll also will
 						/// be missing from the resulting vector. The vector is ordered by the nonce.
 						fn message_details(
-							lane: LaneId,
-							begin: MessageNonce,
-							end: MessageNonce,
-						) -> Vec<OutboundMessageDetails>;
+							lane: bp_messages::LaneId,
+							begin: bp_messages::MessageNonce,
+							end: bp_messages::MessageNonce,
+						) -> sp_std::vec::Vec<bp_messages::OutboundMessageDetails>;
 					}
 
 					/// Inbound message lane API for messages sent by this chain.
@@ -376,9 +376,9 @@ macro_rules! decl_bridge_messages_runtime_apis {
 					pub trait [<From $chain:camel InboundLaneApi>] {
 						/// Return details of given inbound messages.
 						fn message_details(
-							lane: LaneId,
-							messages: Vec<(MessagePayload, OutboundMessageDetails)>,
-						) -> Vec<InboundMessageDetails>;
+							lane: bp_messages::LaneId,
+							messages: sp_std::vec::Vec<(bp_messages::MessagePayload, bp_messages::OutboundMessageDetails)>,
+						) -> sp_std::vec::Vec<bp_messages::InboundMessageDetails>;
 					}
 				}
 			}
diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs
index e5f1afb4161d665c1ab244b0761a7aecc3fe90d4..16cd603fe3449a318774e1ebbb3b394573df435d 100644
--- a/bridges/primitives/runtime/src/lib.rs
+++ b/bridges/primitives/runtime/src/lib.rs
@@ -73,6 +73,9 @@ pub const MILLAU_CHAIN_ID: ChainId = *b"mlau";
 /// Polkadot chain id.
 pub const POLKADOT_CHAIN_ID: ChainId = *b"pdot";
 
+/// Polkadot Bulletin chain id.
+pub const POLKADOT_BULLETIN_CHAIN_ID: ChainId = *b"pdbc";
+
 /// Kusama chain id.
 pub const KUSAMA_CHAIN_ID: ChainId = *b"ksma";
 
diff --git a/bridges/relays/bin-substrate/Cargo.toml b/bridges/relays/bin-substrate/Cargo.toml
index c9d12a7029678405f110f33ff89e20d814e53e9d..795d91326a157b681036fbca169b12bc9cce351c 100644
--- a/bridges/relays/bin-substrate/Cargo.toml
+++ b/bridges/relays/bin-substrate/Cargo.toml
@@ -26,6 +26,7 @@ bp-header-chain = { path = "../../primitives/header-chain" }
 bp-messages = { path = "../../primitives/messages" }
 bp-parachains = { path = "../../primitives/parachains" }
 bp-millau = { path = "../../primitives/chain-millau" }
+bp-polkadot-bulletin = { path = "../../primitives/chain-polkadot-bulletin" }
 bp-polkadot-core = { path = "../../primitives/polkadot-core" }
 bp-rialto = { path = "../../primitives/chain-rialto" }
 bp-rialto-parachain = { path = "../../primitives/chain-rialto-parachain" }
@@ -43,6 +44,7 @@ relay-bridge-hub-rococo-client = { path = "../client-bridge-hub-rococo" }
 relay-bridge-hub-wococo-client = { path = "../client-bridge-hub-wococo" }
 relay-kusama-client = { path = "../client-kusama" }
 relay-polkadot-client = { path = "../client-polkadot" }
+relay-polkadot-bulletin-client = { path = "../client-polkadot-bulletin" }
 relay-rococo-client = { path = "../client-rococo" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
diff --git a/bridges/relays/bin-substrate/src/bridges/mod.rs b/bridges/relays/bin-substrate/src/bridges/mod.rs
index 62e69cc0e5fbd87bd2e3aeb84b494884ff9e8a74..53fe5e0c192d834fb58ced64a7a80e0d7719cd7d 100644
--- a/bridges/relays/bin-substrate/src/bridges/mod.rs
+++ b/bridges/relays/bin-substrate/src/bridges/mod.rs
@@ -17,6 +17,7 @@
 //! Declaration of all bridges that the relay is able to serve.
 
 pub mod kusama_polkadot;
+pub mod polkadot_bulletin;
 pub mod rialto_millau;
 pub mod rialto_parachain_millau;
 pub mod rococo_wococo;
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/bridge_hub_polkadot_messages_to_polkadot_bulletin.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/bridge_hub_polkadot_messages_to_polkadot_bulletin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..614b42de4ed898bc3ae9d8cf7fef68135dc4b4a1
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/bridge_hub_polkadot_messages_to_polkadot_bulletin.rs
@@ -0,0 +1,65 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! BridgeHubPolkadot-to-PolkadotBulletin messages sync entrypoint.
+
+use crate::cli::bridge::{CliBridgeBase, MessagesCliBridge};
+use relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+use relay_polkadot_bulletin_client::PolkadotBulletin;
+use substrate_relay_helper::{messages_lane::SubstrateMessageLane, UtilityPalletBatchCallBuilder};
+
+/// BridgeHubPolkadot-to-PolkadotBulletin messages bridge.
+pub struct BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge {}
+
+impl CliBridgeBase for BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge {
+	type Source = BridgeHubPolkadot;
+	type Target = PolkadotBulletin;
+}
+
+impl MessagesCliBridge for BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge {
+	type MessagesLane = BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane;
+}
+
+substrate_relay_helper::generate_receive_message_proof_call_builder!(
+	BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane,
+	BridgeHubPolkadotMessagesToPolkadotBulletinMessageLaneReceiveMessagesProofCallBuilder,
+	relay_polkadot_bulletin_client::RuntimeCall::BridgePolkadotBridgeHubMessages,
+	relay_polkadot_bulletin_client::BridgePolkadotBridgeHubMessagesCall::receive_messages_proof
+);
+
+substrate_relay_helper::generate_receive_message_delivery_proof_call_builder!(
+	BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane,
+	BridgeHubPolkadotMessagesToPolkadotBulletinMessageLaneReceiveMessagesDeliveryProofCallBuilder,
+	relay_bridge_hub_polkadot_client::runtime::Call::BridgePolkadotBulletinMessages,
+	relay_bridge_hub_polkadot_client::runtime::BridgePolkadotBulletinMessagesCall::receive_messages_delivery_proof
+);
+
+/// BridgeHubPolkadot-to-PolkadotBulletin messages lane.
+#[derive(Clone, Debug)]
+pub struct BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane;
+
+impl SubstrateMessageLane for BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane {
+	type SourceChain = BridgeHubPolkadot;
+	type TargetChain = PolkadotBulletin;
+
+	type ReceiveMessagesProofCallBuilder =
+		BridgeHubPolkadotMessagesToPolkadotBulletinMessageLaneReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		BridgeHubPolkadotMessagesToPolkadotBulletinMessageLaneReceiveMessagesDeliveryProofCallBuilder;
+
+	type SourceBatchCallBuilder = UtilityPalletBatchCallBuilder<BridgeHubPolkadot>;
+	type TargetBatchCallBuilder = ();
+}
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/mod.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d7c740f601abcb886fe4937172e18759dbcdab2f
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/mod.rs
@@ -0,0 +1,23 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Declaration of all bridges between Polkadot Bulletin Chain and Polkadot Bridge Hub.
+
+pub mod bridge_hub_polkadot_messages_to_polkadot_bulletin;
+pub mod polkadot_bulletin_headers_to_bridge_hub_polkadot;
+pub mod polkadot_bulletin_messages_to_bridge_hub_polkadot;
+pub mod polkadot_headers_to_polkadot_bulletin;
+pub mod polkadot_parachains_to_polkadot_bulletin;
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_headers_to_bridge_hub_polkadot.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_headers_to_bridge_hub_polkadot.rs
new file mode 100644
index 0000000000000000000000000000000000000000..33f59feda3bae45f142b73fd5abfec20e980844e
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_headers_to_bridge_hub_polkadot.rs
@@ -0,0 +1,101 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! PolkadotBulletin-to-BridgeHubPolkadot headers sync entrypoint.
+
+use crate::cli::bridge::{
+	CliBridgeBase, MessagesCliBridge, RelayToRelayEquivocationDetectionCliBridge,
+	RelayToRelayHeadersCliBridge,
+};
+
+use async_trait::async_trait;
+use relay_substrate_client::{AccountKeyPairOf, Client};
+use substrate_relay_helper::{
+	equivocation::SubstrateEquivocationDetectionPipeline,
+	finality::SubstrateFinalitySyncPipeline,
+	finality_base::{engine::Grandpa as GrandpaFinalityEngine, SubstrateFinalityPipeline},
+	TransactionParams,
+};
+
+/// Description of `PolkadotBulletin` -> `PolkadotBridgeHub` finalized headers bridge.
+#[derive(Clone, Debug)]
+pub struct PolkadotBulletinFinalityToBridgeHubPolkadot;
+
+substrate_relay_helper::generate_submit_finality_proof_call_builder!(
+	PolkadotBulletinFinalityToBridgeHubPolkadot,
+	SubmitFinalityProofCallBuilder,
+	relay_bridge_hub_polkadot_client::runtime::Call::BridgePolkadotBulletinGrandpa,
+	relay_bridge_hub_polkadot_client::runtime::BridgePolkadotBulletinGrandpaCall::submit_finality_proof
+);
+
+substrate_relay_helper::generate_report_equivocation_call_builder!(
+	PolkadotBulletinFinalityToBridgeHubPolkadot,
+	ReportEquivocationCallBuilder,
+	relay_polkadot_bulletin_client::RuntimeCall::Grandpa,
+	relay_polkadot_bulletin_client::GrandpaCall::report_equivocation
+);
+
+#[async_trait]
+impl SubstrateFinalityPipeline for PolkadotBulletinFinalityToBridgeHubPolkadot {
+	type SourceChain = relay_polkadot_bulletin_client::PolkadotBulletin;
+	type TargetChain = relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+
+	type FinalityEngine = GrandpaFinalityEngine<Self::SourceChain>;
+}
+
+#[async_trait]
+impl SubstrateFinalitySyncPipeline for PolkadotBulletinFinalityToBridgeHubPolkadot {
+	type SubmitFinalityProofCallBuilder = SubmitFinalityProofCallBuilder;
+
+	async fn start_relay_guards(
+		target_client: &Client<Self::TargetChain>,
+		_transaction_params: &TransactionParams<AccountKeyPairOf<Self::TargetChain>>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		if enable_version_guard {
+			relay_substrate_client::guard::abort_on_spec_version_change(
+				target_client.clone(),
+				target_client.simple_runtime_version().await?.spec_version,
+			);
+		}
+		Ok(())
+	}
+}
+
+#[async_trait]
+impl SubstrateEquivocationDetectionPipeline for PolkadotBulletinFinalityToBridgeHubPolkadot {
+	type ReportEquivocationCallBuilder = ReportEquivocationCallBuilder;
+}
+
+/// `PolkadotBulletin` to BridgeHub `Polkadot` bridge definition.
+pub struct PolkadotBulletinToBridgeHubPolkadotCliBridge {}
+
+impl CliBridgeBase for PolkadotBulletinToBridgeHubPolkadotCliBridge {
+	type Source = relay_polkadot_bulletin_client::PolkadotBulletin;
+	type Target = relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+}
+
+impl RelayToRelayHeadersCliBridge for PolkadotBulletinToBridgeHubPolkadotCliBridge {
+	type Finality = PolkadotBulletinFinalityToBridgeHubPolkadot;
+}
+
+impl RelayToRelayEquivocationDetectionCliBridge for PolkadotBulletinToBridgeHubPolkadotCliBridge {
+	type Equivocation = PolkadotBulletinFinalityToBridgeHubPolkadot;
+}
+
+impl MessagesCliBridge for PolkadotBulletinToBridgeHubPolkadotCliBridge {
+	type MessagesLane = crate::bridges::polkadot_bulletin::polkadot_bulletin_messages_to_bridge_hub_polkadot::PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane;
+}
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_messages_to_bridge_hub_polkadot.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_messages_to_bridge_hub_polkadot.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ddfbcea495f902d7b5e718992263d34d396cf6ae
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_bulletin_messages_to_bridge_hub_polkadot.rs
@@ -0,0 +1,65 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! PolkadotBulletin-to-BridgeHubPolkadot messages sync entrypoint.
+
+use crate::cli::bridge::{CliBridgeBase, MessagesCliBridge};
+use relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+use relay_polkadot_bulletin_client::PolkadotBulletin;
+use substrate_relay_helper::{messages_lane::SubstrateMessageLane, UtilityPalletBatchCallBuilder};
+
+/// PolkadotBulletin-to-BridgeHubPolkadot messages bridge.
+pub struct PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge {}
+
+impl CliBridgeBase for PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge {
+	type Source = PolkadotBulletin;
+	type Target = BridgeHubPolkadot;
+}
+
+impl MessagesCliBridge for PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge {
+	type MessagesLane = PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane;
+}
+
+substrate_relay_helper::generate_receive_message_proof_call_builder!(
+	PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane,
+	PolkadotBulletinMessagesToBridgeHubPolkadotMessageLaneReceiveMessagesProofCallBuilder,
+	relay_bridge_hub_polkadot_client::runtime::Call::BridgePolkadotBulletinMessages,
+	relay_bridge_hub_polkadot_client::runtime::BridgePolkadotBulletinMessagesCall::receive_messages_proof
+);
+
+substrate_relay_helper::generate_receive_message_delivery_proof_call_builder!(
+	PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane,
+	PolkadotBulletinMessagesToBridgeHubPolkadotMessageLaneReceiveMessagesDeliveryProofCallBuilder,
+	relay_polkadot_bulletin_client::RuntimeCall::BridgePolkadotBridgeHubMessages,
+	relay_polkadot_bulletin_client::BridgePolkadotBridgeHubMessagesCall::receive_messages_delivery_proof
+);
+
+/// PolkadotBulletin-to-BridgeHubPolkadot messages lane.
+#[derive(Clone, Debug)]
+pub struct PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane;
+
+impl SubstrateMessageLane for PolkadotBulletinMessagesToBridgeHubPolkadotMessageLane {
+	type SourceChain = PolkadotBulletin;
+	type TargetChain = BridgeHubPolkadot;
+
+	type ReceiveMessagesProofCallBuilder =
+		PolkadotBulletinMessagesToBridgeHubPolkadotMessageLaneReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		PolkadotBulletinMessagesToBridgeHubPolkadotMessageLaneReceiveMessagesDeliveryProofCallBuilder;
+
+	type SourceBatchCallBuilder = ();
+	type TargetBatchCallBuilder = UtilityPalletBatchCallBuilder<BridgeHubPolkadot>;
+}
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_headers_to_polkadot_bulletin.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_headers_to_polkadot_bulletin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7770fd1559444cef9de4ac3b3327b586e4617aab
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_headers_to_polkadot_bulletin.rs
@@ -0,0 +1,96 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Polkadot-to-PolkadotBulletin headers sync entrypoint.
+
+use crate::cli::bridge::{
+	CliBridgeBase, RelayToRelayEquivocationDetectionCliBridge, RelayToRelayHeadersCliBridge,
+};
+
+use async_trait::async_trait;
+use relay_substrate_client::{AccountKeyPairOf, Client};
+use substrate_relay_helper::{
+	equivocation::SubstrateEquivocationDetectionPipeline,
+	finality::SubstrateFinalitySyncPipeline,
+	finality_base::{engine::Grandpa as GrandpaFinalityEngine, SubstrateFinalityPipeline},
+	TransactionParams,
+};
+
+/// Description of Polkadot -> `PolkadotBulletin` finalized headers bridge.
+#[derive(Clone, Debug)]
+pub struct PolkadotFinalityToPolkadotBulletin;
+
+substrate_relay_helper::generate_submit_finality_proof_call_builder!(
+	PolkadotFinalityToPolkadotBulletin,
+	SubmitFinalityProofCallBuilder,
+	relay_polkadot_bulletin_client::RuntimeCall::BridgePolkadotGrandpa,
+	relay_polkadot_bulletin_client::BridgePolkadotGrandpaCall::submit_finality_proof
+);
+
+substrate_relay_helper::generate_report_equivocation_call_builder!(
+	PolkadotFinalityToPolkadotBulletin,
+	ReportEquivocationCallBuilder,
+	relay_polkadot_client::RuntimeCall::Grandpa,
+	relay_polkadot_client::GrandpaCall::report_equivocation
+);
+
+#[async_trait]
+impl SubstrateFinalityPipeline for PolkadotFinalityToPolkadotBulletin {
+	type SourceChain = relay_polkadot_client::Polkadot;
+	type TargetChain = relay_polkadot_bulletin_client::PolkadotBulletin;
+
+	type FinalityEngine = GrandpaFinalityEngine<Self::SourceChain>;
+}
+
+#[async_trait]
+impl SubstrateFinalitySyncPipeline for PolkadotFinalityToPolkadotBulletin {
+	type SubmitFinalityProofCallBuilder = SubmitFinalityProofCallBuilder;
+
+	async fn start_relay_guards(
+		target_client: &Client<Self::TargetChain>,
+		_transaction_params: &TransactionParams<AccountKeyPairOf<Self::TargetChain>>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		if enable_version_guard {
+			relay_substrate_client::guard::abort_on_spec_version_change(
+				target_client.clone(),
+				target_client.simple_runtime_version().await?.spec_version,
+			);
+		}
+		Ok(())
+	}
+}
+
+#[async_trait]
+impl SubstrateEquivocationDetectionPipeline for PolkadotFinalityToPolkadotBulletin {
+	type ReportEquivocationCallBuilder = ReportEquivocationCallBuilder;
+}
+
+/// `Polkadot` to BridgeHub `PolkadotBulletin` bridge definition.
+pub struct PolkadotToPolkadotBulletinCliBridge {}
+
+impl CliBridgeBase for PolkadotToPolkadotBulletinCliBridge {
+	type Source = relay_polkadot_client::Polkadot;
+	type Target = relay_polkadot_bulletin_client::PolkadotBulletin;
+}
+
+impl RelayToRelayHeadersCliBridge for PolkadotToPolkadotBulletinCliBridge {
+	type Finality = PolkadotFinalityToPolkadotBulletin;
+}
+
+impl RelayToRelayEquivocationDetectionCliBridge for PolkadotToPolkadotBulletinCliBridge {
+	type Equivocation = PolkadotFinalityToPolkadotBulletin;
+}
diff --git a/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_parachains_to_polkadot_bulletin.rs b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_parachains_to_polkadot_bulletin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..674c84adb39c64765641c7f8c757da08248b30f5
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/bridges/polkadot_bulletin/polkadot_parachains_to_polkadot_bulletin.rs
@@ -0,0 +1,92 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Polkadot-to-PolkadotBulletin parachains sync entrypoint.
+
+use crate::cli::bridge::{CliBridgeBase, MessagesCliBridge, ParachainToRelayHeadersCliBridge};
+
+use bp_polkadot_core::parachains::{ParaHash, ParaHeadsProof, ParaId};
+use bp_runtime::Chain;
+use relay_substrate_client::{CallOf, HeaderIdOf};
+use substrate_relay_helper::{
+	messages_lane::MessagesRelayLimits,
+	parachains::{SubmitParachainHeadsCallBuilder, SubstrateParachainsPipeline},
+};
+
+/// Polkadot-to-PolkadotBulletin parachain sync description.
+#[derive(Clone, Debug)]
+pub struct PolkadotToPolkadotBulletin;
+
+impl SubstrateParachainsPipeline for PolkadotToPolkadotBulletin {
+	type SourceParachain = relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+	type SourceRelayChain = relay_polkadot_client::Polkadot;
+	type TargetChain = relay_polkadot_bulletin_client::PolkadotBulletin;
+
+	type SubmitParachainHeadsCallBuilder = PolkadotToPolkadotBulletinCallBuilder;
+}
+
+pub struct PolkadotToPolkadotBulletinCallBuilder;
+impl SubmitParachainHeadsCallBuilder<PolkadotToPolkadotBulletin>
+	for PolkadotToPolkadotBulletinCallBuilder
+{
+	fn build_submit_parachain_heads_call(
+		at_relay_block: HeaderIdOf<relay_polkadot_client::Polkadot>,
+		parachains: Vec<(ParaId, ParaHash)>,
+		parachain_heads_proof: ParaHeadsProof,
+	) -> CallOf<relay_polkadot_bulletin_client::PolkadotBulletin> {
+		relay_polkadot_bulletin_client::RuntimeCall::BridgePolkadotParachains(
+			relay_polkadot_bulletin_client::BridgePolkadotParachainsCall::submit_parachain_heads {
+				at_relay_block: (at_relay_block.0, at_relay_block.1),
+				parachains,
+				parachain_heads_proof,
+			},
+		)
+	}
+}
+
+/// Polkadot-to-PolkadotBulletin parachain sync description for the CLI.
+pub struct PolkadotToPolkadotBulletinCliBridge {}
+
+impl ParachainToRelayHeadersCliBridge for PolkadotToPolkadotBulletinCliBridge {
+	type SourceRelay = relay_polkadot_client::Polkadot;
+	type ParachainFinality = PolkadotToPolkadotBulletin;
+	type RelayFinality =
+		crate::bridges::polkadot_bulletin::polkadot_headers_to_polkadot_bulletin::PolkadotFinalityToPolkadotBulletin;
+}
+
+impl CliBridgeBase for PolkadotToPolkadotBulletinCliBridge {
+	type Source = relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+	type Target = relay_polkadot_bulletin_client::PolkadotBulletin;
+}
+
+impl MessagesCliBridge for PolkadotToPolkadotBulletinCliBridge {
+	type MessagesLane =
+		crate::bridges::polkadot_bulletin::bridge_hub_polkadot_messages_to_polkadot_bulletin::BridgeHubPolkadotMessagesToPolkadotBulletinMessageLane;
+
+	fn maybe_messages_limits() -> Option<MessagesRelayLimits> {
+		// Polkadot Bulletin chain is missing the `TransactionPayment` runtime API (as well as the
+		// transaction payment pallet itself), so we can't estimate limits using runtime calls.
+		// Let's do it here.
+		//
+		// Folloiung constants are just safe **underestimations**. Normally, we are able to deliver
+		// and dispatch thousands of messages in the same transaction.
+		Some(MessagesRelayLimits {
+			max_messages_in_single_batch: 128,
+			max_messages_weight_in_single_batch:
+				bp_polkadot_bulletin::PolkadotBulletin::max_extrinsic_weight() / 20,
+		})
+	}
+}
diff --git a/bridges/relays/bin-substrate/src/chains/mod.rs b/bridges/relays/bin-substrate/src/chains/mod.rs
index b1a91ed1e85cbd1e9250dd6dbaac2f6b29ac0aa5..c9a55e9e900f008cad129ac731182d63834edc37 100644
--- a/bridges/relays/bin-substrate/src/chains/mod.rs
+++ b/bridges/relays/bin-substrate/src/chains/mod.rs
@@ -19,6 +19,7 @@
 mod kusama;
 mod millau;
 mod polkadot;
+mod polkadot_bulletin;
 mod rialto;
 mod rialto_parachain;
 mod rococo;
diff --git a/bridges/relays/bin-substrate/src/chains/polkadot_bulletin.rs b/bridges/relays/bin-substrate/src/chains/polkadot_bulletin.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ee7edbd9f423129d74efc4eac6058537f9f140d4
--- /dev/null
+++ b/bridges/relays/bin-substrate/src/chains/polkadot_bulletin.rs
@@ -0,0 +1,26 @@
+// Copyright 2022 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Polkadot + Polkadot parachains specification for CLI.
+
+use crate::cli::CliChain;
+use relay_polkadot_bulletin_client::PolkadotBulletin;
+use relay_substrate_client::SimpleRuntimeVersion;
+
+impl CliChain for PolkadotBulletin {
+	const RUNTIME_VERSION: Option<SimpleRuntimeVersion> =
+		Some(SimpleRuntimeVersion { spec_version: 100, transaction_version: 1 });
+}
diff --git a/bridges/relays/bin-substrate/src/cli/bridge.rs b/bridges/relays/bin-substrate/src/cli/bridge.rs
index 6cbbdd0ebe50c8d8cdee239e54cb6668e18c4911..59015f07f247bcbeef83a8d785a5bb17a4ee4812 100644
--- a/bridges/relays/bin-substrate/src/cli/bridge.rs
+++ b/bridges/relays/bin-substrate/src/cli/bridge.rs
@@ -19,8 +19,10 @@ use pallet_bridge_parachains::{RelayBlockHash, RelayBlockHasher, RelayBlockNumbe
 use relay_substrate_client::{Chain, ChainWithTransactions, Parachain, RelayChain};
 use strum::{EnumString, EnumVariantNames};
 use substrate_relay_helper::{
-	equivocation::SubstrateEquivocationDetectionPipeline, finality::SubstrateFinalitySyncPipeline,
-	messages_lane::SubstrateMessageLane, parachains::SubstrateParachainsPipeline,
+	equivocation::SubstrateEquivocationDetectionPipeline,
+	finality::SubstrateFinalitySyncPipeline,
+	messages_lane::{MessagesRelayLimits, SubstrateMessageLane},
+	parachains::SubstrateParachainsPipeline,
 };
 
 #[derive(Debug, PartialEq, Eq, EnumString, EnumVariantNames)]
@@ -35,6 +37,8 @@ pub enum FullBridge {
 	BridgeHubWococoToBridgeHubRococo,
 	BridgeHubKusamaToBridgeHubPolkadot,
 	BridgeHubPolkadotToBridgeHubKusama,
+	PolkadotBulletinToBridgeHubPolkadot,
+	BridgeHubPolkadotToPolkadotBulletin,
 }
 
 /// Minimal bridge representation that can be used from the CLI.
@@ -109,4 +113,11 @@ where
 pub trait MessagesCliBridge: CliBridgeBase {
 	/// The Source -> Destination messages synchronization pipeline.
 	type MessagesLane: SubstrateMessageLane<SourceChain = Self::Source, TargetChain = Self::Target>;
+
+	/// Optional messages delivery transaction limits that the messages relay is going
+	/// to use. If it returns `None`, limits are estimated using `TransactionPayment` API
+	/// at the target chain.
+	fn maybe_messages_limits() -> Option<MessagesRelayLimits> {
+		None
+	}
 }
diff --git a/bridges/relays/bin-substrate/src/cli/init_bridge.rs b/bridges/relays/bin-substrate/src/cli/init_bridge.rs
index 110d5e94f8b39bb5d223f745c310249c5e0ff769..af938102e40d1c07b6a1739934df8f7229aaa646 100644
--- a/bridges/relays/bin-substrate/src/cli/init_bridge.rs
+++ b/bridges/relays/bin-substrate/src/cli/init_bridge.rs
@@ -23,6 +23,10 @@ use crate::{
 			kusama_headers_to_bridge_hub_polkadot::KusamaToBridgeHubPolkadotCliBridge,
 			polkadot_headers_to_bridge_hub_kusama::PolkadotToBridgeHubKusamaCliBridge,
 		},
+		polkadot_bulletin::{
+			polkadot_bulletin_headers_to_bridge_hub_polkadot::PolkadotBulletinToBridgeHubPolkadotCliBridge,
+			polkadot_headers_to_polkadot_bulletin::PolkadotToPolkadotBulletinCliBridge,
+		},
 		rialto_millau::{
 			millau_headers_to_rialto::MillauToRialtoCliBridge,
 			rialto_headers_to_millau::RialtoToMillauCliBridge,
@@ -72,6 +76,8 @@ pub enum InitBridgeName {
 	WococoToBridgeHubRococo,
 	KusamaToBridgeHubPolkadot,
 	PolkadotToBridgeHubKusama,
+	PolkadotToPolkadotBulletin,
+	PolkadotBulletinToBridgeHubPolkadot,
 }
 
 #[async_trait]
@@ -232,6 +238,37 @@ impl BridgeInitializer for PolkadotToBridgeHubKusamaCliBridge {
 	}
 }
 
+impl BridgeInitializer for PolkadotToPolkadotBulletinCliBridge {
+	type Engine = GrandpaFinalityEngine<Self::Source>;
+
+	fn encode_init_bridge(
+		init_data: <Self::Engine as Engine<Self::Source>>::InitializationData,
+	) -> <Self::Target as Chain>::Call {
+		type RuntimeCall = relay_polkadot_bulletin_client::RuntimeCall;
+		type BridgePolkadotGrandpaCall = relay_polkadot_bulletin_client::BridgePolkadotGrandpaCall;
+		type SudoCall = relay_polkadot_bulletin_client::SudoCall;
+
+		let initialize_call =
+			RuntimeCall::BridgePolkadotGrandpa(BridgePolkadotGrandpaCall::initialize { init_data });
+
+		RuntimeCall::Sudo(SudoCall::sudo { call: Box::new(initialize_call) })
+	}
+}
+
+impl BridgeInitializer for PolkadotBulletinToBridgeHubPolkadotCliBridge {
+	type Engine = GrandpaFinalityEngine<Self::Source>;
+
+	fn encode_init_bridge(
+		init_data: <Self::Engine as Engine<Self::Source>>::InitializationData,
+	) -> <Self::Target as Chain>::Call {
+		relay_bridge_hub_polkadot_client::runtime::Call::BridgePolkadotBulletinGrandpa(
+			relay_bridge_hub_polkadot_client::runtime::BridgePolkadotBulletinGrandpaCall::initialize {
+				init_data,
+			},
+		)
+	}
+}
+
 impl InitBridge {
 	/// Run the command.
 	pub async fn run(self) -> anyhow::Result<()> {
@@ -249,6 +286,10 @@ impl InitBridge {
 				KusamaToBridgeHubPolkadotCliBridge::init_bridge(self),
 			InitBridgeName::PolkadotToBridgeHubKusama =>
 				PolkadotToBridgeHubKusamaCliBridge::init_bridge(self),
+			InitBridgeName::PolkadotToPolkadotBulletin =>
+				PolkadotToPolkadotBulletinCliBridge::init_bridge(self),
+			InitBridgeName::PolkadotBulletinToBridgeHubPolkadot =>
+				PolkadotBulletinToBridgeHubPolkadotCliBridge::init_bridge(self),
 		}
 		.await
 	}
diff --git a/bridges/relays/bin-substrate/src/cli/relay_headers.rs b/bridges/relays/bin-substrate/src/cli/relay_headers.rs
index fd84782c07ce0b5e72d2b4ca17833c2e625a3878..800e8587d8b25d707839cd8024551306f6583f09 100644
--- a/bridges/relays/bin-substrate/src/cli/relay_headers.rs
+++ b/bridges/relays/bin-substrate/src/cli/relay_headers.rs
@@ -23,6 +23,10 @@ use crate::bridges::{
 		kusama_headers_to_bridge_hub_polkadot::KusamaToBridgeHubPolkadotCliBridge,
 		polkadot_headers_to_bridge_hub_kusama::PolkadotToBridgeHubKusamaCliBridge,
 	},
+	polkadot_bulletin::{
+		polkadot_bulletin_headers_to_bridge_hub_polkadot::PolkadotBulletinToBridgeHubPolkadotCliBridge,
+		polkadot_headers_to_polkadot_bulletin::PolkadotToPolkadotBulletinCliBridge,
+	},
 	rialto_millau::{
 		millau_headers_to_rialto::MillauToRialtoCliBridge,
 		rialto_headers_to_millau::RialtoToMillauCliBridge,
@@ -71,6 +75,8 @@ pub enum RelayHeadersBridge {
 	WococoToBridgeHubRococo,
 	KusamaToBridgeHubPolkadot,
 	PolkadotToBridgeHubKusama,
+	PolkadotToPolkadotBulletin,
+	PolkadotBulletinToBridgeHubPolkadot,
 }
 
 #[async_trait]
@@ -116,6 +122,8 @@ impl HeadersRelayer for RococoToBridgeHubWococoCliBridge {}
 impl HeadersRelayer for WococoToBridgeHubRococoCliBridge {}
 impl HeadersRelayer for KusamaToBridgeHubPolkadotCliBridge {}
 impl HeadersRelayer for PolkadotToBridgeHubKusamaCliBridge {}
+impl HeadersRelayer for PolkadotToPolkadotBulletinCliBridge {}
+impl HeadersRelayer for PolkadotBulletinToBridgeHubPolkadotCliBridge {}
 
 impl RelayHeaders {
 	/// Run the command.
@@ -134,6 +142,10 @@ impl RelayHeaders {
 				KusamaToBridgeHubPolkadotCliBridge::relay_headers(self),
 			RelayHeadersBridge::PolkadotToBridgeHubKusama =>
 				PolkadotToBridgeHubKusamaCliBridge::relay_headers(self),
+			RelayHeadersBridge::PolkadotToPolkadotBulletin =>
+				PolkadotToPolkadotBulletinCliBridge::relay_headers(self),
+			RelayHeadersBridge::PolkadotBulletinToBridgeHubPolkadot =>
+				PolkadotBulletinToBridgeHubPolkadotCliBridge::relay_headers(self),
 		}
 		.await
 	}
diff --git a/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages/mod.rs b/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages/mod.rs
index cd2d39cdec28b5a29e4df97c2ce54d3ea07ba059..86f6ef9e3472c819eb89df0ed4abfbf705339dcb 100644
--- a/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages/mod.rs
+++ b/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages/mod.rs
@@ -44,6 +44,10 @@ use crate::{
 			kusama_parachains_to_bridge_hub_polkadot::BridgeHubKusamaToBridgeHubPolkadotCliBridge,
 			polkadot_parachains_to_bridge_hub_kusama::BridgeHubPolkadotToBridgeHubKusamaCliBridge,
 		},
+		polkadot_bulletin::{
+			polkadot_bulletin_headers_to_bridge_hub_polkadot::PolkadotBulletinToBridgeHubPolkadotCliBridge,
+			polkadot_parachains_to_polkadot_bulletin::PolkadotToPolkadotBulletinCliBridge,
+		},
 		rialto_millau::{
 			millau_headers_to_rialto::MillauToRialtoCliBridge,
 			rialto_headers_to_millau::RialtoToMillauCliBridge,
@@ -77,7 +81,9 @@ use relay_substrate_client::{
 use relay_utils::metrics::MetricsParams;
 use sp_core::Pair;
 use substrate_relay_helper::{
-	messages_lane::MessagesRelayParams, on_demand::OnDemandRelay, TaggedAccount, TransactionParams,
+	messages_lane::{MessagesRelayLimits, MessagesRelayParams},
+	on_demand::OnDemandRelay,
+	TaggedAccount, TransactionParams,
 };
 
 /// Parameters that have the same names across all bridges.
@@ -176,6 +182,7 @@ where
 		source_to_target_headers_relay: Arc<dyn OnDemandRelay<Source, Target>>,
 		target_to_source_headers_relay: Arc<dyn OnDemandRelay<Target, Source>>,
 		lane_id: LaneId,
+		maybe_limits: Option<MessagesRelayLimits>,
 	) -> MessagesRelayParams<Bridge::MessagesLane> {
 		MessagesRelayParams {
 			source_client: self.source.client.clone(),
@@ -185,6 +192,7 @@ where
 			source_to_target_headers_relay: Some(source_to_target_headers_relay),
 			target_to_source_headers_relay: Some(target_to_source_headers_relay),
 			lane_id,
+			limits: maybe_limits,
 			metrics_params: self.metrics_params.clone().disable(),
 		}
 	}
@@ -202,6 +210,7 @@ declare_chain_cli_schema!(Kusama, kusama);
 declare_chain_cli_schema!(BridgeHubKusama, bridge_hub_kusama);
 declare_chain_cli_schema!(Polkadot, polkadot);
 declare_chain_cli_schema!(BridgeHubPolkadot, bridge_hub_polkadot);
+declare_chain_cli_schema!(PolkadotBulletin, polkadot_bulletin);
 // Means to override signers of different layer transactions.
 declare_chain_cli_schema!(MillauHeadersToRialto, millau_headers_to_rialto);
 declare_chain_cli_schema!(MillauHeadersToRialtoParachain, millau_headers_to_rialto_parachain);
@@ -227,11 +236,21 @@ declare_chain_cli_schema!(
 	PolkadotParachainsToBridgeHubKusama,
 	polkadot_parachains_to_bridge_hub_kusama
 );
+declare_chain_cli_schema!(
+	PolkadotBulletinHeadersToBridgeHubPolkadot,
+	polkadot_bulletin_headers_to_bridge_hub_polkadot
+);
+declare_chain_cli_schema!(PolkadotHeadersToPolkadotBulletin, polkadot_headers_to_polkadot_bulletin);
+declare_chain_cli_schema!(
+	PolkadotParachainsToPolkadotBulletin,
+	polkadot_parachains_to_polkadot_bulletin
+);
 // All supported bridges.
 declare_relay_to_relay_bridge_schema!(Millau, Rialto);
 declare_relay_to_parachain_bridge_schema!(Millau, RialtoParachain, Rialto);
 declare_parachain_to_parachain_bridge_schema!(BridgeHubRococo, Rococo, BridgeHubWococo, Wococo);
 declare_parachain_to_parachain_bridge_schema!(BridgeHubKusama, Kusama, BridgeHubPolkadot, Polkadot);
+declare_relay_to_parachain_bridge_schema!(PolkadotBulletin, BridgeHubPolkadot, Polkadot);
 
 /// Base portion of the bidirectional complex relay.
 ///
@@ -370,6 +389,7 @@ where
 				left_to_right_on_demand_headers.clone(),
 				right_to_left_on_demand_headers.clone(),
 				lane,
+				Self::L2R::maybe_messages_limits(),
 			))
 			.map_err(|e| anyhow::format_err!("{}", e))
 			.boxed();
@@ -381,6 +401,7 @@ where
 				right_to_left_on_demand_headers.clone(),
 				left_to_right_on_demand_headers.clone(),
 				lane,
+				Self::R2L::maybe_messages_limits(),
 			))
 			.map_err(|e| anyhow::format_err!("{}", e))
 			.boxed();
@@ -500,6 +521,32 @@ impl Full2WayBridge for BridgeHubKusamaBridgeHubPolkadotFull2WayBridge {
 	}
 }
 
+/// `PolkadotBulletin` <> `BridgeHubPolkadot` complex relay.
+pub struct PolkadotBulletinBridgeHubPolkadotFull2WayBridge {
+	base: <Self as Full2WayBridge>::Base,
+}
+
+#[async_trait]
+impl Full2WayBridge for PolkadotBulletinBridgeHubPolkadotFull2WayBridge {
+	type Base = RelayToParachainBridge<Self::L2R, Self::R2L>;
+	type Left = relay_polkadot_bulletin_client::PolkadotBulletin;
+	type Right = relay_bridge_hub_polkadot_client::BridgeHubPolkadot;
+	type L2R = PolkadotBulletinToBridgeHubPolkadotCliBridge;
+	type R2L = PolkadotToPolkadotBulletinCliBridge;
+
+	fn new(base: Self::Base) -> anyhow::Result<Self> {
+		Ok(Self { base })
+	}
+
+	fn base(&self) -> &Self::Base {
+		&self.base
+	}
+
+	fn mut_base(&mut self) -> &mut Self::Base {
+		&mut self.base
+	}
+}
+
 /// Complex headers+messages relay.
 #[derive(Debug, PartialEq, StructOpt)]
 pub enum RelayHeadersAndMessages {
@@ -511,6 +558,8 @@ pub enum RelayHeadersAndMessages {
 	BridgeHubRococoBridgeHubWococo(BridgeHubRococoBridgeHubWococoHeadersAndMessages),
 	/// BridgeHubKusama <> BridgeHubPolkadot relay.
 	BridgeHubKusamaBridgeHubPolkadot(BridgeHubKusamaBridgeHubPolkadotHeadersAndMessages),
+	/// `PolkadotBulletin` <> `BridgeHubPolkadot` relay.
+	PolkadotBulletinBridgeHubPolkadot(PolkadotBulletinBridgeHubPolkadotHeadersAndMessages),
 }
 
 impl RelayHeadersAndMessages {
@@ -531,6 +580,10 @@ impl RelayHeadersAndMessages {
 				BridgeHubKusamaBridgeHubPolkadotFull2WayBridge::new(params.into_bridge().await?)?
 					.run()
 					.await,
+			RelayHeadersAndMessages::PolkadotBulletinBridgeHubPolkadot(params) =>
+				PolkadotBulletinBridgeHubPolkadotFull2WayBridge::new(params.into_bridge().await?)?
+					.run()
+					.await,
 		}
 	}
 }
diff --git a/bridges/relays/bin-substrate/src/cli/relay_messages.rs b/bridges/relays/bin-substrate/src/cli/relay_messages.rs
index 1624524e4a5b1969e24d2cefba4539eb34fad44f..b5ba275af6fa0be8ca219a4b036a930e95efdb1e 100644
--- a/bridges/relays/bin-substrate/src/cli/relay_messages.rs
+++ b/bridges/relays/bin-substrate/src/cli/relay_messages.rs
@@ -24,6 +24,10 @@ use crate::bridges::{
 		bridge_hub_kusama_messages_to_bridge_hub_polkadot::BridgeHubKusamaToBridgeHubPolkadotMessagesCliBridge,
 		bridge_hub_polkadot_messages_to_bridge_hub_kusama::BridgeHubPolkadotToBridgeHubKusamaMessagesCliBridge,
 	},
+	polkadot_bulletin::{
+		bridge_hub_polkadot_messages_to_polkadot_bulletin::BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge,
+		polkadot_bulletin_messages_to_bridge_hub_polkadot::PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge,
+	},
 	rialto_millau::{
 		millau_headers_to_rialto::MillauToRialtoCliBridge,
 		rialto_headers_to_millau::RialtoToMillauCliBridge,
@@ -93,6 +97,7 @@ where
 			source_to_target_headers_relay: None,
 			target_to_source_headers_relay: None,
 			lane_id: data.lane.into(),
+			limits: Self::maybe_messages_limits(),
 			metrics_params: data.prometheus_params.into_metrics_params()?,
 		})
 		.await
@@ -108,6 +113,8 @@ impl MessagesRelayer for BridgeHubRococoToBridgeHubWococoMessagesCliBridge {}
 impl MessagesRelayer for BridgeHubWococoToBridgeHubRococoMessagesCliBridge {}
 impl MessagesRelayer for BridgeHubKusamaToBridgeHubPolkadotMessagesCliBridge {}
 impl MessagesRelayer for BridgeHubPolkadotToBridgeHubKusamaMessagesCliBridge {}
+impl MessagesRelayer for PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge {}
+impl MessagesRelayer for BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge {}
 
 impl RelayMessages {
 	/// Run the command.
@@ -127,6 +134,10 @@ impl RelayMessages {
 				BridgeHubKusamaToBridgeHubPolkadotMessagesCliBridge::relay_messages(self),
 			FullBridge::BridgeHubPolkadotToBridgeHubKusama =>
 				BridgeHubPolkadotToBridgeHubKusamaMessagesCliBridge::relay_messages(self),
+			FullBridge::PolkadotBulletinToBridgeHubPolkadot =>
+				PolkadotBulletinToBridgeHubPolkadotMessagesCliBridge::relay_messages(self),
+			FullBridge::BridgeHubPolkadotToPolkadotBulletin =>
+				BridgeHubPolkadotToPolkadotBulletinMessagesCliBridge::relay_messages(self),
 		}
 		.await
 	}
diff --git a/bridges/relays/bin-substrate/src/cli/relay_parachains.rs b/bridges/relays/bin-substrate/src/cli/relay_parachains.rs
index 67daf01c8f4b2c2fa2dee7b76faeae0be30078c0..3dc9daad1aab440ac6e770ba73ff69b0334f000f 100644
--- a/bridges/relays/bin-substrate/src/cli/relay_parachains.rs
+++ b/bridges/relays/bin-substrate/src/cli/relay_parachains.rs
@@ -19,6 +19,7 @@ use crate::bridges::{
 		kusama_parachains_to_bridge_hub_polkadot::BridgeHubKusamaToBridgeHubPolkadotCliBridge,
 		polkadot_parachains_to_bridge_hub_kusama::BridgeHubPolkadotToBridgeHubKusamaCliBridge,
 	},
+	polkadot_bulletin::polkadot_parachains_to_polkadot_bulletin::PolkadotToPolkadotBulletinCliBridge,
 	rialto_parachain_millau::rialto_parachains_to_millau::RialtoParachainToMillauCliBridge,
 	rococo_wococo::{
 		rococo_parachains_to_bridge_hub_wococo::BridgeHubRococoToBridgeHubWococoCliBridge,
@@ -71,6 +72,7 @@ pub enum RelayParachainsBridge {
 	WococoToBridgeHubRococo,
 	KusamaToBridgeHubPolkadot,
 	PolkadotToBridgeHubKusama,
+	PolkadotToPolkadotBulletin,
 }
 
 #[async_trait]
@@ -120,6 +122,7 @@ impl ParachainsRelayer for BridgeHubRococoToBridgeHubWococoCliBridge {}
 impl ParachainsRelayer for BridgeHubWococoToBridgeHubRococoCliBridge {}
 impl ParachainsRelayer for BridgeHubKusamaToBridgeHubPolkadotCliBridge {}
 impl ParachainsRelayer for BridgeHubPolkadotToBridgeHubKusamaCliBridge {}
+impl ParachainsRelayer for PolkadotToPolkadotBulletinCliBridge {}
 
 impl RelayParachains {
 	/// Run the command.
@@ -137,6 +140,8 @@ impl RelayParachains {
 				BridgeHubKusamaToBridgeHubPolkadotCliBridge::relay_parachains(self),
 			RelayParachainsBridge::PolkadotToBridgeHubKusama =>
 				BridgeHubPolkadotToBridgeHubKusamaCliBridge::relay_parachains(self),
+			RelayParachainsBridge::PolkadotToPolkadotBulletin =>
+				PolkadotToPolkadotBulletinCliBridge::relay_parachains(self),
 		}
 		.await
 	}
diff --git a/bridges/relays/bin-substrate/src/cli/send_message.rs b/bridges/relays/bin-substrate/src/cli/send_message.rs
index 918643faadb6938a6aa8a8c6bfe8a7b2ab9d1b25..02ef58b3719488d5b4de2d94e3d813ff5529a114 100644
--- a/bridges/relays/bin-substrate/src/cli/send_message.rs
+++ b/bridges/relays/bin-substrate/src/cli/send_message.rs
@@ -109,18 +109,8 @@ impl SendMessage {
 				MillauToRialtoParachainCliBridge::send_message(self),
 			FullBridge::RialtoParachainToMillau =>
 				RialtoParachainToMillauCliBridge::send_message(self),
-			FullBridge::BridgeHubRococoToBridgeHubWococo => unimplemented!(
-				"Sending message from BridgeHubRococo to BridgeHubWococo is not supported"
-			),
-			FullBridge::BridgeHubWococoToBridgeHubRococo => unimplemented!(
-				"Sending message from BridgeHubWococo to BridgeHubRococo is not supported"
-			),
-			FullBridge::BridgeHubKusamaToBridgeHubPolkadot => unimplemented!(
-				"Sending message from BridgeHubKusama to BridgeHubPolkadot is not supported"
-			),
-			FullBridge::BridgeHubPolkadotToBridgeHubKusama => unimplemented!(
-				"Sending message from BridgeHubPolkadot to BridgeHubKusama is not supported"
-			),
+			// all our (soon to retire_ testnets are above, so if is fine to use `_`
+			_ => unimplemented!("Sending message from in {:?} is not supported", self.bridge,),
 		}
 		.await
 	}
diff --git a/bridges/relays/client-bridge-hub-polkadot/Cargo.toml b/bridges/relays/client-bridge-hub-polkadot/Cargo.toml
index bf6d65b304d80b0c66284f7a1758d499566aef6c..f071778446d84ad479b1b1f256da7c57b3172acd 100644
--- a/bridges/relays/client-bridge-hub-polkadot/Cargo.toml
+++ b/bridges/relays/client-bridge-hub-polkadot/Cargo.toml
@@ -17,7 +17,8 @@ bp-bridge-hub-polkadot = { path = "../../primitives/chain-bridge-hub-polkadot" }
 bp-header-chain = { path = "../../primitives/header-chain" }
 bp-messages = { path = "../../primitives/messages" }
 bp-parachains = { path = "../../primitives/parachains" }
-bp-polkadot-core= { path = "../../primitives/polkadot-core" }
+bp-polkadot-bulletin = { path = "../../primitives/chain-polkadot-bulletin" }
+bp-polkadot-core = { path = "../../primitives/polkadot-core" }
 bp-kusama = { path = "../../primitives/chain-kusama" }
 bp-runtime = { path = "../../primitives/runtime" }
 
diff --git a/bridges/relays/client-bridge-hub-polkadot/src/runtime_wrapper.rs b/bridges/relays/client-bridge-hub-polkadot/src/runtime_wrapper.rs
index 924d3bbef07d67977dfea79a94a9a9a8bfea2eec..ded177996df9488e71d5b9b7c5f159cdeefea58a 100644
--- a/bridges/relays/client-bridge-hub-polkadot/src/runtime_wrapper.rs
+++ b/bridges/relays/client-bridge-hub-polkadot/src/runtime_wrapper.rs
@@ -29,11 +29,18 @@ pub use relay_substrate_client::calls::{SystemCall, UtilityCall};
 /// Unchecked BridgeHubPolkadot extrinsic.
 pub type UncheckedExtrinsic = bp_bridge_hub_polkadot::UncheckedExtrinsic<Call, SignedExtension>;
 
-// The indirect pallet call used to sync `Kusama` GRANDPA finality to `BHPolkadot`.
+/// The indirect pallet call used to sync `Kusama` GRANDPA finality to `BHPolkadot`.
 pub type BridgeKusamaGrandpaCall = BridgeGrandpaCallOf<bp_kusama::Kusama>;
-// The indirect pallet call used to sync `BridgeHubKusama` messages to `BridgeHubPolkadot`.
+/// The indirect pallet call used to sync `BridgeHubKusama` messages to `BridgeHubPolkadot`.
 pub type BridgeKusamaMessagesCall = BridgeMessagesCallOf<bp_bridge_hub_kusama::BridgeHubKusama>;
 
+/// The indirect pallet call used to sync `PolkadotBulletin` GRANDPA finality to `BHPolkadot`.
+pub type BridgePolkadotBulletinGrandpaCall =
+	BridgeGrandpaCallOf<bp_polkadot_bulletin::PolkadotBulletin>;
+/// The indirect pallet call used to sync `PolkadotBulletin` messages to `BridgeHubPolkadot`.
+pub type BridgePolkadotBulletinMessagesCall =
+	BridgeMessagesCallOf<bp_polkadot_bulletin::PolkadotBulletin>;
+
 /// `BridgeHubPolkadot` Runtime `Call` enum.
 ///
 /// The enum represents a subset of possible `Call`s we can send to `BridgeHubPolkadot` chain.
@@ -52,15 +59,22 @@ pub enum Call {
 	#[codec(index = 40)]
 	Utility(UtilityCall<Call>),
 
-	/// Kusama bridge pallet.
+	/// Kusama grandpa bridge pallet.
 	#[codec(index = 51)]
 	BridgeKusamaGrandpa(BridgeKusamaGrandpaCall),
-	/// Kusama parachain bridge pallet.
+	/// Kusama parachains bridge pallet.
 	#[codec(index = 52)]
 	BridgeKusamaParachain(BridgeParachainCall),
 	/// Kusama messages bridge pallet.
 	#[codec(index = 53)]
 	BridgeKusamaMessages(BridgeKusamaMessagesCall),
+
+	/// Polkadot Bulletin grandpa bridge pallet.
+	#[codec(index = 55)]
+	BridgePolkadotBulletinGrandpa(BridgePolkadotBulletinGrandpaCall),
+	/// Polkadot Bulletin messages bridge pallet.
+	#[codec(index = 56)]
+	BridgePolkadotBulletinMessages(BridgePolkadotBulletinMessagesCall),
 }
 
 impl From<UtilityCall<Call>> for Call {
diff --git a/bridges/relays/client-polkadot-bulletin/Cargo.toml b/bridges/relays/client-polkadot-bulletin/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..bcff9d2f21220263c62b899c7e8a1859dcd46193
--- /dev/null
+++ b/bridges/relays/client-polkadot-bulletin/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "relay-polkadot-bulletin-client"
+version = "0.1.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2021"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "3.1.5", features = ["derive"] }
+scale-info = { version = "2.9.0", default-features = false, features = ["derive"] }
+subxt = { version = "0.31.0", default-features = false, features = ["native"] }
+
+# Bridge dependencies
+
+bp-header-chain = { path = "../../primitives/header-chain" }
+bp-messages = { path = "../../primitives/messages" }
+bp-polkadot-core = { path = "../../primitives/polkadot-core" }
+bp-polkadot-bulletin = { path = "../../primitives/chain-polkadot-bulletin" }
+bp-runtime = { path = "../../primitives/runtime" }
+bridge-runtime-common = { path = "../../bin/runtime-common" }
+relay-substrate-client = { path = "../client-substrate" }
+relay-utils = { path = "../utils" }
+
+# Substrate Dependencies
+
+sp-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+sp-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
+sp-weights = { git = "https://github.com/paritytech/polkadot-sdk", branch = "master" }
diff --git a/bridges/relays/client-polkadot-bulletin/src/codegen_runtime.rs b/bridges/relays/client-polkadot-bulletin/src/codegen_runtime.rs
new file mode 100644
index 0000000000000000000000000000000000000000..37af5b0b98e0b4af00d448b91fcce20dff7bcd0c
--- /dev/null
+++ b/bridges/relays/client-polkadot-bulletin/src/codegen_runtime.rs
@@ -0,0 +1,1480 @@
+// Copyright 2019-2023 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Autogenerated runtime API
+//! THIS FILE WAS AUTOGENERATED USING parity-bridges-common::runtime-codegen
+//! EXECUTED COMMAND: target/debug/runtime-codegen --from-node-url ws://127.0.0.1:9944
+
+#[allow(dead_code, unused_imports, non_camel_case_types)]
+#[allow(clippy::all)]
+pub mod api {
+	use super::api as root_mod;
+	pub mod runtime_types {
+		use super::runtime_types;
+		pub mod bounded_collections {
+			use super::runtime_types;
+			pub mod bounded_vec {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct BoundedVec<_0>(pub ::std::vec::Vec<_0>);
+			}
+			pub mod weak_bounded_vec {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct WeakBoundedVec<_0>(pub ::std::vec::Vec<_0>);
+			}
+		}
+		pub mod bp_header_chain {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct AuthoritySet {
+				pub authorities: ::std::vec::Vec<(
+					runtime_types::sp_consensus_grandpa::app::Public,
+					::core::primitive::u64,
+				)>,
+				pub set_id: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum HeaderChainError {
+				#[codec(index = 0)]
+				UnknownHeader,
+				#[codec(index = 1)]
+				StorageProof(runtime_types::bp_runtime::storage_proof::Error),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct HeaderFinalityInfo<_0, _1> {
+				pub finality_proof: _0,
+				pub new_verification_context: ::core::option::Option<_1>,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct StoredHeaderData<_0, _1> {
+				pub number: _0,
+				pub state_root: _1,
+			}
+		}
+		pub mod bp_messages {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct DeliveredMessages {
+				pub begin: ::core::primitive::u64,
+				pub end: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct InboundLaneData<_0> {
+				pub relayers: ::std::vec::Vec<runtime_types::bp_messages::UnrewardedRelayer<_0>>,
+				pub last_confirmed_nonce: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct LaneId(pub [::core::primitive::u8; 4usize]);
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct MessageKey {
+				pub lane_id: runtime_types::bp_messages::LaneId,
+				pub nonce: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum MessagesOperatingMode {
+				#[codec(index = 0)]
+				Basic(runtime_types::bp_runtime::BasicOperatingMode),
+				#[codec(index = 1)]
+				RejectingOutboundMessages,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct OutboundLaneData {
+				pub oldest_unpruned_nonce: ::core::primitive::u64,
+				pub latest_received_nonce: ::core::primitive::u64,
+				pub latest_generated_nonce: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum ReceivalResult<_0> {
+				#[codec(index = 0)]
+				Dispatched(runtime_types::bp_runtime::messages::MessageDispatchResult<_0>),
+				#[codec(index = 1)]
+				InvalidNonce,
+				#[codec(index = 2)]
+				TooManyUnrewardedRelayers,
+				#[codec(index = 3)]
+				TooManyUnconfirmedMessages,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct ReceivedMessages<_0> {
+				pub lane: runtime_types::bp_messages::LaneId,
+				pub receive_results: ::std::vec::Vec<(
+					::core::primitive::u64,
+					runtime_types::bp_messages::ReceivalResult<_0>,
+				)>,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct UnrewardedRelayer<_0> {
+				pub relayer: _0,
+				pub messages: runtime_types::bp_messages::DeliveredMessages,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum VerificationError {
+				#[codec(index = 0)]
+				EmptyMessageProof,
+				#[codec(index = 1)]
+				HeaderChain(runtime_types::bp_header_chain::HeaderChainError),
+				#[codec(index = 2)]
+				InboundLaneStorage(runtime_types::bp_runtime::storage_proof::Error),
+				#[codec(index = 3)]
+				InvalidMessageWeight,
+				#[codec(index = 4)]
+				MessagesCountMismatch,
+				#[codec(index = 5)]
+				MessageStorage(runtime_types::bp_runtime::storage_proof::Error),
+				#[codec(index = 6)]
+				MessageTooLarge,
+				#[codec(index = 7)]
+				OutboundLaneStorage(runtime_types::bp_runtime::storage_proof::Error),
+				#[codec(index = 8)]
+				StorageProof(runtime_types::bp_runtime::storage_proof::Error),
+				#[codec(index = 9)]
+				Other,
+			}
+		}
+		pub mod bp_parachains {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct BestParaHeadHash {
+				pub at_relay_block_number: ::core::primitive::u32,
+				pub head_hash: ::subxt::utils::H256,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct ParaInfo {
+				pub best_head_hash: runtime_types::bp_parachains::BestParaHeadHash,
+				pub next_imported_hash_position: ::core::primitive::u32,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct ParaStoredHeaderData(pub ::std::vec::Vec<::core::primitive::u8>);
+		}
+		pub mod bp_runtime {
+			use super::runtime_types;
+			pub mod messages {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct MessageDispatchResult<_0> {
+					pub unspent_weight: ::sp_weights::Weight,
+					pub dispatch_level_result: _0,
+				}
+			}
+			pub mod storage_proof {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					DuplicateNodesInProof,
+					#[codec(index = 1)]
+					UnusedNodesInTheProof,
+					#[codec(index = 2)]
+					StorageRootMismatch,
+					#[codec(index = 3)]
+					StorageValueUnavailable,
+					#[codec(index = 4)]
+					StorageValueEmpty,
+					#[codec(index = 5)]
+					StorageValueDecodeFailed(runtime_types::bp_runtime::StrippableError),
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum BasicOperatingMode {
+				#[codec(index = 0)]
+				Normal,
+				#[codec(index = 1)]
+				Halted,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct HeaderId<_0, _1>(pub _1, pub _0);
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum OwnedBridgeModuleError {
+				#[codec(index = 0)]
+				Halted,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct StrippableError;
+		}
+		pub mod finality_grandpa {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Commit<_0, _1, _2, _3> {
+				pub target_hash: _0,
+				pub target_number: _1,
+				pub precommits: ::std::vec::Vec<
+					runtime_types::finality_grandpa::SignedPrecommit<_0, _1, _2, _3>,
+				>,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Equivocation<_0, _1, _2> {
+				pub round_number: ::core::primitive::u64,
+				pub identity: _0,
+				pub first: (_1, _2),
+				pub second: (_1, _2),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Precommit<_0, _1> {
+				pub target_hash: _0,
+				pub target_number: _1,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Prevote<_0, _1> {
+				pub target_hash: _0,
+				pub target_number: _1,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct SignedPrecommit<_0, _1, _2, _3> {
+				pub precommit: runtime_types::finality_grandpa::Precommit<_0, _1>,
+				pub signature: _2,
+				pub id: _3,
+			}
+		}
+		pub mod frame_support {
+			use super::runtime_types;
+			pub mod dispatch {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum DispatchClass {
+					#[codec(index = 0)]
+					Normal,
+					#[codec(index = 1)]
+					Operational,
+					#[codec(index = 2)]
+					Mandatory,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct DispatchInfo {
+					pub weight: ::sp_weights::Weight,
+					pub class: runtime_types::frame_support::dispatch::DispatchClass,
+					pub pays_fee: runtime_types::frame_support::dispatch::Pays,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Pays {
+					#[codec(index = 0)]
+					Yes,
+					#[codec(index = 1)]
+					No,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct PerDispatchClass<_0> {
+					pub normal: _0,
+					pub operational: _0,
+					pub mandatory: _0,
+				}
+			}
+		}
+		pub mod frame_system {
+			use super::runtime_types;
+			pub mod extensions {
+				use super::runtime_types;
+				pub mod check_genesis {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckGenesis;
+				}
+				pub mod check_mortality {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckMortality(pub ::sp_runtime::generic::Era);
+				}
+				pub mod check_non_zero_sender {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckNonZeroSender;
+				}
+				pub mod check_nonce {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckNonce(#[codec(compact)] pub ::core::primitive::u32);
+				}
+				pub mod check_spec_version {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckSpecVersion;
+				}
+				pub mod check_tx_version {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckTxVersion;
+				}
+				pub mod check_weight {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct CheckWeight;
+				}
+			}
+			pub mod limits {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct BlockLength {
+					pub max: runtime_types::frame_support::dispatch::PerDispatchClass<
+						::core::primitive::u32,
+					>,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct BlockWeights {
+					pub base_block: ::sp_weights::Weight,
+					pub max_block: ::sp_weights::Weight,
+					pub per_class: runtime_types::frame_support::dispatch::PerDispatchClass<
+						runtime_types::frame_system::limits::WeightsPerClass,
+					>,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct WeightsPerClass {
+					pub base_extrinsic: ::sp_weights::Weight,
+					pub max_extrinsic: ::core::option::Option<::sp_weights::Weight>,
+					pub max_total: ::core::option::Option<::sp_weights::Weight>,
+					pub reserved: ::core::option::Option<::sp_weights::Weight>,
+				}
+			}
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					remark { remark: ::std::vec::Vec<::core::primitive::u8> },
+					#[codec(index = 1)]
+					set_heap_pages { pages: ::core::primitive::u64 },
+					#[codec(index = 2)]
+					set_code { code: ::std::vec::Vec<::core::primitive::u8> },
+					#[codec(index = 3)]
+					set_code_without_checks { code: ::std::vec::Vec<::core::primitive::u8> },
+					#[codec(index = 4)]
+					set_storage {
+						items: ::std::vec::Vec<(
+							::std::vec::Vec<::core::primitive::u8>,
+							::std::vec::Vec<::core::primitive::u8>,
+						)>,
+					},
+					#[codec(index = 5)]
+					kill_storage { keys: ::std::vec::Vec<::std::vec::Vec<::core::primitive::u8>> },
+					#[codec(index = 6)]
+					kill_prefix {
+						prefix: ::std::vec::Vec<::core::primitive::u8>,
+						subkeys: ::core::primitive::u32,
+					},
+					#[codec(index = 7)]
+					remark_with_event { remark: ::std::vec::Vec<::core::primitive::u8> },
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					InvalidSpecName,
+					#[codec(index = 1)]
+					SpecVersionNeedsToIncrease,
+					#[codec(index = 2)]
+					FailedToExtractRuntimeVersion,
+					#[codec(index = 3)]
+					NonDefaultComposite,
+					#[codec(index = 4)]
+					NonZeroRefCount,
+					#[codec(index = 5)]
+					CallFiltered,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					ExtrinsicSuccess {
+						dispatch_info: runtime_types::frame_support::dispatch::DispatchInfo,
+					},
+					#[codec(index = 1)]
+					ExtrinsicFailed {
+						dispatch_error: runtime_types::sp_runtime::DispatchError,
+						dispatch_info: runtime_types::frame_support::dispatch::DispatchInfo,
+					},
+					#[codec(index = 2)]
+					CodeUpdated,
+					#[codec(index = 3)]
+					NewAccount { account: ::sp_core::crypto::AccountId32 },
+					#[codec(index = 4)]
+					KilledAccount { account: ::sp_core::crypto::AccountId32 },
+					#[codec(index = 5)]
+					Remarked { sender: ::sp_core::crypto::AccountId32, hash: ::subxt::utils::H256 },
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct AccountInfo<_0, _1> {
+				pub nonce: _0,
+				pub consumers: _0,
+				pub providers: _0,
+				pub sufficients: _0,
+				pub data: _1,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct EventRecord<_0, _1> {
+				pub phase: runtime_types::frame_system::Phase,
+				pub event: _0,
+				pub topics: ::std::vec::Vec<_1>,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct LastRuntimeUpgradeInfo {
+				#[codec(compact)]
+				pub spec_version: ::core::primitive::u32,
+				pub spec_name: ::std::string::String,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum Phase {
+				#[codec(index = 0)]
+				ApplyExtrinsic(::core::primitive::u32),
+				#[codec(index = 1)]
+				Finalization,
+				#[codec(index = 2)]
+				Initialization,
+			}
+		}
+		pub mod pallet_babe {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					report_equivocation {
+						equivocation_proof: ::std::boxed::Box<
+							runtime_types::sp_consensus_slots::EquivocationProof<
+								::sp_runtime::generic::Header<
+									::core::primitive::u32,
+									::sp_runtime::traits::BlakeTwo256,
+								>,
+								runtime_types::sp_consensus_babe::app::Public,
+							>,
+						>,
+						key_owner_proof: ::sp_session::MembershipProof,
+					},
+					#[codec(index = 1)]
+					report_equivocation_unsigned {
+						equivocation_proof: ::std::boxed::Box<
+							runtime_types::sp_consensus_slots::EquivocationProof<
+								::sp_runtime::generic::Header<
+									::core::primitive::u32,
+									::sp_runtime::traits::BlakeTwo256,
+								>,
+								runtime_types::sp_consensus_babe::app::Public,
+							>,
+						>,
+						key_owner_proof: ::sp_session::MembershipProof,
+					},
+					#[codec(index = 2)]
+					plan_config_change {
+						config: runtime_types::sp_consensus_babe::digests::NextConfigDescriptor,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					InvalidEquivocationProof,
+					#[codec(index = 1)]
+					InvalidKeyOwnershipProof,
+					#[codec(index = 2)]
+					DuplicateOffenceReport,
+					#[codec(index = 3)]
+					InvalidConfiguration,
+				}
+			}
+		}
+		pub mod pallet_bridge_grandpa {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					submit_finality_proof {
+						finality_target: ::std::boxed::Box<
+							::sp_runtime::generic::Header<
+								::core::primitive::u32,
+								::sp_runtime::traits::BlakeTwo256,
+							>,
+						>,
+						justification: ::bp_header_chain::justification::GrandpaJustification<
+							::sp_runtime::generic::Header<
+								::core::primitive::u32,
+								::sp_runtime::traits::BlakeTwo256,
+							>,
+						>,
+					},
+					#[codec(index = 1)]
+					initialize {
+						init_data: ::bp_header_chain::InitializationData<
+							::sp_runtime::generic::Header<
+								::core::primitive::u32,
+								::sp_runtime::traits::BlakeTwo256,
+							>,
+						>,
+					},
+					#[codec(index = 2)]
+					set_owner { new_owner: ::core::option::Option<::sp_core::crypto::AccountId32> },
+					#[codec(index = 3)]
+					set_operating_mode {
+						operating_mode: runtime_types::bp_runtime::BasicOperatingMode,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					InvalidJustification,
+					#[codec(index = 1)]
+					InvalidAuthoritySet,
+					#[codec(index = 2)]
+					OldHeader,
+					#[codec(index = 3)]
+					UnsupportedScheduledChange,
+					#[codec(index = 4)]
+					NotInitialized,
+					#[codec(index = 5)]
+					AlreadyInitialized,
+					#[codec(index = 6)]
+					TooManyAuthoritiesInSet,
+					#[codec(index = 7)]
+					BridgeModule(runtime_types::bp_runtime::OwnedBridgeModuleError),
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					UpdatedBestFinalizedHeader {
+						number: ::core::primitive::u32,
+						hash: ::subxt::utils::H256,
+						grandpa_info: runtime_types::bp_header_chain::HeaderFinalityInfo<
+							::bp_header_chain::justification::GrandpaJustification<
+								::sp_runtime::generic::Header<
+									::core::primitive::u32,
+									::sp_runtime::traits::BlakeTwo256,
+								>,
+							>,
+							runtime_types::bp_header_chain::AuthoritySet,
+						>,
+					},
+				}
+			}
+			pub mod storage_types {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct StoredAuthoritySet {
+					pub authorities: runtime_types::bounded_collections::bounded_vec::BoundedVec<(
+						runtime_types::sp_consensus_grandpa::app::Public,
+						::core::primitive::u64,
+					)>,
+					pub set_id: ::core::primitive::u64,
+				}
+			}
+		}
+		pub mod pallet_bridge_messages {
+			use super::runtime_types;
+			pub mod outbound_lane {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum ReceivalConfirmationError {
+					#[codec(index = 0)]
+					FailedToConfirmFutureMessages,
+					#[codec(index = 1)]
+					EmptyUnrewardedRelayerEntry,
+					#[codec(index = 2)]
+					NonConsecutiveUnrewardedRelayerEntries,
+					#[codec(index = 3)]
+					TryingToConfirmMoreMessagesThanExpected,
+				}
+			}
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					set_owner { new_owner: ::core::option::Option<::sp_core::crypto::AccountId32> },
+					#[codec(index = 1)]
+					set_operating_mode {
+						operating_mode: runtime_types::bp_messages::MessagesOperatingMode,
+					},
+					#[codec(index = 2)]
+					receive_messages_proof {
+						relayer_id_at_bridged_chain: ::sp_core::crypto::AccountId32,
+						proof: bridge_runtime_common::messages::target::FromBridgedChainMessagesProof<
+							::subxt::utils::H256,
+						>,
+						messages_count: ::core::primitive::u32,
+						dispatch_weight: ::sp_weights::Weight,
+					},
+					#[codec(index = 3)]
+					receive_messages_delivery_proof {
+						proof: bridge_runtime_common::messages::source::FromBridgedChainMessagesDeliveryProof<
+							::subxt::utils::H256,
+						>,
+						relayers_state: ::bp_messages::UnrewardedRelayersState,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					# [codec (index = 0)] NotOperatingNormally , # [codec (index = 1)] InactiveOutboundLane , # [codec (index = 2)] MessageDispatchInactive , # [codec (index = 3)] MessageRejectedByChainVerifier (runtime_types :: bp_messages :: VerificationError ,) , # [codec (index = 4)] MessageRejectedByLaneVerifier (runtime_types :: bp_messages :: VerificationError ,) , # [codec (index = 5)] MessageRejectedByPallet (runtime_types :: bp_messages :: VerificationError ,) , # [codec (index = 6)] FailedToWithdrawMessageFee , # [codec (index = 7)] TooManyMessagesInTheProof , # [codec (index = 8)] InvalidMessagesProof , # [codec (index = 9)] InvalidMessagesDeliveryProof , # [codec (index = 10)] InvalidUnrewardedRelayersState , # [codec (index = 11)] InsufficientDispatchWeight , # [codec (index = 12)] MessageIsNotYetSent , # [codec (index = 13)] ReceivalConfirmation (runtime_types :: pallet_bridge_messages :: outbound_lane :: ReceivalConfirmationError ,) , # [codec (index = 14)] BridgeModule (runtime_types :: bp_runtime :: OwnedBridgeModuleError ,) , }
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					MessageAccepted {
+						lane_id: runtime_types::bp_messages::LaneId,
+						nonce: ::core::primitive::u64,
+					},
+					#[codec(index = 1)]
+					MessagesReceived(
+						::std::vec::Vec<runtime_types::bp_messages::ReceivedMessages<()>>,
+					),
+					#[codec(index = 2)]
+					MessagesDelivered {
+						lane_id: runtime_types::bp_messages::LaneId,
+						messages: runtime_types::bp_messages::DeliveredMessages,
+					},
+				}
+			}
+		}
+		pub mod pallet_bridge_parachains {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					submit_parachain_heads {
+						at_relay_block: (::core::primitive::u32, ::subxt::utils::H256),
+						parachains: ::std::vec::Vec<(
+							::bp_polkadot_core::parachains::ParaId,
+							::subxt::utils::H256,
+						)>,
+						parachain_heads_proof: ::bp_polkadot_core::parachains::ParaHeadsProof,
+					},
+					#[codec(index = 1)]
+					set_owner { new_owner: ::core::option::Option<::sp_core::crypto::AccountId32> },
+					#[codec(index = 2)]
+					set_operating_mode {
+						operating_mode: runtime_types::bp_runtime::BasicOperatingMode,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					UnknownRelayChainBlock,
+					#[codec(index = 1)]
+					InvalidRelayChainBlockNumber,
+					#[codec(index = 2)]
+					HeaderChainStorageProof(runtime_types::bp_header_chain::HeaderChainError),
+					#[codec(index = 3)]
+					BridgeModule(runtime_types::bp_runtime::OwnedBridgeModuleError),
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					UntrackedParachainRejected { parachain: ::bp_polkadot_core::parachains::ParaId },
+					#[codec(index = 1)]
+					MissingParachainHead { parachain: ::bp_polkadot_core::parachains::ParaId },
+					#[codec(index = 2)]
+					IncorrectParachainHeadHash {
+						parachain: ::bp_polkadot_core::parachains::ParaId,
+						parachain_head_hash: ::subxt::utils::H256,
+						actual_parachain_head_hash: ::subxt::utils::H256,
+					},
+					#[codec(index = 3)]
+					RejectedObsoleteParachainHead {
+						parachain: ::bp_polkadot_core::parachains::ParaId,
+						parachain_head_hash: ::subxt::utils::H256,
+					},
+					#[codec(index = 4)]
+					RejectedLargeParachainHead {
+						parachain: ::bp_polkadot_core::parachains::ParaId,
+						parachain_head_hash: ::subxt::utils::H256,
+						parachain_head_size: ::core::primitive::u32,
+					},
+					#[codec(index = 5)]
+					UpdatedParachainHead {
+						parachain: ::bp_polkadot_core::parachains::ParaId,
+						parachain_head_hash: ::subxt::utils::H256,
+					},
+				}
+			}
+		}
+		pub mod pallet_grandpa {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					report_equivocation {
+						equivocation_proof: ::std::boxed::Box<
+							::sp_consensus_grandpa::EquivocationProof<
+								::subxt::utils::H256,
+								::core::primitive::u32,
+							>,
+						>,
+						key_owner_proof: ::sp_session::MembershipProof,
+					},
+					#[codec(index = 1)]
+					report_equivocation_unsigned {
+						equivocation_proof: ::std::boxed::Box<
+							::sp_consensus_grandpa::EquivocationProof<
+								::subxt::utils::H256,
+								::core::primitive::u32,
+							>,
+						>,
+						key_owner_proof: ::sp_session::MembershipProof,
+					},
+					#[codec(index = 2)]
+					note_stalled {
+						delay: ::core::primitive::u32,
+						best_finalized_block_number: ::core::primitive::u32,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					PauseFailed,
+					#[codec(index = 1)]
+					ResumeFailed,
+					#[codec(index = 2)]
+					ChangePending,
+					#[codec(index = 3)]
+					TooSoon,
+					#[codec(index = 4)]
+					InvalidKeyOwnershipProof,
+					#[codec(index = 5)]
+					InvalidEquivocationProof,
+					#[codec(index = 6)]
+					DuplicateOffenceReport,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					NewAuthorities {
+						authority_set: ::std::vec::Vec<(
+							runtime_types::sp_consensus_grandpa::app::Public,
+							::core::primitive::u64,
+						)>,
+					},
+					#[codec(index = 1)]
+					Paused,
+					#[codec(index = 2)]
+					Resumed,
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct StoredPendingChange<_0> {
+				pub scheduled_at: _0,
+				pub delay: _0,
+				pub next_authorities:
+					runtime_types::bounded_collections::weak_bounded_vec::WeakBoundedVec<(
+						runtime_types::sp_consensus_grandpa::app::Public,
+						::core::primitive::u64,
+					)>,
+				pub forced: ::core::option::Option<_0>,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum StoredState<_0> {
+				#[codec(index = 0)]
+				Live,
+				#[codec(index = 1)]
+				PendingPause { scheduled_at: _0, delay: _0 },
+				#[codec(index = 2)]
+				Paused,
+				#[codec(index = 3)]
+				PendingResume { scheduled_at: _0, delay: _0 },
+			}
+		}
+		pub mod pallet_im_online {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					heartbeat {
+						heartbeat:
+							runtime_types::pallet_im_online::Heartbeat<::core::primitive::u32>,
+						signature: runtime_types::pallet_im_online::sr25519::app_sr25519::Signature,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					InvalidKey,
+					#[codec(index = 1)]
+					DuplicatedHeartbeat,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					HeartbeatReceived {
+						authority_id: runtime_types::pallet_im_online::sr25519::app_sr25519::Public,
+					},
+					#[codec(index = 1)]
+					AllGood,
+					#[codec(index = 2)]
+					SomeOffline {
+						offline: ::std::vec::Vec<(
+							::sp_core::crypto::AccountId32,
+							::sp_core::crypto::AccountId32,
+						)>,
+					},
+				}
+			}
+			pub mod sr25519 {
+				use super::runtime_types;
+				pub mod app_sr25519 {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct Public(pub runtime_types::sp_core::sr25519::Public);
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct Signature(pub runtime_types::sp_core::sr25519::Signature);
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Heartbeat<_0> {
+				pub block_number: _0,
+				pub session_index: _0,
+				pub authority_index: _0,
+				pub validators_len: _0,
+			}
+		}
+		pub mod pallet_offences {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					Offence {
+						kind: [::core::primitive::u8; 16usize],
+						timeslot: ::std::vec::Vec<::core::primitive::u8>,
+					},
+				}
+			}
+		}
+		pub mod pallet_session {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					set_keys {
+						keys: runtime_types::polkadot_bulletin_chain_runtime::opaque::SessionKeys,
+						proof: ::std::vec::Vec<::core::primitive::u8>,
+					},
+					#[codec(index = 1)]
+					purge_keys,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					InvalidProof,
+					#[codec(index = 1)]
+					NoAssociatedValidatorId,
+					#[codec(index = 2)]
+					DuplicatedKey,
+					#[codec(index = 3)]
+					NoKeys,
+					#[codec(index = 4)]
+					NoAccount,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					NewSession { session_index: ::core::primitive::u32 },
+				}
+			}
+		}
+		pub mod pallet_sudo {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					sudo {
+						call: ::std::boxed::Box<
+							runtime_types::polkadot_bulletin_chain_runtime::RuntimeCall,
+						>,
+					},
+					#[codec(index = 1)]
+					sudo_unchecked_weight {
+						call: ::std::boxed::Box<
+							runtime_types::polkadot_bulletin_chain_runtime::RuntimeCall,
+						>,
+						weight: ::sp_weights::Weight,
+					},
+					#[codec(index = 2)]
+					set_key {
+						new: ::subxt::utils::MultiAddress<::sp_core::crypto::AccountId32, ()>,
+					},
+					#[codec(index = 3)]
+					sudo_as {
+						who: ::subxt::utils::MultiAddress<::sp_core::crypto::AccountId32, ()>,
+						call: ::std::boxed::Box<
+							runtime_types::polkadot_bulletin_chain_runtime::RuntimeCall,
+						>,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					RequireSudo,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					Sudid {
+						sudo_result:
+							::core::result::Result<(), runtime_types::sp_runtime::DispatchError>,
+					},
+					#[codec(index = 1)]
+					KeyChanged {
+						old_sudoer: ::core::option::Option<::sp_core::crypto::AccountId32>,
+					},
+					#[codec(index = 2)]
+					SudoAsDone {
+						sudo_result:
+							::core::result::Result<(), runtime_types::sp_runtime::DispatchError>,
+					},
+				}
+			}
+		}
+		pub mod pallet_timestamp {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					set {
+						#[codec(compact)]
+						now: ::core::primitive::u64,
+					},
+				}
+			}
+		}
+		pub mod pallet_transaction_storage {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					store { data: ::std::vec::Vec<::core::primitive::u8> },
+					#[codec(index = 1)]
+					renew { block: ::core::primitive::u32, index: ::core::primitive::u32 },
+					#[codec(index = 2)]
+					check_proof {
+						proof: runtime_types::sp_transaction_storage_proof::TransactionStorageProof,
+					},
+					#[codec(index = 3)]
+					authorize_account {
+						who: ::sp_core::crypto::AccountId32,
+						transactions: ::core::primitive::u32,
+						bytes: ::core::primitive::u64,
+					},
+					#[codec(index = 4)]
+					authorize_preimage {
+						hash: [::core::primitive::u8; 32usize],
+						max_size: ::core::primitive::u64,
+					},
+					#[codec(index = 5)]
+					remove_expired_account_authorization { who: ::sp_core::crypto::AccountId32 },
+					#[codec(index = 6)]
+					remove_expired_preimage_authorization { hash: [::core::primitive::u8; 32usize] },
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					BadContext,
+					#[codec(index = 1)]
+					BadDataSize,
+					#[codec(index = 2)]
+					TooManyTransactions,
+					#[codec(index = 3)]
+					RenewedNotFound,
+					#[codec(index = 4)]
+					UnexpectedProof,
+					#[codec(index = 5)]
+					InvalidProof,
+					#[codec(index = 6)]
+					MissingStateData,
+					#[codec(index = 7)]
+					DoubleCheck,
+					#[codec(index = 8)]
+					AuthorizationNotFound,
+					#[codec(index = 9)]
+					AuthorizationNotExpired,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					Stored { index: ::core::primitive::u32 },
+					#[codec(index = 1)]
+					Renewed { index: ::core::primitive::u32 },
+					#[codec(index = 2)]
+					ProofChecked,
+					#[codec(index = 3)]
+					AccountAuthorized {
+						who: ::sp_core::crypto::AccountId32,
+						transactions: ::core::primitive::u32,
+						bytes: ::core::primitive::u64,
+					},
+					#[codec(index = 4)]
+					PreimageAuthorized {
+						hash: [::core::primitive::u8; 32usize],
+						max_size: ::core::primitive::u64,
+					},
+					#[codec(index = 5)]
+					ExpiredAccountAuthorizationRemoved { who: ::sp_core::crypto::AccountId32 },
+					#[codec(index = 6)]
+					ExpiredPreimageAuthorizationRemoved { hash: [::core::primitive::u8; 32usize] },
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Authorization<_0> {
+				pub extent: runtime_types::pallet_transaction_storage::AuthorizationExtent,
+				pub expiration: _0,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct AuthorizationExtent {
+				pub transactions: ::core::primitive::u32,
+				pub bytes: ::core::primitive::u64,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum AuthorizationScope<_0> {
+				#[codec(index = 0)]
+				Account(_0),
+				#[codec(index = 1)]
+				Preimage([::core::primitive::u8; 32usize]),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct TransactionInfo {
+				pub chunk_root: ::subxt::utils::H256,
+				pub content_hash: ::subxt::utils::H256,
+				pub size: ::core::primitive::u32,
+				pub block_chunks: ::core::primitive::u32,
+			}
+		}
+		pub mod pallet_validator_set {
+			use super::runtime_types;
+			pub mod pallet {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Call {
+					#[codec(index = 0)]
+					add_validator { who: ::sp_core::crypto::AccountId32 },
+					#[codec(index = 1)]
+					remove_validator { who: ::sp_core::crypto::AccountId32 },
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Error {
+					#[codec(index = 0)]
+					Duplicate,
+					#[codec(index = 1)]
+					NotAValidator,
+					#[codec(index = 2)]
+					TooManyValidators,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum Event {
+					#[codec(index = 0)]
+					ValidatorAdded(::sp_core::crypto::AccountId32),
+					#[codec(index = 1)]
+					ValidatorRemoved(::sp_core::crypto::AccountId32),
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Validator<_0> {
+				pub min_set_keys_block: _0,
+			}
+		}
+		pub mod polkadot_bulletin_chain_runtime {
+			use super::runtime_types;
+			pub mod opaque {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct SessionKeys {
+					pub babe: runtime_types::sp_consensus_babe::app::Public,
+					pub grandpa: runtime_types::sp_consensus_grandpa::app::Public,
+					pub im_online: runtime_types::pallet_im_online::sr25519::app_sr25519::Public,
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct Runtime;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum RuntimeCall {
+				#[codec(index = 0)]
+				System(runtime_types::frame_system::pallet::Call),
+				#[codec(index = 1)]
+				Babe(runtime_types::pallet_babe::pallet::Call),
+				#[codec(index = 2)]
+				Timestamp(runtime_types::pallet_timestamp::pallet::Call),
+				#[codec(index = 6)]
+				ValidatorSet(runtime_types::pallet_validator_set::pallet::Call),
+				#[codec(index = 7)]
+				Session(runtime_types::pallet_session::pallet::Call),
+				#[codec(index = 8)]
+				ImOnline(runtime_types::pallet_im_online::pallet::Call),
+				#[codec(index = 9)]
+				Grandpa(runtime_types::pallet_grandpa::pallet::Call),
+				#[codec(index = 10)]
+				Sudo(runtime_types::pallet_sudo::pallet::Call),
+				#[codec(index = 11)]
+				TransactionStorage(runtime_types::pallet_transaction_storage::pallet::Call),
+				#[codec(index = 12)]
+				BridgePolkadotGrandpa(runtime_types::pallet_bridge_grandpa::pallet::Call),
+				#[codec(index = 13)]
+				BridgePolkadotParachains(runtime_types::pallet_bridge_parachains::pallet::Call),
+				#[codec(index = 14)]
+				BridgePolkadotBridgeHubMessages(
+					runtime_types::pallet_bridge_messages::pallet::Call,
+				),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum RuntimeEvent {
+				#[codec(index = 0)]
+				System(runtime_types::frame_system::pallet::Event),
+				#[codec(index = 4)]
+				Offences(runtime_types::pallet_offences::pallet::Event),
+				#[codec(index = 6)]
+				ValidatorSet(runtime_types::pallet_validator_set::pallet::Event),
+				#[codec(index = 7)]
+				Session(runtime_types::pallet_session::pallet::Event),
+				#[codec(index = 8)]
+				ImOnline(runtime_types::pallet_im_online::pallet::Event),
+				#[codec(index = 9)]
+				Grandpa(runtime_types::pallet_grandpa::pallet::Event),
+				#[codec(index = 10)]
+				Sudo(runtime_types::pallet_sudo::pallet::Event),
+				#[codec(index = 11)]
+				TransactionStorage(runtime_types::pallet_transaction_storage::pallet::Event),
+				#[codec(index = 12)]
+				BridgePolkadotGrandpa(runtime_types::pallet_bridge_grandpa::pallet::Event),
+				#[codec(index = 13)]
+				BridgePolkadotParachains(runtime_types::pallet_bridge_parachains::pallet::Event),
+				#[codec(index = 14)]
+				BridgePolkadotBridgeHubMessages(
+					runtime_types::pallet_bridge_messages::pallet::Event,
+				),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct ValidateSigned;
+		}
+		pub mod sp_arithmetic {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum ArithmeticError {
+				#[codec(index = 0)]
+				Underflow,
+				#[codec(index = 1)]
+				Overflow,
+				#[codec(index = 2)]
+				DivisionByZero,
+			}
+		}
+		pub mod sp_consensus_babe {
+			use super::runtime_types;
+			pub mod app {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Public(pub runtime_types::sp_core::sr25519::Public);
+			}
+			pub mod digests {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum NextConfigDescriptor {
+					#[codec(index = 1)]
+					V1 {
+						c: (::core::primitive::u64, ::core::primitive::u64),
+						allowed_slots: runtime_types::sp_consensus_babe::AllowedSlots,
+					},
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub enum PreDigest {
+					#[codec(index = 1)]
+					Primary(runtime_types::sp_consensus_babe::digests::PrimaryPreDigest),
+					#[codec(index = 2)]
+					SecondaryPlain(
+						runtime_types::sp_consensus_babe::digests::SecondaryPlainPreDigest,
+					),
+					#[codec(index = 3)]
+					SecondaryVRF(runtime_types::sp_consensus_babe::digests::SecondaryVRFPreDigest),
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct PrimaryPreDigest {
+					pub authority_index: ::core::primitive::u32,
+					pub slot: runtime_types::sp_consensus_slots::Slot,
+					pub vrf_signature: runtime_types::sp_core::sr25519::vrf::VrfSignature,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct SecondaryPlainPreDigest {
+					pub authority_index: ::core::primitive::u32,
+					pub slot: runtime_types::sp_consensus_slots::Slot,
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct SecondaryVRFPreDigest {
+					pub authority_index: ::core::primitive::u32,
+					pub slot: runtime_types::sp_consensus_slots::Slot,
+					pub vrf_signature: runtime_types::sp_core::sr25519::vrf::VrfSignature,
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum AllowedSlots {
+				#[codec(index = 0)]
+				PrimarySlots,
+				#[codec(index = 1)]
+				PrimaryAndSecondaryPlainSlots,
+				#[codec(index = 2)]
+				PrimaryAndSecondaryVRFSlots,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct BabeEpochConfiguration {
+				pub c: (::core::primitive::u64, ::core::primitive::u64),
+				pub allowed_slots: runtime_types::sp_consensus_babe::AllowedSlots,
+			}
+		}
+		pub mod sp_consensus_grandpa {
+			use super::runtime_types;
+			pub mod app {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Public(pub runtime_types::sp_core::ed25519::Public);
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Signature(pub runtime_types::sp_core::ed25519::Signature);
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum Equivocation<_0, _1> {
+				#[codec(index = 0)]
+				Prevote(
+					runtime_types::finality_grandpa::Equivocation<
+						runtime_types::sp_consensus_grandpa::app::Public,
+						runtime_types::finality_grandpa::Prevote<_0, _1>,
+						runtime_types::sp_consensus_grandpa::app::Signature,
+					>,
+				),
+				#[codec(index = 1)]
+				Precommit(
+					runtime_types::finality_grandpa::Equivocation<
+						runtime_types::sp_consensus_grandpa::app::Public,
+						runtime_types::finality_grandpa::Precommit<_0, _1>,
+						runtime_types::sp_consensus_grandpa::app::Signature,
+					>,
+				),
+			}
+		}
+		pub mod sp_consensus_slots {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct EquivocationProof<_0, _1> {
+				pub offender: _1,
+				pub slot: runtime_types::sp_consensus_slots::Slot,
+				pub first_header: _0,
+				pub second_header: _0,
+			}
+			#[derive(
+				:: codec :: Decode,
+				:: codec :: Encode,
+				:: subxt :: ext :: codec :: CompactAs,
+				Clone,
+				Debug,
+				PartialEq,
+			)]
+			pub struct Slot(pub ::core::primitive::u64);
+		}
+		pub mod sp_core {
+			use super::runtime_types;
+			pub mod crypto {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct KeyTypeId(pub [::core::primitive::u8; 4usize]);
+			}
+			pub mod ecdsa {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Signature(pub [::core::primitive::u8; 65usize]);
+			}
+			pub mod ed25519 {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Public(pub [::core::primitive::u8; 32usize]);
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Signature(pub [::core::primitive::u8; 64usize]);
+			}
+			pub mod sr25519 {
+				use super::runtime_types;
+				pub mod vrf {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct VrfSignature {
+						pub output: [::core::primitive::u8; 32usize],
+						pub proof: [::core::primitive::u8; 64usize],
+					}
+				}
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Public(pub [::core::primitive::u8; 32usize]);
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct Signature(pub [::core::primitive::u8; 64usize]);
+			}
+		}
+		pub mod sp_runtime {
+			use super::runtime_types;
+			pub mod generic {
+				use super::runtime_types;
+				pub mod digest {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub enum DigestItem {
+						#[codec(index = 6)]
+						PreRuntime(
+							[::core::primitive::u8; 4usize],
+							::std::vec::Vec<::core::primitive::u8>,
+						),
+						#[codec(index = 4)]
+						Consensus(
+							[::core::primitive::u8; 4usize],
+							::std::vec::Vec<::core::primitive::u8>,
+						),
+						#[codec(index = 5)]
+						Seal(
+							[::core::primitive::u8; 4usize],
+							::std::vec::Vec<::core::primitive::u8>,
+						),
+						#[codec(index = 0)]
+						Other(::std::vec::Vec<::core::primitive::u8>),
+						#[codec(index = 8)]
+						RuntimeEnvironmentUpdated,
+					}
+				}
+				pub mod unchecked_extrinsic {
+					use super::runtime_types;
+					#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+					pub struct UncheckedExtrinsic<_0, _1, _2, _3>(
+						pub ::std::vec::Vec<::core::primitive::u8>,
+						#[codec(skip)] pub ::core::marker::PhantomData<(_1, _0, _2, _3)>,
+					);
+				}
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum DispatchError {
+				#[codec(index = 0)]
+				Other,
+				#[codec(index = 1)]
+				CannotLookup,
+				#[codec(index = 2)]
+				BadOrigin,
+				#[codec(index = 3)]
+				Module(runtime_types::sp_runtime::ModuleError),
+				#[codec(index = 4)]
+				ConsumerRemaining,
+				#[codec(index = 5)]
+				NoProviders,
+				#[codec(index = 6)]
+				TooManyConsumers,
+				#[codec(index = 7)]
+				Token(runtime_types::sp_runtime::TokenError),
+				#[codec(index = 8)]
+				Arithmetic(runtime_types::sp_arithmetic::ArithmeticError),
+				#[codec(index = 9)]
+				Transactional(runtime_types::sp_runtime::TransactionalError),
+				#[codec(index = 10)]
+				Exhausted,
+				#[codec(index = 11)]
+				Corruption,
+				#[codec(index = 12)]
+				Unavailable,
+				#[codec(index = 13)]
+				RootNotAllowed,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct ModuleError {
+				pub index: ::core::primitive::u8,
+				pub error: [::core::primitive::u8; 4usize],
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum MultiSignature {
+				#[codec(index = 0)]
+				Ed25519(runtime_types::sp_core::ed25519::Signature),
+				#[codec(index = 1)]
+				Sr25519(runtime_types::sp_core::sr25519::Signature),
+				#[codec(index = 2)]
+				Ecdsa(runtime_types::sp_core::ecdsa::Signature),
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum TokenError {
+				#[codec(index = 0)]
+				FundsUnavailable,
+				#[codec(index = 1)]
+				OnlyProvider,
+				#[codec(index = 2)]
+				BelowMinimum,
+				#[codec(index = 3)]
+				CannotCreate,
+				#[codec(index = 4)]
+				UnknownAsset,
+				#[codec(index = 5)]
+				Frozen,
+				#[codec(index = 6)]
+				Unsupported,
+				#[codec(index = 7)]
+				CannotCreateHold,
+				#[codec(index = 8)]
+				NotExpendable,
+				#[codec(index = 9)]
+				Blocked,
+			}
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub enum TransactionalError {
+				#[codec(index = 0)]
+				LimitReached,
+				#[codec(index = 1)]
+				NoLayer,
+			}
+		}
+		pub mod sp_staking {
+			use super::runtime_types;
+			pub mod offence {
+				use super::runtime_types;
+				#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+				pub struct OffenceDetails<_0, _1> {
+					pub offender: _1,
+					pub reporters: ::std::vec::Vec<_0>,
+				}
+			}
+		}
+		pub mod sp_transaction_storage_proof {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct TransactionStorageProof {
+				pub chunk: ::std::vec::Vec<::core::primitive::u8>,
+				pub proof: ::std::vec::Vec<::std::vec::Vec<::core::primitive::u8>>,
+			}
+		}
+		pub mod sp_version {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct RuntimeVersion {
+				pub spec_name: ::std::string::String,
+				pub impl_name: ::std::string::String,
+				pub authoring_version: ::core::primitive::u32,
+				pub spec_version: ::core::primitive::u32,
+				pub impl_version: ::core::primitive::u32,
+				pub apis:
+					::std::vec::Vec<([::core::primitive::u8; 8usize], ::core::primitive::u32)>,
+				pub transaction_version: ::core::primitive::u32,
+				pub state_version: ::core::primitive::u8,
+			}
+		}
+		pub mod sp_weights {
+			use super::runtime_types;
+			#[derive(:: codec :: Decode, :: codec :: Encode, Clone, Debug, PartialEq)]
+			pub struct RuntimeDbWeight {
+				pub read: ::core::primitive::u64,
+				pub write: ::core::primitive::u64,
+			}
+		}
+	}
+}
diff --git a/bridges/relays/client-polkadot-bulletin/src/lib.rs b/bridges/relays/client-polkadot-bulletin/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..09fb7863a8b1c60e7c4468c7cb0411abb32c5dfd
--- /dev/null
+++ b/bridges/relays/client-polkadot-bulletin/src/lib.rs
@@ -0,0 +1,158 @@
+// Copyright 2019-2021 Parity Technologies (UK) Ltd.
+// This file is part of Parity Bridges Common.
+
+// Parity Bridges Common 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.
+
+// Parity Bridges Common 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 Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Types used to connect to the Polkadot Bulletin chain.
+
+mod codegen_runtime;
+
+use bp_messages::MessageNonce;
+use bp_polkadot_bulletin::POLKADOT_BULLETIN_SYNCED_HEADERS_GRANDPA_INFO_METHOD;
+use bp_runtime::ChainId;
+use codec::Encode;
+use relay_substrate_client::{
+	Chain, ChainWithBalances, ChainWithGrandpa, ChainWithMessages, ChainWithTransactions,
+	Error as SubstrateError, SignParam, UnderlyingChainProvider, UnsignedTransaction,
+};
+use sp_core::{storage::StorageKey, Pair};
+use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount, MultiAddress};
+use sp_session::MembershipProof;
+use std::time::Duration;
+
+pub use codegen_runtime::api::runtime_types;
+
+/// Call of the Polkadot Bulletin Chain runtime.
+pub type RuntimeCall = runtime_types::polkadot_bulletin_chain_runtime::RuntimeCall;
+/// Call of the `Sudo` pallet.
+pub type SudoCall = runtime_types::pallet_sudo::pallet::Call;
+/// Call of the GRANDPA pallet.
+pub type GrandpaCall = runtime_types::pallet_grandpa::pallet::Call;
+/// Call of the with-PolkadotBridgeHub bridge GRANDPA pallet.
+pub type BridgePolkadotGrandpaCall = runtime_types::pallet_bridge_grandpa::pallet::Call;
+/// Call of the with-PolkadotBridgeHub bridge parachains pallet.
+pub type BridgePolkadotParachainsCall = runtime_types::pallet_bridge_parachains::pallet::Call;
+/// Call of the with-PolkadotBridgeHub bridge messages pallet.
+pub type BridgePolkadotBridgeHubMessagesCall = runtime_types::pallet_bridge_messages::pallet::Call;
+
+/// Polkadot header id.
+pub type HeaderId =
+	relay_utils::HeaderId<bp_polkadot_bulletin::Hash, bp_polkadot_bulletin::BlockNumber>;
+
+/// Polkadot header type used in headers sync.
+pub type SyncHeader = relay_substrate_client::SyncHeader<bp_polkadot_bulletin::Header>;
+
+/// The address format for describing accounts.
+pub type Address = MultiAddress<bp_polkadot_bulletin::AccountId, ()>;
+
+/// Polkadot chain definition
+#[derive(Debug, Clone, Copy)]
+pub struct PolkadotBulletin;
+
+impl UnderlyingChainProvider for PolkadotBulletin {
+	type Chain = bp_polkadot_bulletin::PolkadotBulletin;
+}
+
+impl Chain for PolkadotBulletin {
+	const ID: ChainId = *b"pbch";
+
+	const NAME: &'static str = "PolkadotBulletin";
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_polkadot_bulletin::BEST_FINALIZED_POLKADOT_BULLETIN_HEADER_METHOD;
+	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
+
+	type SignedBlock = bp_polkadot_bulletin::SignedBlock;
+	type Call = RuntimeCall;
+}
+
+impl ChainWithGrandpa for PolkadotBulletin {
+	const SYNCED_HEADERS_GRANDPA_INFO_METHOD: &'static str =
+		POLKADOT_BULLETIN_SYNCED_HEADERS_GRANDPA_INFO_METHOD;
+
+	type KeyOwnerProof = MembershipProof;
+}
+
+impl ChainWithMessages for PolkadotBulletin {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_polkadot_bulletin::WITH_POLKADOT_BULLETIN_MESSAGES_PALLET_NAME;
+	// this is not critical (some metrics will be missing from the storage), but probably it needs
+	// to be changed when we'll polish the bridge configuration
+	const WITH_CHAIN_RELAYERS_PALLET_NAME: Option<&'static str> = None;
+
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_polkadot_bulletin::TO_POLKADOT_BULLETIN_MESSAGE_DETAILS_METHOD;
+	const FROM_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_polkadot_bulletin::FROM_POLKADOT_BULLETIN_MESSAGE_DETAILS_METHOD;
+
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_polkadot_bulletin::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_polkadot_bulletin::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+}
+
+impl ChainWithBalances for PolkadotBulletin {
+	fn account_info_storage_key(_account_id: &Self::AccountId) -> StorageKey {
+		// no balances at this chain
+		StorageKey(vec![])
+	}
+}
+
+impl ChainWithTransactions for PolkadotBulletin {
+	type AccountKeyPair = sp_core::sr25519::Pair;
+	type SignedTransaction =
+		bp_polkadot_bulletin::UncheckedExtrinsic<Self::Call, bp_polkadot_bulletin::SignedExtension>;
+
+	fn sign_transaction(
+		param: SignParam<Self>,
+		unsigned: UnsignedTransaction<Self>,
+	) -> Result<Self::SignedTransaction, SubstrateError> {
+		let raw_payload = SignedPayload::new(
+			unsigned.call,
+			bp_polkadot_bulletin::SignedExtension::from_params(
+				param.spec_version,
+				param.transaction_version,
+				unsigned.era,
+				param.genesis_hash,
+				unsigned.nonce,
+			),
+		)?;
+
+		let signature = raw_payload.using_encoded(|payload| param.signer.sign(payload));
+		let signer: sp_runtime::MultiSigner = param.signer.public().into();
+		let (call, extra, _) = raw_payload.deconstruct();
+
+		Ok(Self::SignedTransaction::new_signed(
+			call,
+			signer.into_account().into(),
+			signature.into(),
+			extra,
+		))
+	}
+
+	fn is_signed(tx: &Self::SignedTransaction) -> bool {
+		tx.signature.is_some()
+	}
+
+	fn is_signed_by(signer: &Self::AccountKeyPair, tx: &Self::SignedTransaction) -> bool {
+		tx.signature
+			.as_ref()
+			.map(|(address, _, _)| *address == Address::Id(signer.public().into()))
+			.unwrap_or(false)
+	}
+
+	fn parse_transaction(tx: Self::SignedTransaction) -> Option<UnsignedTransaction<Self>> {
+		let extra = &tx.signature.as_ref()?.2;
+		Some(UnsignedTransaction::new(tx.function, extra.nonce()))
+	}
+}
diff --git a/bridges/relays/client-substrate/src/guard.rs b/bridges/relays/client-substrate/src/guard.rs
index a4ed47424e818e924d343a7e795a741b9f5f4d8b..545396b30b85963fe0cc6afcdc62fe141a1069b8 100644
--- a/bridges/relays/client-substrate/src/guard.rs
+++ b/bridges/relays/client-substrate/src/guard.rs
@@ -17,7 +17,7 @@
 //! Pallet provides a set of guard functions that are running in background threads
 //! and are aborting process if some condition fails.
 
-use crate::{error::Error, Chain, ChainWithBalances, Client};
+use crate::{error::Error, Chain, Client};
 
 use async_trait::async_trait;
 use sp_version::RuntimeVersion;
@@ -28,7 +28,7 @@ use std::{
 
 /// Guards environment.
 #[async_trait]
-pub trait Environment<C: ChainWithBalances>: Send + Sync + 'static {
+pub trait Environment<C>: Send + Sync + 'static {
 	/// Error type.
 	type Error: Display + Send + Sync + 'static;
 
@@ -52,7 +52,7 @@ pub trait Environment<C: ChainWithBalances>: Send + Sync + 'static {
 }
 
 /// Abort when runtime spec version is different from specified.
-pub fn abort_on_spec_version_change<C: ChainWithBalances>(
+pub fn abort_on_spec_version_change<C: Chain>(
 	mut env: impl Environment<C>,
 	expected_spec_version: u32,
 ) {
@@ -98,7 +98,7 @@ fn conditions_check_delay<C: Chain>() -> Duration {
 }
 
 #[async_trait]
-impl<C: ChainWithBalances> Environment<C> for Client<C> {
+impl<C: Chain> Environment<C> for Client<C> {
 	type Error = Error;
 
 	async fn runtime_version(&mut self) -> Result<RuntimeVersion, Self::Error> {
diff --git a/bridges/relays/lib-substrate-relay/src/messages_lane.rs b/bridges/relays/lib-substrate-relay/src/messages_lane.rs
index b86a2629b07515e7e47a93303ff7c48cd6f4aecf..413f3d2c51ef5ca66f9a99810b0dc5cc7ab1badc 100644
--- a/bridges/relays/lib-substrate-relay/src/messages_lane.rs
+++ b/bridges/relays/lib-substrate-relay/src/messages_lane.rs
@@ -105,10 +105,21 @@ pub struct MessagesRelayParams<P: SubstrateMessageLane> {
 		Option<Arc<dyn OnDemandRelay<P::TargetChain, P::SourceChain>>>,
 	/// Identifier of lane that needs to be served.
 	pub lane_id: LaneId,
+	/// Messages relay limits. If not provided, the relay tries to determine it automatically,
+	/// using `TransactionPayment` pallet runtime API.
+	pub limits: Option<MessagesRelayLimits>,
 	/// Metrics parameters.
 	pub metrics_params: MetricsParams,
 }
 
+/// Delivery transaction limits.
+pub struct MessagesRelayLimits {
+	/// Maximal number of messages in the delivery transaction.
+	pub max_messages_in_single_batch: MessageNonce,
+	/// Maximal cumulative weight of messages in the delivery transaction.
+	pub max_messages_weight_in_single_batch: Weight,
+}
+
 /// Batch transaction that brings headers + and messages delivery/receiving confirmations to the
 /// source node.
 #[derive(Clone)]
@@ -178,15 +189,18 @@ where
 	let max_messages_size_in_single_batch = P::TargetChain::max_extrinsic_size() / 3;
 	// we don't know exact weights of the Polkadot runtime. So to guess weights we'll be using
 	// weights from Rialto and then simply dividing it by x2.
+	let limits = match params.limits {
+		Some(limits) => limits,
+		None =>
+			select_delivery_transaction_limits_rpc::<P>(
+				&params,
+				P::TargetChain::max_extrinsic_weight(),
+				P::SourceChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+			)
+			.await?,
+	};
 	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits_rpc::<P>(
-			&params,
-			P::TargetChain::max_extrinsic_weight(),
-			P::SourceChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
-		)
-		.await?;
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		(max_messages_in_single_batch / 2, max_messages_weight_in_single_batch / 2);
+		(limits.max_messages_in_single_batch / 2, limits.max_messages_weight_in_single_batch / 2);
 
 	let source_client = params.source_client;
 	let target_client = params.target_client;
@@ -457,7 +471,7 @@ async fn select_delivery_transaction_limits_rpc<P: SubstrateMessageLane>(
 	params: &MessagesRelayParams<P>,
 	max_extrinsic_weight: Weight,
 	max_unconfirmed_messages_at_inbound_lane: MessageNonce,
-) -> anyhow::Result<(MessageNonce, Weight)>
+) -> anyhow::Result<MessagesRelayLimits>
 where
 	AccountIdOf<P::SourceChain>: From<<AccountKeyPairOf<P::SourceChain> as Pair>::Public>,
 {
@@ -515,7 +529,10 @@ where
 		"Relay shall be able to deliver messages with dispatch weight = max_extrinsic_weight / 2",
 	);
 
-	Ok((max_number_of_messages, weight_for_messages_dispatch))
+	Ok(MessagesRelayLimits {
+		max_messages_in_single_batch: max_number_of_messages,
+		max_messages_weight_in_single_batch: weight_for_messages_dispatch,
+	})
 }
 
 /// Returns dummy message delivery transaction with zero messages and `1kb` proof.