diff --git a/polkadot/Cargo.lock b/polkadot/Cargo.lock
index d18327a56656be0b9f6710ed74a678afed7aaca9..194de7660432e7657b7f70114608cf29410b070a 100644
--- a/polkadot/Cargo.lock
+++ b/polkadot/Cargo.lock
@@ -726,9 +726,12 @@ name = "bp-header-chain"
 version = "0.1.0"
 dependencies = [
  "assert_matches",
+ "bp-runtime",
  "bp-test-utils",
  "finality-grandpa",
  "frame-support",
+ "hex",
+ "hex-literal",
  "parity-scale-codec",
  "scale-info",
  "serde",
@@ -757,10 +760,13 @@ dependencies = [
  "bp-runtime",
  "frame-support",
  "frame-system",
+ "hex",
+ "hex-literal",
  "impl-trait-for-tuples",
  "parity-scale-codec",
  "scale-info",
  "serde",
+ "sp-core",
  "sp-std",
 ]
 
@@ -853,18 +859,23 @@ dependencies = [
  "bp-runtime",
  "ed25519-dalek",
  "frame-support",
+ "frame-system",
  "hash-db",
+ "pallet-balances",
  "pallet-bridge-dispatch",
  "pallet-bridge-grandpa",
  "pallet-bridge-messages",
  "pallet-transaction-payment",
  "parity-scale-codec",
  "scale-info",
+ "sp-api",
  "sp-core",
  "sp-runtime",
  "sp-state-machine",
  "sp-std",
  "sp-trie",
+ "sp-version",
+ "static_assertions",
 ]
 
 [[package]]
@@ -5256,8 +5267,6 @@ dependencies = [
  "frame-benchmarking",
  "frame-support",
  "frame-system",
- "hex",
- "hex-literal",
  "log",
  "num-traits",
  "pallet-balances",
diff --git a/polkadot/bridges/.gitlab-ci.yml b/polkadot/bridges/.gitlab-ci.yml
index 0e69a91af165143d5d71b26fceb7ecb0645a893f..7d3bf6fd8ac2f2faa4201d8d8c0e2a005080741b 100644
--- a/polkadot/bridges/.gitlab-ci.yml
+++ b/polkadot/bridges/.gitlab-ci.yml
@@ -47,7 +47,7 @@ default:
     when:
       - runner_system_failure
       - unknown_failure
-      - api_failure
+      - api_failure 
   interruptible:                   true
   tags:
     - linux-docker
@@ -240,7 +240,14 @@ build-nightly:
       elif [[ "${CI_COMMIT_REF_NAME}" ]]; then
         VERSION=$(echo ${CI_COMMIT_REF_NAME} | sed -r 's#/+#-#g');
       fi
-    - echo "Effective tags = ${VERSION} sha-${CI_COMMIT_SHORT_SHA} latest"
+    # When building from version tags (v1.0, v2.1rc1, ...) we'll use "production" to tag
+    # docker image. In all other cases, it'll be "latest".
+    - if [[ $CI_COMMIT_REF_NAME =~ ^v[0-9]+\.[0-9]+.*$ ]]; then
+        FLOATING_TAG="production";
+      else
+        FLOATING_TAG="latest";
+      fi
+    - echo "Effective tags = ${VERSION} sha-${CI_COMMIT_SHORT_SHA} ${FLOATING_TAG}"
   secrets:
       DOCKER_HUB_USER:
         vault:                     cicd/gitlab/parity/DOCKER_HUB_USER@kv
@@ -260,7 +267,7 @@ build-nightly:
         --build-arg VERSION="${VERSION}"
         --tag "${IMAGE_NAME}:${VERSION}"
         --tag "${IMAGE_NAME}:sha-${CI_COMMIT_SHORT_SHA}"
-        --tag "${IMAGE_NAME}:latest"
+        --tag "${IMAGE_NAME}:${FLOATING_TAG}"
         --file "${DOCKERFILE}" .
     # The job will success only on the protected branch
     - echo "${DOCKER_HUB_PASS}" |
@@ -268,7 +275,7 @@ build-nightly:
     - buildah info
     - buildah push --format=v2s2 "${IMAGE_NAME}:${VERSION}"
     - buildah push --format=v2s2 "${IMAGE_NAME}:sha-${CI_COMMIT_SHORT_SHA}"
-    - buildah push --format=v2s2 "${IMAGE_NAME}:latest"
+    - buildah push --format=v2s2 "${IMAGE_NAME}:${FLOATING_TAG}"
   after_script:
     - env REGISTRY_AUTH_FILE= buildah logout --all
 
diff --git a/polkadot/bridges/.maintain/rialto-weight-template.hbs b/polkadot/bridges/.maintain/rialto-weight-template.hbs
deleted file mode 100644
index cb1b58d23b26420e9f6d89c9b7fc6ce3b7fa9141..0000000000000000000000000000000000000000
--- a/polkadot/bridges/.maintain/rialto-weight-template.hbs
+++ /dev/null
@@ -1,108 +0,0 @@
-// 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 />.
-
-//! Autogenerated weights for `{{pallet}}`
-//!
-//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION {{version}}
-//! DATE: {{date}}, STEPS: {{cmd.steps}}, REPEAT: {{cmd.repeat}}
-//! LOW RANGE: {{cmd.lowest_range_values}}, HIGH RANGE: {{cmd.highest_range_values}}
-//! EXECUTION: {{cmd.execution}}, WASM-EXECUTION: {{cmd.wasm_execution}}
-//! CHAIN: {{cmd.chain}}, DB CACHE: {{cmd.db_cache}}
-
-// Executed Command:
-{{#each args as |arg|~}}
-// {{arg}}
-{{/each}}
-
-#![allow(clippy::all)]
-#![allow(unused_parens)]
-#![allow(unused_imports)]
-
-use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
-use sp_std::marker::PhantomData;
-
-/// Weight functions needed for `{{pallet}}`.
-pub trait WeightInfo {
-{{~#each benchmarks as |benchmark|}}
-fn {{benchmark.name~}}
-(
-{{~#each benchmark.components as |c| ~}}
-{{c.name}}: u32, {{/each~}}
-) -> Weight;
-{{~/each}}
-}
-
-/// Weights for `{{pallet}}` using the Rialto node and recommended hardware.
-pub struct RialtoWeight<T>(PhantomData<T>);
-		impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
-				{{~#each benchmarks as |benchmark|}}
-				fn {{benchmark.name~}}
-				(
-				{{~#each benchmark.components as |c| ~}}
-				{{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}}
-				) -> Weight {
-				({{underscore benchmark.base_weight}} as Weight)
-				{{~#each benchmark.component_weight as |cw|}}
-				.saturating_add(({{underscore cw.slope}} as Weight).saturating_mul({{cw.name}} as Weight))
-				{{~/each}}
-				{{~#if (ne benchmark.base_reads "0")}}
-				.saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}} as Weight))
-				{{~/if}}
-				{{~#each benchmark.component_reads as |cr|}}
-				.saturating_add(T::DbWeight::get().reads(({{cr.slope}} as Weight).saturating_mul({{cr.name}} as
-				Weight)))
-				{{~/each}}
-				{{~#if (ne benchmark.base_writes "0")}}
-				.saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}} as Weight))
-				{{~/if}}
-				{{~#each benchmark.component_writes as |cw|}}
-				.saturating_add(T::DbWeight::get().writes(({{cw.slope}} as Weight).saturating_mul({{cw.name}} as
-				Weight)))
-				{{~/each}}
-				}
-				{{~/each}}
-				}
-
-				// For backwards compatibility and tests
-				impl WeightInfo for () {
-				{{~#each benchmarks as |benchmark|}}
-				fn {{benchmark.name~}}
-				(
-				{{~#each benchmark.components as |c| ~}}
-				{{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}}
-				) -> Weight {
-				({{underscore benchmark.base_weight}} as Weight)
-				{{~#each benchmark.component_weight as |cw|}}
-				.saturating_add(({{underscore cw.slope}} as Weight).saturating_mul({{cw.name}} as Weight))
-				{{~/each}}
-				{{~#if (ne benchmark.base_reads "0")}}
-				.saturating_add(RocksDbWeight::get().reads({{benchmark.base_reads}} as Weight))
-				{{~/if}}
-				{{~#each benchmark.component_reads as |cr|}}
-				.saturating_add(RocksDbWeight::get().reads(({{cr.slope}} as Weight).saturating_mul({{cr.name}} as
-				Weight)))
-				{{~/each}}
-				{{~#if (ne benchmark.base_writes "0")}}
-				.saturating_add(RocksDbWeight::get().writes({{benchmark.base_writes}} as Weight))
-				{{~/if}}
-				{{~#each benchmark.component_writes as |cw|}}
-				.saturating_add(RocksDbWeight::get().writes(({{cw.slope}} as Weight).saturating_mul({{cw.name}} as
-				Weight)))
-				{{~/each}}
-				}
-				{{~/each}}
-				}
\ No newline at end of file
diff --git a/polkadot/bridges/bin/millau/node/Cargo.toml b/polkadot/bridges/bin/millau/node/Cargo.toml
index c4438d0cef3ee610566efacce609ad2193b12bdd..3825b92b703c24307004b700513ff769c3553fdf 100644
--- a/polkadot/bridges/bin/millau/node/Cargo.toml
+++ b/polkadot/bridges/bin/millau/node/Cargo.toml
@@ -3,15 +3,15 @@ name = "millau-bridge-node"
 description = "Substrate node compatible with Millau runtime"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 build = "build.rs"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
+clap = { version = "3.1", features = ["derive"] }
 jsonrpc-core = "18.0"
-structopt = "0.3.21"
 serde_json = "1.0.59"
 
 # Bridge dependencies
diff --git a/polkadot/bridges/bin/millau/node/src/chain_spec.rs b/polkadot/bridges/bin/millau/node/src/chain_spec.rs
index d3d30b151b2f11d3ee477f300ee36ee769b61ab3..a7e3c7c8771830cb9adb38cae89442c04d11118e 100644
--- a/polkadot/bridges/bin/millau/node/src/chain_spec.rs
+++ b/polkadot/bridges/bin/millau/node/src/chain_spec.rs
@@ -88,21 +88,14 @@ impl Alternative {
 					testnet_genesis(
 						vec![get_authority_keys_from_seed("Alice")],
 						get_account_id_from_seed::<sr25519::Public>("Alice"),
-						vec![
-							get_account_id_from_seed::<sr25519::Public>("Alice"),
-							get_account_id_from_seed::<sr25519::Public>("Bob"),
-							get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Alice"),
-							)),
-						],
+						endowed_accounts(),
 						true,
 					)
 				},
 				vec![],
 				None,
 				None,
+				None,
 				properties,
 				None,
 			),
@@ -120,54 +113,14 @@ impl Alternative {
 							get_authority_keys_from_seed("Eve"),
 						],
 						get_account_id_from_seed::<sr25519::Public>("Alice"),
-						vec![
-							get_account_id_from_seed::<sr25519::Public>("Alice"),
-							get_account_id_from_seed::<sr25519::Public>("Bob"),
-							get_account_id_from_seed::<sr25519::Public>("Charlie"),
-							get_account_id_from_seed::<sr25519::Public>("Dave"),
-							get_account_id_from_seed::<sr25519::Public>("Eve"),
-							get_account_id_from_seed::<sr25519::Public>("Ferdie"),
-							get_account_id_from_seed::<sr25519::Public>("George"),
-							get_account_id_from_seed::<sr25519::Public>("Harry"),
-							get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Charlie//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Dave//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Eve//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Ferdie//stash"),
-							get_account_id_from_seed::<sr25519::Public>("George//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Harry//stash"),
-							get_account_id_from_seed::<sr25519::Public>("RialtoMessagesOwner"),
-							get_account_id_from_seed::<sr25519::Public>("WithRialtoTokenSwap"),
-							pallet_bridge_messages::relayer_fund_account_id::<
-								bp_millau::AccountId,
-								bp_millau::AccountIdConverter,
-							>(),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Alice"),
-							)),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Bob"),
-							)),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Charlie"),
-							)),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Dave"),
-							)),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Eve"),
-							)),
-							derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Ferdie"),
-							)),
-						],
+						endowed_accounts(),
 						true,
 					)
 				},
 				vec![],
 				None,
 				None,
+				None,
 				properties,
 				None,
 			),
@@ -175,6 +128,55 @@ impl Alternative {
 	}
 }
 
+/// We're using the same set of endowed accounts on all Millau chains (dev/local) to make
+/// sure that all accounts, required for bridge to be functional (e.g. relayers fund account,
+/// accounts used by relayers in our test deployments, accounts used for demonstration
+/// purposes), are all available on these chains.
+fn endowed_accounts() -> Vec<AccountId> {
+	vec![
+		get_account_id_from_seed::<sr25519::Public>("Alice"),
+		get_account_id_from_seed::<sr25519::Public>("Bob"),
+		get_account_id_from_seed::<sr25519::Public>("Charlie"),
+		get_account_id_from_seed::<sr25519::Public>("Dave"),
+		get_account_id_from_seed::<sr25519::Public>("Eve"),
+		get_account_id_from_seed::<sr25519::Public>("Ferdie"),
+		get_account_id_from_seed::<sr25519::Public>("George"),
+		get_account_id_from_seed::<sr25519::Public>("Harry"),
+		get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Charlie//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Dave//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Eve//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Ferdie//stash"),
+		get_account_id_from_seed::<sr25519::Public>("George//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Harry//stash"),
+		get_account_id_from_seed::<sr25519::Public>("RialtoMessagesOwner"),
+		get_account_id_from_seed::<sr25519::Public>("WithRialtoTokenSwap"),
+		pallet_bridge_messages::relayer_fund_account_id::<
+			bp_millau::AccountId,
+			bp_millau::AccountIdConverter,
+		>(),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Alice"),
+		)),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Bob"),
+		)),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Charlie"),
+		)),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Dave"),
+		)),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Eve"),
+		)),
+		derive_account_from_rialto_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Ferdie"),
+		)),
+	]
+}
+
 fn session_keys(aura: AuraId, beefy: BeefyId, grandpa: GrandpaId) -> SessionKeys {
 	SessionKeys { aura, beefy, grandpa }
 }
diff --git a/polkadot/bridges/bin/millau/node/src/cli.rs b/polkadot/bridges/bin/millau/node/src/cli.rs
index 086def633c59866d067ee11e891e93490e06e0d9..c3c3d134e341176c30927d2978ef2de579966d6e 100644
--- a/polkadot/bridges/bin/millau/node/src/cli.rs
+++ b/polkadot/bridges/bin/millau/node/src/cli.rs
@@ -14,10 +14,10 @@
 // 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/>.
 
+use clap::Parser;
 use sc_cli::RunCmd;
-use structopt::StructOpt;
 
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct Cli {
 	#[structopt(subcommand)]
 	pub subcommand: Option<Subcommand>,
@@ -27,9 +27,10 @@ pub struct Cli {
 }
 
 /// Possible subcommands of the main binary.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub enum Subcommand {
 	/// Key management CLI utilities
+	#[clap(subcommand)]
 	Key(sc_cli::KeySubcommand),
 
 	/// Verify a signature for a message, provided on `STDIN`, with a given (public or secret) key.
diff --git a/polkadot/bridges/bin/millau/node/src/service.rs b/polkadot/bridges/bin/millau/node/src/service.rs
index 1fc7584d0a0fb45ed78788707431bfa929d3e716..15f88269aa9c6971dfe67fdc40d7568b090dc01a 100644
--- a/polkadot/bridges/bin/millau/node/src/service.rs
+++ b/polkadot/bridges/bin/millau/node/src/service.rs
@@ -30,14 +30,13 @@
 // =====================================================================================
 
 use millau_runtime::{self, opaque::Block, RuntimeApi};
-use sc_client_api::ExecutorProvider;
+use sc_client_api::{BlockBackend, ExecutorProvider};
 use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams};
 pub use sc_executor::NativeElseWasmExecutor;
 use sc_finality_grandpa::SharedVoterState;
 use sc_keystore::LocalKeystore;
 use sc_service::{error::Error as ServiceError, Configuration, TaskManager};
 use sc_telemetry::{Telemetry, TelemetryWorker};
-use sp_consensus::SlotData;
 use sp_consensus_aura::sr25519::AuthorityPair as AuraPair;
 use std::{sync::Arc, time::Duration};
 
@@ -66,6 +65,7 @@ type FullClient =
 type FullBackend = sc_service::TFullBackend<Block>;
 type FullSelectChain = sc_consensus::LongestChain<FullBackend, Block>;
 
+#[allow(clippy::type_complexity)]
 pub fn new_partial(
 	config: &Configuration,
 ) -> Result<
@@ -89,7 +89,7 @@ pub fn new_partial(
 	ServiceError,
 > {
 	if config.keystore_remote.is_some() {
-		return Err(ServiceError::Other(format!("Remote Keystores are not supported.")))
+		return Err(ServiceError::Other("Remote Keystores are not supported.".into()))
 	}
 
 	let telemetry = config
@@ -112,7 +112,7 @@ pub fn new_partial(
 
 	let (client, backend, keystore_container, task_manager) =
 		sc_service::new_full_parts::<Block, RuntimeApi, _>(
-			&config,
+			config,
 			telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()),
 			executor,
 		)?;
@@ -140,7 +140,7 @@ pub fn new_partial(
 		telemetry.as_ref().map(|x| x.handle()),
 	)?;
 
-	let slot_duration = sc_consensus_aura::slot_duration(&*client)?.slot_duration();
+	let slot_duration = sc_consensus_aura::slot_duration(&*client)?;
 
 	let import_queue =
 		sc_consensus_aura::import_queue::<AuraPair, _, _, _, _, _, _>(ImportQueueParams {
@@ -151,7 +151,7 @@ pub fn new_partial(
 				let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
 
 				let slot =
-					sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_duration(
+					sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
 						*timestamp,
 						slot_duration,
 					);
@@ -179,7 +179,7 @@ pub fn new_partial(
 	})
 }
 
-fn remote_keystore(_url: &String) -> Result<Arc<LocalKeystore>, &'static str> {
+fn remote_keystore(_url: &str) -> Result<Arc<LocalKeystore>, &'static str> {
 	// FIXME: here would the concrete keystore be built,
 	//        must return a concrete type (NOT `LocalKeystore`) that
 	//        implements `CryptoStore` and `SyncCryptoStore`
@@ -210,8 +210,27 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 		};
 	}
 
-	config.network.extra_sets.push(sc_finality_grandpa::grandpa_peers_set_config());
-	config.network.extra_sets.push(beefy_gadget::beefy_peers_set_config());
+	// Note: GrandPa is pushed before the Polkadot-specific protocols. This doesn't change
+	// anything in terms of behaviour, but makes the logs more consistent with the other
+	// Substrate nodes.
+	let grandpa_protocol_name = sc_finality_grandpa::protocol_standard_name(
+		&client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"),
+		&config.chain_spec,
+	);
+	config
+		.network
+		.extra_sets
+		.push(sc_finality_grandpa::grandpa_peers_set_config(grandpa_protocol_name.clone()));
+
+	let beefy_protocol_name = beefy_gadget::protocol_standard_name(
+		&client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"),
+		&config.chain_spec,
+	);
+	config
+		.network
+		.extra_sets
+		.push(beefy_gadget::beefy_peers_set_config(beefy_protocol_name.clone()));
+
 	let warp_sync = Arc::new(sc_finality_grandpa::warp_proof::NetworkProvider::new(
 		backend.clone(),
 		grandpa_link.shared_authority_set().clone(),
@@ -245,8 +264,10 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 	let enable_grandpa = !config.disable_grandpa;
 	let prometheus_registry = config.prometheus_registry().cloned();
 	let shared_voter_state = SharedVoterState::empty();
-	let (signed_commitment_sender, signed_commitment_stream) =
-		beefy_gadget::notification::BeefySignedCommitmentStream::channel();
+	let (beefy_commitment_link, beefy_commitment_stream) =
+		beefy_gadget::notification::BeefySignedCommitmentStream::<Block>::channel();
+	let (beefy_best_block_link, beefy_best_block_stream) =
+		beefy_gadget::notification::BeefyBestBlockStream::<Block>::channel();
 
 	let rpc_extensions_builder = {
 		use sc_finality_grandpa::FinalityProofProvider as GrandpaFinalityProofProvider;
@@ -287,10 +308,12 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 				finality_proof_provider.clone(),
 			)));
 			io.extend_with(beefy_gadget_rpc::BeefyApi::to_delegate(
-				beefy_gadget_rpc::BeefyRpcHandler::new(
-					signed_commitment_stream.clone(),
+				beefy_gadget_rpc::BeefyRpcHandler::<Block>::new(
+					beefy_commitment_stream.clone(),
+					beefy_best_block_stream.clone(),
 					subscription_executor,
-				),
+				)
+				.map_err(|e| sc_service::Error::Other(format!("{}", e)))?,
 			));
 			io.extend_with(pallet_mmr_rpc::MmrApi::to_delegate(pallet_mmr_rpc::Mmr::new(
 				client.clone(),
@@ -325,7 +348,6 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 			sp_consensus::CanAuthorWithNativeVersion::new(client.executor().clone());
 
 		let slot_duration = sc_consensus_aura::slot_duration(&*client)?;
-		let raw_slot_duration = slot_duration.slot_duration();
 
 		let aura = sc_consensus_aura::start_aura::<AuraPair, _, _, _, _, _, _, _, _, _, _, _>(
 			StartAuraParams {
@@ -338,9 +360,9 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 					let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
 
 					let slot =
-						sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_duration(
+						sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
 							*timestamp,
-							raw_slot_duration,
+							slot_duration,
 						);
 
 					Ok((timestamp, slot))
@@ -374,9 +396,11 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 		backend,
 		key_store: keystore.clone(),
 		network: network.clone(),
-		signed_commitment_sender,
+		signed_commitment_sender: beefy_commitment_link,
+		beefy_best_block_sender: beefy_best_block_link,
 		min_block_delta: 4,
 		prometheus_registry: prometheus_registry.clone(),
+		protocol_name: beefy_protocol_name,
 	};
 
 	// Start the BEEFY bridge gadget.
@@ -395,6 +419,7 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
 		keystore,
 		local_role: role,
 		telemetry: telemetry.as_ref().map(|x| x.handle()),
+		protocol_name: grandpa_protocol_name,
 	};
 
 	if enable_grandpa {
diff --git a/polkadot/bridges/bin/millau/runtime/Cargo.toml b/polkadot/bridges/bin/millau/runtime/Cargo.toml
index 13195b95194ba787f82746181a530ce498754578..162404b77e7de4bb8c9bc22125ecb628cc0c8d4c 100644
--- a/polkadot/bridges/bin/millau/runtime/Cargo.toml
+++ b/polkadot/bridges/bin/millau/runtime/Cargo.toml
@@ -2,20 +2,22 @@
 name = "millau-runtime"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 hex-literal = "0.3"
-codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false, features = ["derive"] }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
+libsecp256k1 = { version = "0.7", optional = true, default-features = false, features = ["hmac"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true, features = ["derive"] }
 
 # Bridge dependencies
 
 bp-header-chain = { path = "../../../primitives/header-chain", default-features = false }
+bp-message-dispatch = { path = "../../../primitives/message-dispatch", default-features = false }
 bp-messages = { path = "../../../primitives/messages", default-features = false }
 bp-millau = { path = "../../../primitives/chain-millau", default-features = false }
 bp-rialto = { path = "../../../primitives/chain-rialto", default-features = false }
@@ -63,6 +65,10 @@ sp-transaction-pool = { git = "https://github.com/paritytech/substrate", branch
 sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-version = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
+[dev-dependencies]
+bridge-runtime-common = { path = "../../runtime-common", features = ["integrity-test"] }
+static_assertions = "1.1"
+
 [build-dependencies]
 substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
 
@@ -71,6 +77,7 @@ default = ["std"]
 std = [
 	"beefy-primitives/std",
 	"bp-header-chain/std",
+	"bp-message-dispatch/std",
 	"bp-messages/std",
 	"bp-millau/std",
 	"bp-rialto/std",
@@ -116,8 +123,12 @@ std = [
 	"sp-version/std",
 ]
 runtime-benchmarks = [
-	"frame-benchmarking",
+	"bridge-runtime-common/runtime-benchmarks",
+	"frame-benchmarking/runtime-benchmarks",
 	"frame-support/runtime-benchmarks",
 	"frame-system/runtime-benchmarks",
+	"libsecp256k1",
+	"pallet-bridge-messages/runtime-benchmarks",
 	"pallet-bridge-token-swap/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
 ]
diff --git a/polkadot/bridges/bin/millau/runtime/src/lib.rs b/polkadot/bridges/bin/millau/runtime/src/lib.rs
index 97fe4d49bc5f119dcd5e23cbf9e049b27761bfa2..810a4572e60bd830587e8253e9c697d0cc759fde 100644
--- a/polkadot/bridges/bin/millau/runtime/src/lib.rs
+++ b/polkadot/bridges/bin/millau/runtime/src/lib.rs
@@ -50,7 +50,7 @@ use sp_runtime::{
 	create_runtime_str, generic, impl_opaque_keys,
 	traits::{Block as BlockT, IdentityLookup, Keccak256, NumberFor, OpaqueKeys},
 	transaction_validity::{TransactionSource, TransactionValidity},
-	ApplyExtrinsicResult, FixedPointNumber, MultiSignature, MultiSigner, Perquintill,
+	ApplyExtrinsicResult, FixedPointNumber, FixedU128, MultiSignature, MultiSigner, Perquintill,
 };
 use sp_std::prelude::*;
 #[cfg(feature = "std")]
@@ -136,6 +136,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	impl_version: 1,
 	apis: RUNTIME_API_VERSIONS,
 	transaction_version: 1,
+	state_version: 0,
 };
 
 /// The version information used to identify this runtime when compiled natively.
@@ -373,12 +374,26 @@ parameter_types! {
 	// Note that once this is hit the pallet will essentially throttle incoming requests down to one
 	// call per block.
 	pub const MaxRequests: u32 = 50;
+}
 
-	// Number of headers to keep.
-	//
-	// Assuming the worst case of every header being finalized, we will keep headers for at least a
-	// week.
-	pub const HeadersToKeep: u32 = 7 * bp_millau::DAYS as u32;
+#[cfg(feature = "runtime-benchmarks")]
+parameter_types! {
+	/// Number of headers to keep in benchmarks.
+	///
+	/// In benchmarks we always populate with full number of `HeadersToKeep` to make sure that
+	/// pruning is taken into account.
+	///
+	/// Note: This is lower than regular value, to speed up benchmarking setup.
+	pub const HeadersToKeep: u32 = 1024;
+}
+
+#[cfg(not(feature = "runtime-benchmarks"))]
+parameter_types! {
+	/// Number of headers to keep.
+	///
+	/// Assuming the worst case of every header being finalized, we will keep headers at least for a
+	/// week.
+	pub const HeadersToKeep: u32 = 7 * bp_rialto::DAYS as u32;
 }
 
 pub type RialtoGrandpaInstance = ();
@@ -387,8 +402,7 @@ impl pallet_bridge_grandpa::Config for Runtime {
 	type MaxRequests = MaxRequests;
 	type HeadersToKeep = HeadersToKeep;
 
-	// TODO [#391]: Use weights generated for the Millau runtime instead of Rialto ones.
-	type WeightInfo = pallet_bridge_grandpa::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_grandpa::weights::MillauWeight<Runtime>;
 }
 
 pub type WestendGrandpaInstance = pallet_bridge_grandpa::Instance1;
@@ -397,8 +411,7 @@ impl pallet_bridge_grandpa::Config<WestendGrandpaInstance> for Runtime {
 	type MaxRequests = MaxRequests;
 	type HeadersToKeep = HeadersToKeep;
 
-	// TODO [#391]: Use weights generated for the Millau runtime instead of Rialto ones.
-	type WeightInfo = pallet_bridge_grandpa::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_grandpa::weights::MillauWeight<Runtime>;
 }
 
 impl pallet_shift_session_manager::Config for Runtime {}
@@ -406,9 +419,9 @@ impl pallet_shift_session_manager::Config for Runtime {}
 parameter_types! {
 	pub const MaxMessagesToPruneAtOnce: bp_messages::MessageNonce = 8;
 	pub const MaxUnrewardedRelayerEntriesAtInboundLane: bp_messages::MessageNonce =
-		bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE;
+		bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
 	pub const MaxUnconfirmedMessagesAtInboundLane: bp_messages::MessageNonce =
-		bp_millau::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE;
+		bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
 	// `IdentityFee` is used by Millau => we may use weight directly
 	pub const GetDeliveryConfirmationTransactionFee: Balance =
 		bp_millau::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT as _;
@@ -421,8 +434,7 @@ pub type WithRialtoMessagesInstance = ();
 
 impl pallet_bridge_messages::Config<WithRialtoMessagesInstance> for Runtime {
 	type Event = Event;
-	// TODO: https://github.com/paritytech/parity-bridges-common/issues/390
-	type WeightInfo = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_messages::weights::MillauWeight<Runtime>;
 	type Parameter = rialto_messages::MillauToRialtoMessagesParameter;
 	type MaxMessagesToPruneAtOnce = MaxMessagesToPruneAtOnce;
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
@@ -442,10 +454,9 @@ impl pallet_bridge_messages::Config<WithRialtoMessagesInstance> for Runtime {
 	type MessageDeliveryAndDispatchPayment =
 		pallet_bridge_messages::instant_payments::InstantCurrencyPayments<
 			Runtime,
-			(),
+			WithRialtoMessagesInstance,
 			pallet_balances::Pallet<Runtime>,
 			GetDeliveryConfirmationTransactionFee,
-			RootAccountForPayments,
 		>;
 	type OnMessageAccepted = ();
 	type OnDeliveryConfirmed =
@@ -511,7 +522,7 @@ construct_runtime!(
 		BridgeRialtoGrandpa: pallet_bridge_grandpa::{Pallet, Call, Storage},
 		BridgeDispatch: pallet_bridge_dispatch::{Pallet, Event<T>},
 		BridgeRialtoMessages: pallet_bridge_messages::{Pallet, Call, Storage, Event<T>, Config<T>},
-		BridgeRialtoTokenSwap: pallet_bridge_token_swap::{Pallet, Call, Storage, Event<T>},
+		BridgeRialtoTokenSwap: pallet_bridge_token_swap::{Pallet, Call, Storage, Event<T>, Origin<T>},
 
 		// Westend bridge modules.
 		BridgeWestendGrandpa: pallet_bridge_grandpa::<Instance1>::{Pallet, Call, Config<T>, Storage},
@@ -551,7 +562,7 @@ pub type Executive = frame_executive::Executive<
 	Block,
 	frame_system::ChainContext<Runtime>,
 	Runtime,
-	AllPallets,
+	AllPalletsWithSystem,
 >;
 
 #[cfg(feature = "runtime-benchmarks")]
@@ -664,7 +675,7 @@ impl_runtime_apis! {
 	}
 
 	impl beefy_primitives::BeefyApi<Block> for Runtime {
-		fn validator_set() -> ValidatorSet<BeefyId> {
+		fn validator_set() -> Option<ValidatorSet<BeefyId>> {
 			Beefy::validator_set()
 		}
 	}
@@ -742,10 +753,6 @@ impl_runtime_apis! {
 			let header = BridgeRialtoGrandpa::best_finalized();
 			(header.number, header.hash())
 		}
-
-		fn is_known_header(hash: bp_rialto::Hash) -> bool {
-			BridgeRialtoGrandpa::is_known_header(hash)
-		}
 	}
 
 	impl bp_westend::WestendFinalityApi<Block> for Runtime {
@@ -753,20 +760,18 @@ impl_runtime_apis! {
 			let header = BridgeWestendGrandpa::best_finalized();
 			(header.number, header.hash())
 		}
-
-		fn is_known_header(hash: bp_westend::Hash) -> bool {
-			BridgeWestendGrandpa::is_known_header(hash)
-		}
 	}
 
 	impl bp_rialto::ToRialtoOutboundLaneApi<Block, Balance, ToRialtoMessagePayload> for Runtime {
 		fn estimate_message_delivery_and_dispatch_fee(
 			_lane_id: bp_messages::LaneId,
 			payload: ToRialtoMessagePayload,
+			rialto_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<Balance> {
 			estimate_message_dispatch_and_delivery_fee::<WithRialtoMessageBridge>(
 				&payload,
 				WithRialtoMessageBridge::RELAYER_FEE_PERCENT,
+				rialto_to_this_conversion_rate,
 			).ok()
 		}
 
@@ -781,28 +786,6 @@ impl_runtime_apis! {
 				WithRialtoMessageBridge,
 			>(lane, begin, end)
 		}
-
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRialtoMessages::outbound_latest_received_nonce(lane)
-		}
-
-		fn latest_generated_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRialtoMessages::outbound_latest_generated_nonce(lane)
-		}
-	}
-
-	impl bp_rialto::FromRialtoInboundLaneApi<Block> for Runtime {
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRialtoMessages::inbound_latest_received_nonce(lane)
-		}
-
-		fn latest_confirmed_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRialtoMessages::inbound_latest_confirmed_nonce(lane)
-		}
-
-		fn unrewarded_relayers_state(lane: bp_messages::LaneId) -> bp_messages::UnrewardedRelayersState {
-			BridgeRialtoMessages::inbound_unrewarded_relayers_state(lane)
-		}
 	}
 
 	#[cfg(feature = "runtime-benchmarks")]
@@ -814,8 +797,13 @@ impl_runtime_apis! {
 			use frame_benchmarking::{Benchmarking, BenchmarkList};
 			use frame_support::traits::StorageInfoTrait;
 
+			use pallet_bridge_messages::benchmarking::Pallet as MessagesBench;
+
 			let mut list = Vec::<BenchmarkList>::new();
-			list_benchmarks!(list, extra);
+
+			list_benchmark!(list, extra, pallet_bridge_token_swap, BridgeRialtoTokenSwap);
+			list_benchmark!(list, extra, pallet_bridge_messages, MessagesBench::<Runtime, WithRialtoMessagesInstance>);
+			list_benchmark!(list, extra, pallet_bridge_grandpa, BridgeRialtoGrandpa);
 
 			let storage_info = AllPalletsWithSystem::storage_info();
 			return (list, storage_info)
@@ -842,6 +830,74 @@ impl_runtime_apis! {
 			let mut batches = Vec::<BenchmarkBatch>::new();
 			let params = (&config, &whitelist);
 
+			use bridge_runtime_common::messages_benchmarking::{prepare_message_delivery_proof, prepare_message_proof, prepare_outbound_message};
+			use bridge_runtime_common::messages;
+			use pallet_bridge_messages::benchmarking::{
+				Pallet as MessagesBench,
+				Config as MessagesConfig,
+				MessageDeliveryProofParams,
+				MessageParams,
+				MessageProofParams,
+			};
+			use rialto_messages::WithRialtoMessageBridge;
+
+			impl MessagesConfig<WithRialtoMessagesInstance> for Runtime {
+				fn maximal_message_size() -> u32 {
+					messages::source::maximal_message_size::<WithRialtoMessageBridge>()
+				}
+
+				fn bridged_relayer_id() -> Self::InboundRelayer {
+					[0u8; 32].into()
+				}
+
+				fn account_balance(account: &Self::AccountId) -> Self::OutboundMessageFee {
+					pallet_balances::Pallet::<Runtime>::free_balance(account)
+				}
+
+				fn endow_account(account: &Self::AccountId) {
+					pallet_balances::Pallet::<Runtime>::make_free_balance_be(
+						account,
+						Balance::MAX / 100,
+					);
+				}
+
+				fn prepare_outbound_message(
+					params: MessageParams<Self::AccountId>,
+				) -> (rialto_messages::ToRialtoMessagePayload, Balance) {
+					(prepare_outbound_message::<WithRialtoMessageBridge>(params), Self::message_fee())
+				}
+
+				fn prepare_message_proof(
+					params: MessageProofParams,
+				) -> (rialto_messages::FromRialtoMessagesProof, Weight) {
+					prepare_message_proof::<Runtime, (), (), WithRialtoMessageBridge, bp_rialto::Header, bp_rialto::Hasher>(
+						params,
+						&VERSION,
+						Balance::MAX / 100,
+					)
+				}
+
+				fn prepare_message_delivery_proof(
+					params: MessageDeliveryProofParams<Self::AccountId>,
+				) -> rialto_messages::ToRialtoMessagesDeliveryProof {
+					prepare_message_delivery_proof::<Runtime, (), WithRialtoMessageBridge, bp_rialto::Header, bp_rialto::Hasher>(
+						params,
+					)
+				}
+
+				fn is_message_dispatched(nonce: bp_messages::MessageNonce) -> bool {
+					frame_system::Pallet::<Runtime>::events()
+						.into_iter()
+						.map(|event_record| event_record.event)
+						.any(|event| matches!(
+							event,
+							Event::BridgeDispatch(pallet_bridge_dispatch::Event::<Runtime, _>::MessageDispatched(
+								_, ([0, 0, 0, 0], nonce_from_event), _,
+							)) if nonce_from_event == nonce
+						))
+				}
+			}
+
 			use pallet_bridge_token_swap::benchmarking::Config as TokenSwapConfig;
 
 			impl TokenSwapConfig<WithRialtoTokenSwapInstance> for Runtime {
@@ -857,9 +913,15 @@ impl_runtime_apis! {
 				}
 			}
 
-			add_benchmarks!(params, batches);
+			add_benchmark!(
+				params,
+				batches,
+				pallet_bridge_messages,
+				MessagesBench::<Runtime, WithRialtoMessagesInstance>
+			);
+			add_benchmark!(params, batches, pallet_bridge_grandpa, BridgeRialtoGrandpa);
+			add_benchmark!(params, batches, pallet_bridge_token_swap, BridgeRialtoTokenSwap);
 
-			if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) }
 			Ok(batches)
 		}
 	}
@@ -892,52 +954,18 @@ where
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use bridge_runtime_common::messages;
 
 	#[test]
-	fn ensure_millau_message_lane_weights_are_correct() {
-		// TODO: https://github.com/paritytech/parity-bridges-common/issues/390
-		type Weights = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
-
-		pallet_bridge_messages::ensure_weights_are_correct::<Weights>(
-			bp_millau::DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT,
-			bp_millau::ADDITIONAL_MESSAGE_BYTE_DELIVERY_WEIGHT,
-			bp_millau::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
-			bp_millau::PAY_INBOUND_DISPATCH_FEE_WEIGHT,
-			DbWeight::get(),
-		);
-
-		let max_incoming_message_proof_size = bp_rialto::EXTRA_STORAGE_PROOF_SIZE.saturating_add(
-			messages::target::maximal_incoming_message_size(bp_millau::max_extrinsic_size()),
-		);
-		pallet_bridge_messages::ensure_able_to_receive_message::<Weights>(
-			bp_millau::max_extrinsic_size(),
-			bp_millau::max_extrinsic_weight(),
-			max_incoming_message_proof_size,
-			messages::target::maximal_incoming_message_dispatch_weight(
-				bp_millau::max_extrinsic_weight(),
-			),
+	fn call_size() {
+		const BRIDGES_PALLETS_MAX_CALL_SIZE: usize = 200;
+		assert!(
+			core::mem::size_of::<pallet_bridge_grandpa::Call<Runtime>>() <=
+				BRIDGES_PALLETS_MAX_CALL_SIZE
 		);
-
-		let max_incoming_inbound_lane_data_proof_size =
-			bp_messages::InboundLaneData::<()>::encoded_size_hint(
-				bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-				bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE as _,
-				bp_rialto::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE as _,
-			)
-			.unwrap_or(u32::MAX);
-		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
-			bp_millau::max_extrinsic_size(),
-			bp_millau::max_extrinsic_weight(),
-			max_incoming_inbound_lane_data_proof_size,
-			bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-			bp_rialto::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-			DbWeight::get(),
+		assert!(
+			core::mem::size_of::<pallet_bridge_messages::Call<Runtime>>() <=
+				BRIDGES_PALLETS_MAX_CALL_SIZE
 		);
-	}
-
-	#[test]
-	fn call_size() {
 		const MAX_CALL_SIZE: usize = 230; // value from polkadot-runtime tests
 		assert!(core::mem::size_of::<Call>() <= MAX_CALL_SIZE);
 	}
diff --git a/polkadot/bridges/bin/millau/runtime/src/rialto_messages.rs b/polkadot/bridges/bin/millau/runtime/src/rialto_messages.rs
index 6d9677c45cf91be170b362c2a3c24807abd7029a..d925d805dd04b4444861fbebc534ba6d7b57ca8a 100644
--- a/polkadot/bridges/bin/millau/runtime/src/rialto_messages.rs
+++ b/polkadot/bridges/bin/millau/runtime/src/rialto_messages.rs
@@ -19,11 +19,11 @@
 use crate::Runtime;
 
 use bp_messages::{
-	source_chain::TargetHeaderChain,
+	source_chain::{SenderOrigin, TargetHeaderChain},
 	target_chain::{ProvedMessages, SourceHeaderChain},
 	InboundLaneData, LaneId, Message, MessageNonce, Parameter as MessagesParameter,
 };
-use bp_runtime::{ChainId, MILLAU_CHAIN_ID, RIALTO_CHAIN_ID};
+use bp_runtime::{Chain, ChainId, MILLAU_CHAIN_ID, RIALTO_CHAIN_ID};
 use bridge_runtime_common::messages::{self, MessageBridge, MessageTransaction};
 use codec::{Decode, Encode};
 use frame_support::{
@@ -64,10 +64,10 @@ pub type FromRialtoMessagePayload =
 pub type FromRialtoEncodedCall = messages::target::FromBridgedChainEncodedMessageCall<crate::Call>;
 
 /// Messages proof for Rialto -> Millau messages.
-type FromRialtoMessagesProof = messages::target::FromBridgedChainMessagesProof<bp_rialto::Hash>;
+pub type FromRialtoMessagesProof = messages::target::FromBridgedChainMessagesProof<bp_rialto::Hash>;
 
 /// Messages delivery proof for Millau -> Rialto messages.
-type ToRialtoMessagesDeliveryProof =
+pub type ToRialtoMessagesDeliveryProof =
 	messages::source::FromBridgedChainMessagesDeliveryProof<bp_rialto::Hash>;
 
 /// Call-dispatch based message dispatch for Rialto -> Millau messages.
@@ -86,16 +86,19 @@ impl MessageBridge for WithRialtoMessageBridge {
 	const RELAYER_FEE_PERCENT: u32 = 10;
 	const THIS_CHAIN_ID: ChainId = MILLAU_CHAIN_ID;
 	const BRIDGED_CHAIN_ID: ChainId = RIALTO_CHAIN_ID;
-	const BRIDGED_MESSAGES_PALLET_NAME: &'static str = bp_rialto::WITH_MILLAU_MESSAGES_PALLET_NAME;
+	const BRIDGED_MESSAGES_PALLET_NAME: &'static str = bp_millau::WITH_MILLAU_MESSAGES_PALLET_NAME;
 
 	type ThisChain = Millau;
 	type BridgedChain = Rialto;
 
-	fn bridged_balance_to_this_balance(bridged_balance: bp_rialto::Balance) -> bp_millau::Balance {
-		bp_millau::Balance::try_from(
-			RialtoToMillauConversionRate::get().saturating_mul_int(bridged_balance),
-		)
-		.unwrap_or(bp_millau::Balance::MAX)
+	fn bridged_balance_to_this_balance(
+		bridged_balance: bp_rialto::Balance,
+		bridged_to_this_conversion_rate_override: Option<FixedU128>,
+	) -> bp_millau::Balance {
+		let conversion_rate = bridged_to_this_conversion_rate_override
+			.unwrap_or_else(|| RialtoToMillauConversionRate::get());
+		bp_millau::Balance::try_from(conversion_rate.saturating_mul_int(bridged_balance))
+			.unwrap_or(bp_millau::Balance::MAX)
 	}
 }
 
@@ -113,12 +116,23 @@ impl messages::ChainWithMessages for Millau {
 }
 
 impl messages::ThisChainWithMessages for Millau {
+	type Origin = crate::Origin;
 	type Call = crate::Call;
 
-	fn is_outbound_lane_enabled(lane: &LaneId) -> bool {
-		*lane == [0, 0, 0, 0] ||
-			*lane == [0, 0, 0, 1] ||
-			*lane == crate::TokenSwapMessagesLane::get()
+	fn is_message_accepted(send_origin: &Self::Origin, lane: &LaneId) -> bool {
+		// lanes 0x00000000 && 0x00000001 are accepting any paid messages, while
+		// `TokenSwapMessageLane` only accepts messages from token swap pallet
+		let token_swap_dedicated_lane = crate::TokenSwapMessagesLane::get();
+		match *lane {
+			[0, 0, 0, 0] | [0, 0, 0, 1] => send_origin.linked_account().is_some(),
+			_ if *lane == token_swap_dedicated_lane => matches!(
+				send_origin.caller,
+				crate::OriginCaller::BridgeRialtoTokenSwap(
+					pallet_bridge_token_swap::RawOrigin::TokenSwap { .. }
+				)
+			),
+			_ => false,
+		}
 	}
 
 	fn maximal_pending_messages_at_outbound_lane() -> MessageNonce {
@@ -172,13 +186,13 @@ impl messages::ChainWithMessages for Rialto {
 
 impl messages::BridgedChainWithMessages for Rialto {
 	fn maximal_extrinsic_size() -> u32 {
-		bp_rialto::max_extrinsic_size()
+		bp_rialto::Rialto::max_extrinsic_size()
 	}
 
 	fn message_weight_limits(_message_payload: &[u8]) -> RangeInclusive<Weight> {
 		// we don't want to relay too large messages + keep reserve for future upgrades
 		let upper_limit = messages::target::maximal_incoming_message_dispatch_weight(
-			bp_rialto::max_extrinsic_weight(),
+			bp_rialto::Rialto::max_extrinsic_weight(),
 		);
 
 		// we're charging for payload bytes in `WithRialtoMessageBridge::transaction_payment`
@@ -274,6 +288,25 @@ impl SourceHeaderChain<bp_rialto::Balance> for Rialto {
 	}
 }
 
+impl SenderOrigin<crate::AccountId> for crate::Origin {
+	fn linked_account(&self) -> Option<crate::AccountId> {
+		match self.caller {
+			crate::OriginCaller::system(frame_system::RawOrigin::Signed(ref submitter)) =>
+				Some(submitter.clone()),
+			crate::OriginCaller::system(frame_system::RawOrigin::Root) |
+			crate::OriginCaller::system(frame_system::RawOrigin::None) =>
+				crate::RootAccountForPayments::get(),
+			crate::OriginCaller::BridgeRialtoTokenSwap(
+				pallet_bridge_token_swap::RawOrigin::TokenSwap {
+					ref swap_account_at_this_chain,
+					..
+				},
+			) => Some(swap_account_at_this_chain.clone()),
+			_ => None,
+		}
+	}
+}
+
 /// Millau -> Rialto message lane pallet parameters.
 #[derive(RuntimeDebug, Clone, Encode, Decode, PartialEq, Eq, TypeInfo)]
 pub enum MillauToRialtoMessagesParameter {
@@ -289,3 +322,107 @@ impl MessagesParameter for MillauToRialtoMessagesParameter {
 		}
 	}
 }
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::{DbWeight, RialtoGrandpaInstance, Runtime, WithRialtoMessagesInstance};
+
+	use bp_runtime::Chain;
+	use bridge_runtime_common::{
+		assert_complete_bridge_types,
+		integrity::{
+			assert_complete_bridge_constants, AssertBridgeMessagesPalletConstants,
+			AssertBridgePalletNames, AssertChainConstants, AssertCompleteBridgeConstants,
+		},
+		messages,
+	};
+
+	#[test]
+	fn ensure_millau_message_lane_weights_are_correct() {
+		type Weights = pallet_bridge_messages::weights::MillauWeight<Runtime>;
+
+		pallet_bridge_messages::ensure_weights_are_correct::<Weights>(
+			bp_millau::DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT,
+			bp_millau::ADDITIONAL_MESSAGE_BYTE_DELIVERY_WEIGHT,
+			bp_millau::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
+			bp_millau::PAY_INBOUND_DISPATCH_FEE_WEIGHT,
+			DbWeight::get(),
+		);
+
+		let max_incoming_message_proof_size = bp_rialto::EXTRA_STORAGE_PROOF_SIZE.saturating_add(
+			messages::target::maximal_incoming_message_size(bp_millau::Millau::max_extrinsic_size()),
+		);
+		pallet_bridge_messages::ensure_able_to_receive_message::<Weights>(
+			bp_millau::Millau::max_extrinsic_size(),
+			bp_millau::Millau::max_extrinsic_weight(),
+			max_incoming_message_proof_size,
+			messages::target::maximal_incoming_message_dispatch_weight(
+				bp_millau::Millau::max_extrinsic_weight(),
+			),
+		);
+
+		let max_incoming_inbound_lane_data_proof_size =
+			bp_messages::InboundLaneData::<()>::encoded_size_hint(
+				bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
+				bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as _,
+				bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as _,
+			)
+			.unwrap_or(u32::MAX);
+		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
+			bp_millau::Millau::max_extrinsic_size(),
+			bp_millau::Millau::max_extrinsic_weight(),
+			max_incoming_inbound_lane_data_proof_size,
+			bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+			bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
+			DbWeight::get(),
+		);
+	}
+
+	#[test]
+	fn ensure_bridge_integrity() {
+		assert_complete_bridge_types!(
+			runtime: Runtime,
+			with_bridged_chain_grandpa_instance: RialtoGrandpaInstance,
+			with_bridged_chain_messages_instance: WithRialtoMessagesInstance,
+			bridge: WithRialtoMessageBridge,
+			this_chain: bp_millau::Millau,
+			bridged_chain: bp_rialto::Rialto,
+			this_chain_account_id_converter: bp_millau::AccountIdConverter
+		);
+
+		assert_complete_bridge_constants::<
+			Runtime,
+			RialtoGrandpaInstance,
+			WithRialtoMessagesInstance,
+			WithRialtoMessageBridge,
+			bp_millau::Millau,
+		>(AssertCompleteBridgeConstants {
+			this_chain_constants: AssertChainConstants {
+				block_length: bp_millau::BlockLength::get(),
+				block_weights: bp_millau::BlockWeights::get(),
+			},
+			messages_pallet_constants: AssertBridgeMessagesPalletConstants {
+				max_unrewarded_relayers_in_bridged_confirmation_tx:
+					bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+				max_unconfirmed_messages_in_bridged_confirmation_tx:
+					bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
+				bridged_chain_id: bp_runtime::RIALTO_CHAIN_ID,
+			},
+			pallet_names: AssertBridgePalletNames {
+				with_this_chain_messages_pallet_name: bp_millau::WITH_MILLAU_MESSAGES_PALLET_NAME,
+				with_bridged_chain_grandpa_pallet_name: bp_rialto::WITH_RIALTO_GRANDPA_PALLET_NAME,
+				with_bridged_chain_messages_pallet_name:
+					bp_rialto::WITH_RIALTO_MESSAGES_PALLET_NAME,
+			},
+		});
+
+		assert_eq!(
+			RialtoToMillauConversionRate::key().to_vec(),
+			bp_runtime::storage_parameter_key(
+				bp_millau::RIALTO_TO_MILLAU_CONVERSION_RATE_PARAMETER_NAME
+			)
+			.0,
+		);
+	}
+}
diff --git a/polkadot/bridges/bin/rialto-parachain/node/Cargo.toml b/polkadot/bridges/bin/rialto-parachain/node/Cargo.toml
index 8adc998e47ee38b1eeebb0e8b280cd01ddf3d056..41021a35ed2b002e8baaa0b2e399d3fce3a6bed8 100644
--- a/polkadot/bridges/bin/rialto-parachain/node/Cargo.toml
+++ b/polkadot/bridges/bin/rialto-parachain/node/Cargo.toml
@@ -2,7 +2,7 @@
 name = "rialto-parachain-collator"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
@@ -18,10 +18,10 @@ default = []
 runtime-benchmarks = ['rialto-parachain-runtime/runtime-benchmarks']
 
 [dependencies]
+clap = { version = "3.1", features = ["derive"] }
 derive_more = '0.99.2'
 log = '0.4.14'
-codec = { package = 'parity-scale-codec', version = '2.0.0' }
-structopt = '0.3.8'
+codec = { package = 'parity-scale-codec', version = '3.0.0' }
 serde = { version = '1.0', features = ['derive'] }
 hex-literal = '0.3.1'
 
@@ -80,6 +80,8 @@ cumulus-client-network = { git = "https://github.com/paritytech/cumulus", branch
 cumulus-client-service = { git = "https://github.com/paritytech/cumulus", branch = "master" }
 cumulus-primitives-core = { git = "https://github.com/paritytech/cumulus", branch = "master" }
 cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/cumulus", branch = "master" }
+cumulus-relay-chain-interface = { git = "https://github.com/paritytech/cumulus", branch = "master" }
+cumulus-relay-chain-inprocess-interface = { git = "https://github.com/paritytech/cumulus", branch = "master" }
 
 # Polkadot dependencies
 polkadot-cli = { git = "https://github.com/paritytech/polkadot", branch = "master" }
diff --git a/polkadot/bridges/bin/rialto-parachain/node/src/chain_spec.rs b/polkadot/bridges/bin/rialto-parachain/node/src/chain_spec.rs
index 9ccad8c62f48bd36773cb40591b414e48db0eccf..6a8e751677df7dcdab267d22f0997c86b9aab675 100644
--- a/polkadot/bridges/bin/rialto-parachain/node/src/chain_spec.rs
+++ b/polkadot/bridges/bin/rialto-parachain/node/src/chain_spec.rs
@@ -89,6 +89,7 @@ pub fn development_config(id: ParaId) -> ChainSpec {
 		None,
 		None,
 		None,
+		None,
 		Extensions {
 			relay_chain: "rococo-local".into(), // You MUST set this to the correct network!
 			para_id: id.into(),
@@ -133,6 +134,7 @@ pub fn local_testnet_config(id: ParaId) -> ChainSpec {
 		None,
 		None,
 		None,
+		None,
 		Extensions {
 			relay_chain: "rococo-local".into(), // You MUST set this to the correct network!
 			para_id: id.into(),
diff --git a/polkadot/bridges/bin/rialto-parachain/node/src/cli.rs b/polkadot/bridges/bin/rialto-parachain/node/src/cli.rs
index 78c05f90c88001c2f5d413a6481beee4a8cb0185..89d049f022e3e7185b1baff8653cd44e01153ebe 100644
--- a/polkadot/bridges/bin/rialto-parachain/node/src/cli.rs
+++ b/polkadot/bridges/bin/rialto-parachain/node/src/cli.rs
@@ -15,18 +15,18 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use crate::chain_spec;
+use clap::Parser;
 use std::path::PathBuf;
-use structopt::StructOpt;
 
 /// Sub-commands supported by the collator.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub enum Subcommand {
 	/// Export the genesis state of the parachain.
-	#[structopt(name = "export-genesis-state")]
+	#[clap(name = "export-genesis-state")]
 	ExportGenesisState(ExportGenesisStateCommand),
 
 	/// Export the genesis wasm of the parachain.
-	#[structopt(name = "export-genesis-wasm")]
+	#[clap(name = "export-genesis-wasm")]
 	ExportGenesisWasm(ExportGenesisWasmCommand),
 
 	/// Build a chain specification.
@@ -51,66 +51,66 @@ pub enum Subcommand {
 	Revert(sc_cli::RevertCmd),
 
 	/// The custom benchmark subcommmand benchmarking runtime pallets.
-	#[structopt(name = "benchmark", about = "Benchmark runtime pallets.")]
+	#[clap(name = "benchmark", about = "Benchmark runtime pallets.")]
 	Benchmark(frame_benchmarking_cli::BenchmarkCmd),
 }
 
 /// Command for exporting the genesis state of the parachain
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct ExportGenesisStateCommand {
 	/// Output file name or stdout if unspecified.
-	#[structopt(parse(from_os_str))]
+	#[clap(parse(from_os_str))]
 	pub output: Option<PathBuf>,
 
 	/// Id of the parachain this state is for.
 	///
 	/// Default: 100
-	#[structopt(long, conflicts_with = "chain")]
+	#[clap(long, conflicts_with = "chain")]
 	pub parachain_id: Option<u32>,
 
 	/// Write output in binary. Default is to write in hex.
-	#[structopt(short, long)]
+	#[clap(short, long)]
 	pub raw: bool,
 
 	/// The name of the chain for that the genesis state should be exported.
-	#[structopt(long, conflicts_with = "parachain-id")]
+	#[clap(long, conflicts_with = "parachain-id")]
 	pub chain: Option<String>,
 }
 
 /// Command for exporting the genesis wasm file.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct ExportGenesisWasmCommand {
 	/// Output file name or stdout if unspecified.
-	#[structopt(parse(from_os_str))]
+	#[clap(parse(from_os_str))]
 	pub output: Option<PathBuf>,
 
 	/// Write output in binary. Default is to write in hex.
-	#[structopt(short, long)]
+	#[clap(short, long)]
 	pub raw: bool,
 
 	/// The name of the chain for that the genesis wasm file should be exported.
-	#[structopt(long)]
+	#[clap(long)]
 	pub chain: Option<String>,
 }
 
-#[derive(Debug, StructOpt)]
-#[structopt(settings = &[
-	structopt::clap::AppSettings::GlobalVersion,
-	structopt::clap::AppSettings::ArgsNegateSubcommands,
-	structopt::clap::AppSettings::SubcommandsNegateReqs,
-])]
+#[derive(Debug, Parser)]
+#[clap(
+	propagate_version = true,
+	args_conflicts_with_subcommands = true,
+	subcommand_negates_reqs = true
+)]
 pub struct Cli {
-	#[structopt(subcommand)]
+	#[clap(subcommand)]
 	pub subcommand: Option<Subcommand>,
 
-	#[structopt(long)]
+	#[clap(long)]
 	pub parachain_id: Option<u32>,
 
-	#[structopt(flatten)]
+	#[clap(flatten)]
 	pub run: cumulus_client_cli::RunCmd,
 
 	/// Relaychain arguments
-	#[structopt(raw = true)]
+	#[clap(raw = true)]
 	pub relaychain_args: Vec<String>,
 }
 
@@ -135,6 +135,6 @@ impl RelayChainCli {
 		let extension = chain_spec::Extensions::try_get(&*para_config.chain_spec);
 		let chain_id = extension.map(|e| e.relay_chain.clone());
 		let base_path = para_config.base_path.as_ref().map(|x| x.path().join("rialto-bridge-node"));
-		Self { base_path, chain_id, base: polkadot_cli::RunCmd::from_iter(relay_chain_args) }
+		Self { base_path, chain_id, base: polkadot_cli::RunCmd::parse_from(relay_chain_args) }
 	}
 }
diff --git a/polkadot/bridges/bin/rialto-parachain/node/src/command.rs b/polkadot/bridges/bin/rialto-parachain/node/src/command.rs
index e4f52cc026a7e7c9ebbf33276f71719a9500561e..c47e742675da169002019f9aacd3fcce811ffb81 100644
--- a/polkadot/bridges/bin/rialto-parachain/node/src/command.rs
+++ b/polkadot/bridges/bin/rialto-parachain/node/src/command.rs
@@ -211,10 +211,12 @@ pub fn run() -> Result<()> {
 			builder.with_profiling(sc_tracing::TracingReceiver::Log, "");
 			let _ = builder.init();
 
-			let block: Block = generate_genesis_block(&load_spec(
+			let spec = load_spec(
 				&params.chain.clone().unwrap_or_default(),
 				params.parachain_id.expect("Missing ParaId").into(),
-			)?)?;
+			)?;
+			let state_version = Cli::native_runtime_version(&spec).state_version();
+			let block: Block = generate_genesis_block(&spec, state_version)?;
 			let raw_header = block.header().encode();
 			let output_buf = if params.raw {
 				raw_header
@@ -263,6 +265,7 @@ pub fn run() -> Result<()> {
 			},
 		None => {
 			let runner = cli.create_runner(&cli.run.normalize())?;
+			let collator_options = cli.run.collator_options();
 
 			runner.run_node_until_exit(|config| async move {
 				let para_id =
@@ -276,10 +279,12 @@ pub fn run() -> Result<()> {
 				let id = ParaId::from(cli.parachain_id.or(para_id).expect("Missing ParaId"));
 
 				let parachain_account =
-					AccountIdConversion::<polkadot_primitives::v0::AccountId>::into_account(&id);
+					AccountIdConversion::<polkadot_primitives::v2::AccountId>::into_account(&id);
 
-				let block: Block =
-					generate_genesis_block(&config.chain_spec).map_err(|e| format!("{:?}", e))?;
+				let state_version =
+					RelayChainCli::native_runtime_version(&config.chain_spec).state_version();
+				let block: Block = generate_genesis_block(&config.chain_spec, state_version)
+					.map_err(|e| format!("{:?}", e))?;
 				let genesis_state = format!("0x{:?}", HexDisplay::from(&block.header().encode()));
 
 				let polkadot_config = SubstrateCli::create_configuration(
@@ -294,7 +299,7 @@ pub fn run() -> Result<()> {
 				info!("Parachain genesis state: {}", genesis_state);
 				info!("Is collating: {}", if config.role.is_authority() { "yes" } else { "no" });
 
-				crate::service::start_node(config, polkadot_config, id)
+				crate::service::start_node(config, polkadot_config, collator_options, id)
 					.await
 					.map(|r| r.0)
 					.map_err(Into::into)
@@ -357,11 +362,24 @@ impl CliConfiguration<Self> for RelayChainCli {
 		self.base.base.rpc_ws(default_listen_port)
 	}
 
-	fn prometheus_config(&self, default_listen_port: u16) -> Result<Option<PrometheusConfig>> {
-		self.base.base.prometheus_config(default_listen_port)
+	fn prometheus_config(
+		&self,
+		default_listen_port: u16,
+		chain_spec: &Box<dyn ChainSpec>,
+	) -> Result<Option<PrometheusConfig>> {
+		self.base.base.prometheus_config(default_listen_port, chain_spec)
 	}
 
-	fn init<C: SubstrateCli>(&self) -> Result<()> {
+	fn init<F>(
+		&self,
+		_support_url: &String,
+		_impl_version: &String,
+		_logger_hook: F,
+		_config: &sc_service::Configuration,
+	) -> Result<()>
+	where
+		F: FnOnce(&mut sc_cli::LoggerBuilder, &sc_service::Configuration),
+	{
 		unreachable!("PolkadotCli is never initialized; qed");
 	}
 
diff --git a/polkadot/bridges/bin/rialto-parachain/node/src/service.rs b/polkadot/bridges/bin/rialto-parachain/node/src/service.rs
index 5903b4a881ea44fb3347c20f80a1be81f7d34b80..a2299e17457d9266f88fecb94f00ee3424c9af55 100644
--- a/polkadot/bridges/bin/rialto-parachain/node/src/service.rs
+++ b/polkadot/bridges/bin/rialto-parachain/node/src/service.rs
@@ -14,22 +14,30 @@
 // 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/>.
 
+//! Rialto parachain node service.
+//!
+//! The code is mostly copy of `polkadot-parachains/src/service.rs` file from Cumulus
+//! repository with some parts removed. We have added two RPC extensions to the original
+//! service: `pallet_transaction_payment_rpc::TransactionPaymentApi` and
+//! `substrate_frame_rpc_system::SystemApi`.
+
 // std
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 // Local Runtime Types
 use rialto_parachain_runtime::RuntimeApi;
 
 // Cumulus Imports
-use cumulus_client_consensus_aura::{
-	build_aura_consensus, BuildAuraConsensusParams, SlotProportion,
-};
+use cumulus_client_cli::CollatorOptions;
+use cumulus_client_consensus_aura::{AuraConsensus, BuildAuraConsensusParams, SlotProportion};
 use cumulus_client_consensus_common::ParachainConsensus;
-use cumulus_client_network::build_block_announce_validator;
+use cumulus_client_network::BlockAnnounceValidator;
 use cumulus_client_service::{
 	prepare_node_config, start_collator, start_full_node, StartCollatorParams, StartFullNodeParams,
 };
 use cumulus_primitives_core::ParaId;
+use cumulus_relay_chain_inprocess_interface::build_inprocess_relay_chain;
+use cumulus_relay_chain_interface::{RelayChainError, RelayChainInterface};
 
 // Substrate Imports
 use sc_client_api::ExecutorProvider;
@@ -38,7 +46,6 @@ use sc_network::NetworkService;
 use sc_service::{Configuration, PartialComponents, Role, TFullBackend, TFullClient, TaskManager};
 use sc_telemetry::{Telemetry, TelemetryHandle, TelemetryWorker, TelemetryWorkerHandle};
 use sp_api::ConstructRuntimeApi;
-use sp_consensus::SlotData;
 use sp_keystore::SyncCryptoStorePtr;
 use sp_runtime::traits::BlakeTwo256;
 use substrate_prometheus_endpoint::Registry;
@@ -188,6 +195,7 @@ where
 async fn start_node_impl<RuntimeApi, Executor, RB, BIQ, BIC>(
 	parachain_config: Configuration,
 	polkadot_config: Configuration,
+	collator_options: CollatorOptions,
 	id: ParaId,
 	rpc_ext_builder: RB,
 	build_import_queue: BIQ,
@@ -213,7 +221,14 @@ where
 	sc_client_api::StateBackendFor<TFullBackend<Block>, Block>: sp_api::StateBackend<BlakeTwo256>,
 	Executor: NativeExecutionDispatch + 'static,
 	RB: Fn(
+			sc_rpc_api::DenyUnsafe,
 			Arc<TFullClient<Block, RuntimeApi, NativeElseWasmExecutor<Executor>>>,
+			Arc<
+				sc_transaction_pool::FullPool<
+					Block,
+					TFullClient<Block, RuntimeApi, NativeElseWasmExecutor<Executor>>,
+				>,
+			>,
 		) -> jsonrpc_core::IoHandler<sc_rpc::Metadata>
 		+ Send
 		+ 'static,
@@ -234,7 +249,7 @@ where
 		Option<&Registry>,
 		Option<TelemetryHandle>,
 		&TaskManager,
-		&polkadot_service::NewFull<polkadot_service::Client>,
+		Arc<dyn RelayChainInterface>,
 		Arc<
 			sc_transaction_pool::FullPool<
 				Block,
@@ -255,27 +270,26 @@ where
 	let params = new_partial::<RuntimeApi, Executor, BIQ>(&parachain_config, build_import_queue)?;
 	let (mut telemetry, telemetry_worker_handle) = params.other;
 
-	let relay_chain_full_node =
-		cumulus_client_service::build_polkadot_full_node(polkadot_config, telemetry_worker_handle)
-			.map_err(|e| match e {
-				polkadot_service::Error::Sub(x) => x,
-				s => format!("{}", s).into(),
-			})?;
+	let mut task_manager = params.task_manager;
+	let (relay_chain_interface, collator_key) = build_inprocess_relay_chain(
+		polkadot_config,
+		&parachain_config,
+		telemetry_worker_handle,
+		&mut task_manager,
+	)
+	.map_err(|e| match e {
+		RelayChainError::ServiceError(polkadot_service::Error::Sub(x)) => x,
+		s => s.to_string().into(),
+	})?;
 
 	let client = params.client.clone();
 	let backend = params.backend.clone();
-	let block_announce_validator = build_block_announce_validator(
-		relay_chain_full_node.client.clone(),
-		id,
-		Box::new(relay_chain_full_node.network.clone()),
-		relay_chain_full_node.backend.clone(),
-	);
+	let block_announce_validator = BlockAnnounceValidator::new(relay_chain_interface.clone(), id);
 
 	let force_authoring = parachain_config.force_authoring;
 	let validator = parachain_config.role.is_authority();
 	let prometheus_registry = parachain_config.prometheus_registry().cloned();
 	let transaction_pool = params.transaction_pool.clone();
-	let mut task_manager = params.task_manager;
 	let import_queue = cumulus_client_service::SharedImportQueue::new(params.import_queue);
 	let (network, system_rpc_tx, start_network) =
 		sc_service::build_network(sc_service::BuildNetworkParams {
@@ -284,12 +298,17 @@ where
 			transaction_pool: transaction_pool.clone(),
 			spawn_handle: task_manager.spawn_handle(),
 			import_queue: import_queue.clone(),
-			block_announce_validator_builder: Some(Box::new(|_| block_announce_validator)),
+			block_announce_validator_builder: Some(Box::new(|_| {
+				Box::new(block_announce_validator)
+			})),
 			warp_sync: None,
 		})?;
 
 	let rpc_client = client.clone();
-	let rpc_extensions_builder = Box::new(move |_, _| Ok(rpc_ext_builder(rpc_client.clone())));
+	let rpc_transaction_pool = transaction_pool.clone();
+	let rpc_extensions_builder = Box::new(move |deny_unsafe, _| {
+		Ok(rpc_ext_builder(deny_unsafe, rpc_client.clone(), rpc_transaction_pool.clone()))
+	});
 
 	sc_service::spawn_tasks(sc_service::SpawnTasksParams {
 		rpc_extensions_builder,
@@ -309,13 +328,15 @@ where
 		Arc::new(move |hash, data| network.announce_block(hash, data))
 	};
 
+	let relay_chain_slot_duration = Duration::from_secs(6);
+
 	if validator {
 		let parachain_consensus = build_consensus(
 			client.clone(),
 			prometheus_registry.as_ref(),
 			telemetry.as_ref().map(|t| t.handle()),
 			&task_manager,
-			&relay_chain_full_node,
+			relay_chain_interface.clone(),
 			transaction_pool,
 			network,
 			params.keystore_container.sync_keystore(),
@@ -330,10 +351,12 @@ where
 			announce_block,
 			client: client.clone(),
 			task_manager: &mut task_manager,
-			relay_chain_full_node,
+			relay_chain_interface,
 			spawner,
 			parachain_consensus,
 			import_queue,
+			collator_key: collator_key.expect("Command line arguments do not allow this. qed"),
+			relay_chain_slot_duration,
 		};
 
 		start_collator(params).await?;
@@ -343,7 +366,10 @@ where
 			announce_block,
 			task_manager: &mut task_manager,
 			para_id: id,
-			relay_chain_full_node,
+			relay_chain_interface,
+			relay_chain_slot_duration,
+			import_queue,
+			collator_options,
 		};
 
 		start_full_node(params)?;
@@ -385,9 +411,9 @@ pub fn parachain_build_import_queue(
 			let time = sp_timestamp::InherentDataProvider::from_system_time();
 
 			let slot =
-				sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_duration(
+				sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
 					*time,
-					slot_duration.slot_duration(),
+					slot_duration,
 				);
 
 			Ok((time, slot))
@@ -404,6 +430,7 @@ pub fn parachain_build_import_queue(
 pub async fn start_node(
 	parachain_config: Configuration,
 	polkadot_config: Configuration,
+	collator_options: CollatorOptions,
 	id: ParaId,
 ) -> sc_service::error::Result<(
 	TaskManager,
@@ -412,14 +439,27 @@ pub async fn start_node(
 	start_node_impl::<RuntimeApi, ParachainRuntimeExecutor, _, _, _>(
 		parachain_config,
 		polkadot_config,
+		collator_options,
 		id,
-		|_| Default::default(),
+		|deny_unsafe, client, pool| {
+			use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApi};
+			use substrate_frame_rpc_system::{FullSystem, SystemApi};
+
+			let mut io = jsonrpc_core::IoHandler::default();
+			io.extend_with(SystemApi::to_delegate(FullSystem::new(
+				client.clone(),
+				pool,
+				deny_unsafe,
+			)));
+			io.extend_with(TransactionPaymentApi::to_delegate(TransactionPayment::new(client)));
+			io
+		},
 		parachain_build_import_queue,
 		|client,
 		 prometheus_registry,
 		 telemetry,
 		 task_manager,
-		 relay_chain_node,
+		 relay_chain_interface,
 		 transaction_pool,
 		 sync_oracle,
 		 keystore,
@@ -434,60 +474,47 @@ pub async fn start_node(
 				telemetry.clone(),
 			);
 
-			let relay_chain_backend = relay_chain_node.backend.clone();
-			let relay_chain_client = relay_chain_node.client.clone();
-			Ok(build_aura_consensus::<
-				sp_consensus_aura::sr25519::AuthorityPair,
-				_,
-				_,
-				_,
-				_,
-				_,
-				_,
-				_,
-				_,
-				_,
-			>(BuildAuraConsensusParams {
-				proposer_factory,
-				create_inherent_data_providers: move |_, (relay_parent, validation_data)| {
-					let parachain_inherent =
-						cumulus_primitives_parachain_inherent::ParachainInherentData::create_at_with_client(
+			Ok(AuraConsensus::build::<sp_consensus_aura::sr25519::AuthorityPair, _, _, _, _, _, _>(
+				BuildAuraConsensusParams {
+					proposer_factory,
+					create_inherent_data_providers: move |_, (relay_parent, validation_data)| {
+						let relay_chain_interface = relay_chain_interface.clone();
+						async move {
+							let parachain_inherent =
+						cumulus_primitives_parachain_inherent::ParachainInherentData::create_at(
 							relay_parent,
-							&relay_chain_client,
-							&*relay_chain_backend,
+							&relay_chain_interface,
 							&validation_data,
 							id,
-						);
-					async move {
-						let time = sp_timestamp::InherentDataProvider::from_system_time();
+						).await;
+							let time = sp_timestamp::InherentDataProvider::from_system_time();
 
-						let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_duration(
+							let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
 							*time,
-							slot_duration.slot_duration(),
+							slot_duration,
 						);
 
-						let parachain_inherent = parachain_inherent.ok_or_else(|| {
-							Box::<dyn std::error::Error + Send + Sync>::from(
-								"Failed to create parachain inherent",
-							)
-						})?;
-						Ok((time, slot, parachain_inherent))
-					}
+							let parachain_inherent = parachain_inherent.ok_or_else(|| {
+								Box::<dyn std::error::Error + Send + Sync>::from(
+									"Failed to create parachain inherent",
+								)
+							})?;
+							Ok((time, slot, parachain_inherent))
+						}
+					},
+					block_import: client.clone(),
+					para_client: client,
+					backoff_authoring_blocks: Option::<()>::None,
+					sync_oracle,
+					keystore,
+					force_authoring,
+					slot_duration,
+					// We got around 500ms for proposing
+					block_proposal_slot_portion: SlotProportion::new(1f32 / 24f32),
+					telemetry,
+					max_block_proposal_slot_portion: None,
 				},
-				block_import: client.clone(),
-				relay_chain_client: relay_chain_node.client.clone(),
-				relay_chain_backend: relay_chain_node.backend.clone(),
-				para_client: client,
-				backoff_authoring_blocks: Option::<()>::None,
-				sync_oracle,
-				keystore,
-				force_authoring,
-				slot_duration,
-				// We got around 500ms for proposing
-				block_proposal_slot_portion: SlotProportion::new(1f32 / 24f32),
-				telemetry,
-				max_block_proposal_slot_portion: None,
-			}))
+			))
 		},
 	)
 	.await
diff --git a/polkadot/bridges/bin/rialto-parachain/runtime/Cargo.toml b/polkadot/bridges/bin/rialto-parachain/runtime/Cargo.toml
index 20ce70aba8f6b22f219005a9337dceb90f44c0ca..1d0870fcbcd80f527a04f741786830eb082b520e 100644
--- a/polkadot/bridges/bin/rialto-parachain/runtime/Cargo.toml
+++ b/polkadot/bridges/bin/rialto-parachain/runtime/Cargo.toml
@@ -2,7 +2,7 @@
 name = "rialto-parachain-runtime"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
@@ -11,9 +11,9 @@ license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
 
 [dependencies]
-codec = { package = 'parity-scale-codec', version = '2.0.0', default-features = false, features = ['derive']}
+codec = { package = 'parity-scale-codec', version = '3.0.0', default-features = false, features = ['derive']}
 log = { version = "0.4.14", default-features = false }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = '1.0', optional = true, features = ['derive'] }
 
 # Bridge depedencies
diff --git a/polkadot/bridges/bin/rialto-parachain/runtime/src/lib.rs b/polkadot/bridges/bin/rialto-parachain/runtime/src/lib.rs
index 5a4c2ae62854e4551c35f0a503eb4eac06f45e0c..4f81927f0f6f2aa27247e5f1981ef74f424c639a 100644
--- a/polkadot/bridges/bin/rialto-parachain/runtime/src/lib.rs
+++ b/polkadot/bridges/bin/rialto-parachain/runtime/src/lib.rs
@@ -50,7 +50,7 @@ pub use frame_support::{
 	},
 	StorageValue,
 };
-pub use frame_system::Call as SystemCall;
+pub use frame_system::{Call as SystemCall, EnsureRoot};
 pub use pallet_balances::Call as BalancesCall;
 pub use pallet_timestamp::Call as TimestampCall;
 pub use sp_consensus_aura::sr25519::AuthorityId as AuraId;
@@ -104,7 +104,7 @@ pub type Executive = frame_executive::Executive<
 	Block,
 	frame_system::ChainContext<Runtime>,
 	Runtime,
-	AllPallets,
+	AllPalletsWithSystem,
 >;
 
 impl_opaque_keys! {
@@ -123,6 +123,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	impl_version: 0,
 	apis: RUNTIME_API_VERSIONS,
 	transaction_version: 1,
+	state_version: 0,
 };
 
 /// This determines the average expected block time that we are targeting.
@@ -210,6 +211,7 @@ impl frame_system::Config for Runtime {
 	type SS58Prefix = SS58Prefix;
 	/// The action to take on a Runtime Upgrade
 	type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode<Self>;
+	type MaxConsumers = frame_support::traits::ConstU32<16>;
 }
 
 parameter_types! {
@@ -268,7 +270,7 @@ parameter_types! {
 
 impl cumulus_pallet_parachain_system::Config for Runtime {
 	type Event = Event;
-	type OnValidationData = ();
+	type OnSystemEvent = ();
 	type SelfParaId = parachain_info::Pallet<Runtime>;
 	type OutboundXcmpMessageSource = XcmpQueue;
 	type DmpMessageHandler = DmpQueue;
@@ -294,7 +296,7 @@ parameter_types! {
 /// when determining ownership of accounts for asset transacting and when attempting to use XCM
 /// `Transact` in order to determine the dispatch Origin.
 pub type LocationToAccountId = (
-	// The parent (Relay-chain) origin converts to the parent `AccountId`.
+	// The parent (Relay-chain) origin converts to the default `AccountId`.
 	ParentIsPreset<AccountId>,
 	// Sibling parachain origins convert to AccountId via the `ParaId::into`.
 	SiblingParachainConvertsVia<Sibling, AccountId>,
@@ -421,6 +423,10 @@ impl cumulus_pallet_xcmp_queue::Config for Runtime {
 	type XcmExecutor = XcmExecutor<XcmConfig>;
 	type ChannelInfo = ParachainSystem;
 	type VersionWrapper = ();
+	type ExecuteOverweightOrigin = EnsureRoot<AccountId>;
+	type ControllerOrigin = EnsureRoot<AccountId>;
+	type ControllerOriginConverter = XcmOriginToTransactDispatchOrigin;
+	type WeightInfo = ();
 }
 
 impl cumulus_pallet_dmp_queue::Config for Runtime {
@@ -568,8 +574,8 @@ impl_runtime_apis! {
 	}
 
 	impl cumulus_primitives_core::CollectCollationInfo<Block> for Runtime {
-		fn collect_collation_info() -> cumulus_primitives_core::CollationInfo {
-			ParachainSystem::collect_collation_info()
+		fn collect_collation_info(header: &<Block as BlockT>::Header) -> cumulus_primitives_core::CollationInfo {
+			ParachainSystem::collect_collation_info(header)
 		}
 	}
 
@@ -621,7 +627,6 @@ impl_runtime_apis! {
 			let params = (&config, &whitelist);
 			add_benchmarks!(params, batches);
 
-			if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) }
 			Ok(batches)
 		}
 	}
diff --git a/polkadot/bridges/bin/rialto/node/Cargo.toml b/polkadot/bridges/bin/rialto/node/Cargo.toml
index 2795f2eecaecc00cb4532123ccae3eb7c2216a3c..e44ceb45faa97226f2a65f197367d1a1832a8921 100644
--- a/polkadot/bridges/bin/rialto/node/Cargo.toml
+++ b/polkadot/bridges/bin/rialto/node/Cargo.toml
@@ -3,19 +3,19 @@ name = "rialto-bridge-node"
 description = "Substrate node compatible with Rialto runtime"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 build = "build.rs"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
+clap = { version = "3.1", features = ["derive"] }
 futures = "0.3"
 jsonrpc-core = "18.0"
-kvdb = "0.10"
-kvdb-rocksdb = "0.12"
+kvdb = "0.11"
+kvdb-rocksdb = "0.15"
 lru = "0.7"
-structopt = "0.3.21"
 serde_json = "1.0.59"
 thiserror = "1.0"
 
@@ -50,7 +50,6 @@ sc-consensus-uncles = { git = "https://github.com/paritytech/substrate", branch
 sc-executor = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-finality-grandpa-rpc = { git = "https://github.com/paritytech/substrate", branch = "master" }
-#sc-finality-grandpa-warp-sync = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-network = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-rpc = { git = "https://github.com/paritytech/substrate", branch = "master" }
@@ -77,37 +76,10 @@ substrate-prometheus-endpoint = { git = "https://github.com/paritytech/substrate
 
 # Polkadot Dependencies
 
-polkadot-client = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-
-# Polkadot (parachain) Dependencies
-
-polkadot-approval-distribution = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-availability-bitfield-distribution = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-availability-distribution = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-availability-recovery = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-collator-protocol = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-dispute-distribution = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-gossip-support = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-network-bridge = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-collation-generation = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-approval-voting = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-av-store = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-backing = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-bitfield-signing = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-candidate-validation = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-chain-api = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-chain-selection = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-parachains-inherent = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-provisioner = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 polkadot-node-core-pvf = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-runtime-api = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-core-dispute-coordinator = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-network-protocol = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-node-subsystem-util = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-overseer = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 polkadot-primitives = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-polkadot-statement-distribution = { git = "https://github.com/paritytech/polkadot", branch = "master" }
+polkadot-service = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false, features = [ "full-node", "polkadot-native" ] }
 
 [build-dependencies]
 substrate-build-script-utils = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/bin/rialto/node/src/chain_spec.rs b/polkadot/bridges/bin/rialto/node/src/chain_spec.rs
index 4c00ff75efd24624ad5d237e9d7a4a79ba14358f..10315e33c853a61fe885115bb871543ee758ca3a 100644
--- a/polkadot/bridges/bin/rialto/node/src/chain_spec.rs
+++ b/polkadot/bridges/bin/rialto/node/src/chain_spec.rs
@@ -30,7 +30,8 @@ use sp_finality_grandpa::AuthorityId as GrandpaId;
 use sp_runtime::traits::{IdentifyAccount, Verify};
 
 /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type.
-pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
+pub type ChainSpec =
+	sc_service::GenericChainSpec<GenesisConfig, polkadot_service::chain_spec::Extensions>;
 
 /// The chain specification option. This is expected to come in from the CLI and
 /// is little more than one of a number of alternatives which can easily be converted
@@ -96,23 +97,16 @@ impl Alternative {
 					testnet_genesis(
 						vec![get_authority_keys_from_seed("Alice")],
 						get_account_id_from_seed::<sr25519::Public>("Alice"),
-						vec![
-							get_account_id_from_seed::<sr25519::Public>("Alice"),
-							get_account_id_from_seed::<sr25519::Public>("Bob"),
-							get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Bob"),
-							)),
-						],
+						endowed_accounts(),
 						true,
 					)
 				},
 				vec![],
 				None,
 				None,
-				properties,
 				None,
+				properties,
+				Default::default(),
 			),
 			Alternative::LocalTestnet => ChainSpec::from_genesis(
 				"Rialto Local",
@@ -128,61 +122,70 @@ impl Alternative {
 							get_authority_keys_from_seed("Eve"),
 						],
 						get_account_id_from_seed::<sr25519::Public>("Alice"),
-						vec![
-							get_account_id_from_seed::<sr25519::Public>("Alice"),
-							get_account_id_from_seed::<sr25519::Public>("Bob"),
-							get_account_id_from_seed::<sr25519::Public>("Charlie"),
-							get_account_id_from_seed::<sr25519::Public>("Dave"),
-							get_account_id_from_seed::<sr25519::Public>("Eve"),
-							get_account_id_from_seed::<sr25519::Public>("Ferdie"),
-							get_account_id_from_seed::<sr25519::Public>("George"),
-							get_account_id_from_seed::<sr25519::Public>("Harry"),
-							get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Charlie//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Dave//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Eve//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Ferdie//stash"),
-							get_account_id_from_seed::<sr25519::Public>("George//stash"),
-							get_account_id_from_seed::<sr25519::Public>("Harry//stash"),
-							get_account_id_from_seed::<sr25519::Public>("MillauMessagesOwner"),
-							get_account_id_from_seed::<sr25519::Public>("WithMillauTokenSwap"),
-							pallet_bridge_messages::relayer_fund_account_id::<
-								bp_rialto::AccountId,
-								bp_rialto::AccountIdConverter,
-							>(),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Alice"),
-							)),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Bob"),
-							)),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Charlie"),
-							)),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Dave"),
-							)),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Eve"),
-							)),
-							derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
-								get_account_id_from_seed::<sr25519::Public>("Ferdie"),
-							)),
-						],
+						endowed_accounts(),
 						true,
 					)
 				},
 				vec![],
 				None,
 				None,
-				properties,
 				None,
+				properties,
+				Default::default(),
 			),
 		}
 	}
 }
 
+/// We're using the same set of endowed accounts on all Millau chains (dev/local) to make
+/// sure that all accounts, required for bridge to be functional (e.g. relayers fund account,
+/// accounts used by relayers in our test deployments, accounts used for demonstration
+/// purposes), are all available on these chains.
+fn endowed_accounts() -> Vec<AccountId> {
+	vec![
+		get_account_id_from_seed::<sr25519::Public>("Alice"),
+		get_account_id_from_seed::<sr25519::Public>("Bob"),
+		get_account_id_from_seed::<sr25519::Public>("Charlie"),
+		get_account_id_from_seed::<sr25519::Public>("Dave"),
+		get_account_id_from_seed::<sr25519::Public>("Eve"),
+		get_account_id_from_seed::<sr25519::Public>("Ferdie"),
+		get_account_id_from_seed::<sr25519::Public>("George"),
+		get_account_id_from_seed::<sr25519::Public>("Harry"),
+		get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Charlie//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Dave//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Eve//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Ferdie//stash"),
+		get_account_id_from_seed::<sr25519::Public>("George//stash"),
+		get_account_id_from_seed::<sr25519::Public>("Harry//stash"),
+		get_account_id_from_seed::<sr25519::Public>("MillauMessagesOwner"),
+		get_account_id_from_seed::<sr25519::Public>("WithMillauTokenSwap"),
+		pallet_bridge_messages::relayer_fund_account_id::<
+			bp_rialto::AccountId,
+			bp_rialto::AccountIdConverter,
+		>(),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Alice"),
+		)),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Bob"),
+		)),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Charlie"),
+		)),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Dave"),
+		)),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Eve"),
+		)),
+		derive_account_from_millau_id(bp_runtime::SourceAccount::Account(
+			get_account_id_from_seed::<sr25519::Public>("Ferdie"),
+		)),
+	]
+}
+
 fn session_keys(
 	babe: BabeId,
 	beefy: BeefyId,
@@ -247,8 +250,8 @@ fn testnet_genesis(
 		// (see /node/service/src/chain_spec.rs:default_parachains_host_configuration)
 		configuration: ConfigurationConfig {
 			config: polkadot_runtime_parachains::configuration::HostConfiguration {
-				validation_upgrade_frequency: 1u32,
-				validation_upgrade_delay: 1,
+				validation_upgrade_cooldown: 2u32,
+				validation_upgrade_delay: 2,
 				code_retention_period: 1200,
 				max_code_size: polkadot_primitives::v2::MAX_CODE_SIZE,
 				max_pov_size: polkadot_primitives::v2::MAX_POV_SIZE,
@@ -258,13 +261,8 @@ fn testnet_genesis(
 				thread_availability_period: 4,
 				max_upward_queue_count: 8,
 				max_upward_queue_size: 1024 * 1024,
-				max_downward_message_size: 1024,
-				// this is approximatelly 4ms.
-				//
-				// Same as `4 * frame_support::weights::WEIGHT_PER_MILLIS`. We don't bother with
-				// an import since that's a made up number and should be replaced with a constant
-				// obtained by benchmarking anyway.
-				ump_service_total_weight: 4 * 1_000_000_000,
+				max_downward_message_size: 1024 * 1024,
+				ump_service_total_weight: 100_000_000_000,
 				max_upward_message_size: 50 * 1024,
 				max_upward_message_num_per_candidate: 5,
 				hrmp_sender_deposit: 0,
@@ -283,6 +281,7 @@ fn testnet_genesis(
 				needed_approvals: 2,
 				relay_vrf_modulo_samples: 2,
 				zeroth_delay_tranche_width: 0,
+				minimum_validation_upgrade_delay: 5,
 				..Default::default()
 			},
 		},
diff --git a/polkadot/bridges/bin/rialto/node/src/cli.rs b/polkadot/bridges/bin/rialto/node/src/cli.rs
index 3f85a69a713fe5125f2fe8d402c8fb1d9608b107..bb7f54998dd5fbb51626c0ebaeecaacf78aaab41 100644
--- a/polkadot/bridges/bin/rialto/node/src/cli.rs
+++ b/polkadot/bridges/bin/rialto/node/src/cli.rs
@@ -14,10 +14,10 @@
 // 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/>.
 
+use clap::Parser;
 use sc_cli::RunCmd;
-use structopt::StructOpt;
 
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct Cli {
 	#[structopt(subcommand)]
 	pub subcommand: Option<Subcommand>,
@@ -27,9 +27,10 @@ pub struct Cli {
 }
 
 /// Possible subcommands of the main binary.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub enum Subcommand {
 	/// Key management CLI utilities
+	#[clap(subcommand)]
 	Key(sc_cli::KeySubcommand),
 
 	/// Verify a signature for a message, provided on `STDIN`, with a given (public or secret) key.
@@ -69,16 +70,16 @@ pub enum Subcommand {
 	Benchmark(frame_benchmarking_cli::BenchmarkCmd),
 
 	/// FOR INTERNAL USE: analog of the "prepare-worker" command of the polkadot binary.
-	#[structopt(name = "prepare-worker", setting = structopt::clap::AppSettings::Hidden)]
+	#[clap(name = "prepare-worker", hide = true)]
 	PvfPrepareWorker(ValidationWorkerCommand),
 
 	/// FOR INTERNAL USE: analog of the "execute-worker" command of the polkadot binary.
-	#[structopt(name = "execute-worker", setting = structopt::clap::AppSettings::Hidden)]
+	#[clap(name = "execute-worker", hide = true)]
 	PvfExecuteWorker(ValidationWorkerCommand),
 }
 
 /// Validation worker command.
-#[derive(Debug, StructOpt)]
+#[derive(Debug, Parser)]
 pub struct ValidationWorkerCommand {
 	/// The path to the validation host's socket.
 	pub socket_path: String,
diff --git a/polkadot/bridges/bin/rialto/node/src/command.rs b/polkadot/bridges/bin/rialto/node/src/command.rs
index 7be615a57760c56d450f794d775b25df2089b24a..da92837f06c09f47ebe40c9fa264c3c2890b9af4 100644
--- a/polkadot/bridges/bin/rialto/node/src/command.rs
+++ b/polkadot/bridges/bin/rialto/node/src/command.rs
@@ -14,13 +14,9 @@
 // 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/>.
 
-use crate::{
-	cli::{Cli, Subcommand},
-	service::new_partial,
-};
+use crate::cli::{Cli, Subcommand};
 use rialto_runtime::{Block, RuntimeApi};
 use sc_cli::{ChainSpec, Role, RuntimeVersion, SubstrateCli};
-use sc_service::PartialComponents;
 
 impl SubstrateCli for Cli {
 	fn impl_name() -> String {
@@ -67,6 +63,21 @@ impl SubstrateCli for Cli {
 	}
 }
 
+// Rialto native executor instance.
+pub struct ExecutorDispatch;
+
+impl sc_executor::NativeExecutionDispatch for ExecutorDispatch {
+	type ExtendHostFunctions = frame_benchmarking::benchmarking::HostFunctions;
+
+	fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
+		rialto_runtime::api::dispatch(method, data)
+	}
+
+	fn native_version() -> sc_executor::NativeVersion {
+		rialto_runtime::native_version()
+	}
+}
+
 /// Parse and run command line arguments
 pub fn run() -> sc_cli::Result<()> {
 	let cli = Cli::from_args();
@@ -79,7 +90,7 @@ pub fn run() -> sc_cli::Result<()> {
 			if cfg!(feature = "runtime-benchmarks") {
 				let runner = cli.create_runner(cmd)?;
 
-				runner.sync_run(|config| cmd.run::<Block, crate::service::ExecutorDispatch>(config))
+				runner.sync_run(|config| cmd.run::<Block, ExecutorDispatch>(config))
 			} else {
 				println!(
 					"Benchmarking wasn't enabled when building the node. \
@@ -98,32 +109,32 @@ pub fn run() -> sc_cli::Result<()> {
 		Some(Subcommand::CheckBlock(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
 			runner.async_run(|mut config| {
-				let PartialComponents { client, task_manager, import_queue, .. } =
-					new_partial(&mut config).map_err(service_error)?;
+				let (client, _, import_queue, task_manager) =
+					polkadot_service::new_chain_ops(&mut config, None).map_err(service_error)?;
 				Ok((cmd.run(client, import_queue), task_manager))
 			})
 		},
 		Some(Subcommand::ExportBlocks(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
 			runner.async_run(|mut config| {
-				let PartialComponents { client, task_manager, .. } =
-					new_partial(&mut config).map_err(service_error)?;
+				let (client, _, _, task_manager) =
+					polkadot_service::new_chain_ops(&mut config, None).map_err(service_error)?;
 				Ok((cmd.run(client, config.database), task_manager))
 			})
 		},
 		Some(Subcommand::ExportState(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
 			runner.async_run(|mut config| {
-				let PartialComponents { client, task_manager, .. } =
-					new_partial(&mut config).map_err(service_error)?;
+				let (client, _, _, task_manager) =
+					polkadot_service::new_chain_ops(&mut config, None).map_err(service_error)?;
 				Ok((cmd.run(client, config.chain_spec), task_manager))
 			})
 		},
 		Some(Subcommand::ImportBlocks(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
 			runner.async_run(|mut config| {
-				let PartialComponents { client, task_manager, import_queue, .. } =
-					new_partial(&mut config).map_err(service_error)?;
+				let (client, _, import_queue, task_manager) =
+					polkadot_service::new_chain_ops(&mut config, None).map_err(service_error)?;
 				Ok((cmd.run(client, import_queue), task_manager))
 			})
 		},
@@ -134,16 +145,14 @@ pub fn run() -> sc_cli::Result<()> {
 		Some(Subcommand::Revert(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
 			runner.async_run(|mut config| {
-				let PartialComponents { client, task_manager, backend, .. } =
-					new_partial(&mut config).map_err(service_error)?;
+				let (client, backend, _, task_manager) =
+					polkadot_service::new_chain_ops(&mut config, None).map_err(service_error)?;
 				Ok((cmd.run(client, backend), task_manager))
 			})
 		},
 		Some(Subcommand::Inspect(cmd)) => {
 			let runner = cli.create_runner(cmd)?;
-			runner.sync_run(|config| {
-				cmd.run::<Block, RuntimeApi, crate::service::ExecutorDispatch>(config)
-			})
+			runner.sync_run(|config| cmd.run::<Block, RuntimeApi, ExecutorDispatch>(config))
 		},
 		Some(Subcommand::PvfPrepareWorker(cmd)) => {
 			let mut builder = sc_cli::LoggerBuilder::new("");
@@ -170,15 +179,35 @@ pub fn run() -> sc_cli::Result<()> {
 			// let no_beefy = true;
 			// let telemetry_worker_handler = None;
 			// let is_collator = crate::service::IsCollator::No;
-			let overseer_gen = crate::overseer::RealOverseerGen;
+			let overseer_gen = polkadot_service::overseer::RealOverseerGen;
 			runner.run_node_until_exit(|config| async move {
 				match config.role {
 					Role::Light => Err(sc_cli::Error::Service(sc_service::Error::Other(
 						"Light client is not supported by this node".into(),
 					))),
-					_ => crate::service::build_full(config, overseer_gen)
-						.map(|full| full.task_manager)
-						.map_err(service_error),
+					_ => {
+						let is_collator = polkadot_service::IsCollator::No;
+						let grandpa_pause = None;
+						let enable_beefy = true;
+						let jaeger_agent = None;
+						let telemetry_worker_handle = None;
+						let program_path = None;
+						let overseer_enable_anyways = false;
+
+						polkadot_service::new_full::<rialto_runtime::RuntimeApi, ExecutorDispatch, _>(
+							config,
+							is_collator,
+							grandpa_pause,
+							enable_beefy,
+							jaeger_agent,
+							telemetry_worker_handle,
+							program_path,
+							overseer_enable_anyways,
+							overseer_gen,
+						)
+							.map(|full| full.task_manager)
+							.map_err(service_error)
+					},
 				}
 			})
 		},
@@ -187,6 +216,6 @@ pub fn run() -> sc_cli::Result<()> {
 
 // We don't want to change 'service.rs' too much to ease future updates => it'll keep using
 // its own error enum like original polkadot service does.
-fn service_error(err: crate::service::Error) -> sc_cli::Error {
+fn service_error(err: polkadot_service::Error) -> sc_cli::Error {
 	sc_cli::Error::Application(Box::new(err))
 }
diff --git a/polkadot/bridges/bin/rialto/node/src/main.rs b/polkadot/bridges/bin/rialto/node/src/main.rs
index 824814224e548418d402e542eec8e33755b949e4..6dea84a309b262c3a557aa9df5298cb5f09718e2 100644
--- a/polkadot/bridges/bin/rialto/node/src/main.rs
+++ b/polkadot/bridges/bin/rialto/node/src/main.rs
@@ -19,12 +19,8 @@
 #![warn(missing_docs)]
 
 mod chain_spec;
-#[macro_use]
-mod service;
 mod cli;
 mod command;
-mod overseer;
-mod parachains_db;
 
 /// Run the Rialto Node
 fn main() -> sc_cli::Result<()> {
diff --git a/polkadot/bridges/bin/rialto/node/src/overseer.rs b/polkadot/bridges/bin/rialto/node/src/overseer.rs
deleted file mode 100644
index 8a97d472eb6e24da083144e5584168b12cb1a273..0000000000000000000000000000000000000000
--- a/polkadot/bridges/bin/rialto/node/src/overseer.rs
+++ /dev/null
@@ -1,320 +0,0 @@
-// 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/>.
-
-//! This is almost 1:1 copy of `node/service/src/overseer.rs` file from Polkadot repository.
-//! The only exception is that we don't support db upgrades => no `upgrade.rs` module.
-
-// this warning comes from `polkadot_overseer::AllSubsystems` type
-#![allow(clippy::type_complexity)]
-
-use crate::service::{AuthorityDiscoveryApi, Error};
-use rialto_runtime::{opaque::Block, Hash};
-
-use lru::LruCache;
-use polkadot_availability_distribution::IncomingRequestReceivers;
-use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig;
-use polkadot_node_core_av_store::Config as AvailabilityConfig;
-use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig;
-use polkadot_node_core_chain_selection::Config as ChainSelectionConfig;
-use polkadot_node_core_dispute_coordinator::Config as DisputeCoordinatorConfig;
-use polkadot_node_core_provisioner::ProvisionerConfig;
-use polkadot_node_network_protocol::request_response::{v1 as request_v1, IncomingRequestReceiver};
-use polkadot_overseer::{
-	metrics::Metrics as OverseerMetrics, BlockInfo, MetricsTrait, Overseer, InitializedOverseerBuilder,
-	OverseerConnector, OverseerHandle,
-};
-use polkadot_primitives::v2::ParachainHost;
-use sc_authority_discovery::Service as AuthorityDiscoveryService;
-use sc_client_api::AuxStore;
-use sc_keystore::LocalKeystore;
-use sp_api::ProvideRuntimeApi;
-use sp_blockchain::HeaderBackend;
-use sp_consensus_babe::BabeApi;
-use sp_core::traits::SpawnNamed;
-use std::sync::Arc;
-use substrate_prometheus_endpoint::Registry;
-
-pub use polkadot_approval_distribution::ApprovalDistribution as ApprovalDistributionSubsystem;
-pub use polkadot_availability_bitfield_distribution::BitfieldDistribution as BitfieldDistributionSubsystem;
-pub use polkadot_availability_distribution::AvailabilityDistributionSubsystem;
-pub use polkadot_availability_recovery::AvailabilityRecoverySubsystem;
-pub use polkadot_collator_protocol::{CollatorProtocolSubsystem, ProtocolSide};
-pub use polkadot_dispute_distribution::DisputeDistributionSubsystem;
-pub use polkadot_gossip_support::GossipSupport as GossipSupportSubsystem;
-pub use polkadot_network_bridge::NetworkBridge as NetworkBridgeSubsystem;
-pub use polkadot_node_collation_generation::CollationGenerationSubsystem;
-pub use polkadot_node_core_approval_voting::ApprovalVotingSubsystem;
-pub use polkadot_node_core_av_store::AvailabilityStoreSubsystem;
-pub use polkadot_node_core_backing::CandidateBackingSubsystem;
-pub use polkadot_node_core_bitfield_signing::BitfieldSigningSubsystem;
-pub use polkadot_node_core_candidate_validation::CandidateValidationSubsystem;
-pub use polkadot_node_core_chain_api::ChainApiSubsystem;
-pub use polkadot_node_core_chain_selection::ChainSelectionSubsystem;
-pub use polkadot_node_core_dispute_coordinator::DisputeCoordinatorSubsystem;
-pub use polkadot_node_core_provisioner::ProvisionerSubsystem;
-pub use polkadot_node_core_runtime_api::RuntimeApiSubsystem;
-pub use polkadot_statement_distribution::StatementDistributionSubsystem;
-
-/// Arguments passed for overseer construction.
-pub struct OverseerGenArgs<'a, Spawner, RuntimeClient>
-where
-	RuntimeClient: 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block> + AuxStore,
-	RuntimeClient::Api: ParachainHost<Block> + BabeApi<Block> + AuthorityDiscoveryApi<Block>,
-	Spawner: 'static + SpawnNamed + Clone + Unpin,
-{
-	/// Set of initial relay chain leaves to track.
-	pub leaves: Vec<BlockInfo>,
-	/// The keystore to use for i.e. validator keys.
-	pub keystore: Arc<LocalKeystore>,
-	/// Runtime client generic, providing the `ProvieRuntimeApi` trait besides others.
-	pub runtime_client: Arc<RuntimeClient>,
-	/// The underlying key value store for the parachains.
-	pub parachains_db: Arc<dyn kvdb::KeyValueDB>,
-	/// Underlying network service implementation.
-	pub network_service: Arc<sc_network::NetworkService<Block, Hash>>,
-	/// Underlying authority discovery service.
-	pub authority_discovery_service: AuthorityDiscoveryService,
-	/// POV request receiver
-	pub pov_req_receiver: IncomingRequestReceiver<request_v1::PoVFetchingRequest>,
-	pub chunk_req_receiver: IncomingRequestReceiver<request_v1::ChunkFetchingRequest>,
-	pub collation_req_receiver: IncomingRequestReceiver<request_v1::CollationFetchingRequest>,
-	pub available_data_req_receiver:
-		IncomingRequestReceiver<request_v1::AvailableDataFetchingRequest>,
-	pub statement_req_receiver: IncomingRequestReceiver<request_v1::StatementFetchingRequest>,
-	pub dispute_req_receiver: IncomingRequestReceiver<request_v1::DisputeRequest>,
-	/// Prometheus registry, commonly used for production systems, less so for test.
-	pub registry: Option<&'a Registry>,
-	/// Task spawner to be used throughout the overseer and the APIs it provides.
-	pub spawner: Spawner,
-	/// Configuration for the approval voting subsystem.
-	pub approval_voting_config: ApprovalVotingConfig,
-	/// Configuration for the availability store subsystem.
-	pub availability_config: AvailabilityConfig,
-	/// Configuration for the candidate validation subsystem.
-	pub candidate_validation_config: CandidateValidationConfig,
-	/// Configuration for the chain selection subsystem.
-	pub chain_selection_config: ChainSelectionConfig,
-	/// Configuration for the dispute coordinator subsystem.
-	pub dispute_coordinator_config: DisputeCoordinatorConfig,
-	/// Configuration for the provisioner subsystem.
-	pub disputes_enabled: bool,
-}
-
-/// Obtain a prepared `OverseerBuilder`, that is initialized
-/// with all default values.
-pub fn prepared_overseer_builder<Spawner, RuntimeClient>(
-	OverseerGenArgs {
-		leaves,
-		keystore,
-		runtime_client,
-		parachains_db,
-		network_service,
-		authority_discovery_service,
-		pov_req_receiver,
-		chunk_req_receiver,
-		collation_req_receiver: _,
-		available_data_req_receiver,
-		statement_req_receiver,
-		dispute_req_receiver,
-		registry,
-		spawner,
-		approval_voting_config,
-		availability_config,
-		candidate_validation_config,
-		chain_selection_config,
-		dispute_coordinator_config,
-		disputes_enabled,
-	}: OverseerGenArgs<'_, Spawner, RuntimeClient>,
-) -> Result<
-	InitializedOverseerBuilder<
-		Spawner,
-		Arc<RuntimeClient>,
-		CandidateValidationSubsystem,
-		CandidateBackingSubsystem<Spawner>,
-		StatementDistributionSubsystem,
-		AvailabilityDistributionSubsystem,
-		AvailabilityRecoverySubsystem,
-		BitfieldSigningSubsystem<Spawner>,
-		BitfieldDistributionSubsystem,
-		ProvisionerSubsystem<Spawner>,
-		RuntimeApiSubsystem<RuntimeClient>,
-		AvailabilityStoreSubsystem,
-		NetworkBridgeSubsystem<
-			Arc<sc_network::NetworkService<Block, Hash>>,
-			AuthorityDiscoveryService,
-		>,
-		ChainApiSubsystem<RuntimeClient>,
-		CollationGenerationSubsystem,
-		CollatorProtocolSubsystem,
-		ApprovalDistributionSubsystem,
-		ApprovalVotingSubsystem,
-		GossipSupportSubsystem<AuthorityDiscoveryService>,
-		DisputeCoordinatorSubsystem,
-		DisputeDistributionSubsystem<AuthorityDiscoveryService>,
-		ChainSelectionSubsystem,
-	>,
-	Error,
->
-where
-	RuntimeClient: 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block> + AuxStore,
-	RuntimeClient::Api: ParachainHost<Block> + BabeApi<Block> + AuthorityDiscoveryApi<Block>,
-	Spawner: 'static + SpawnNamed + Clone + Unpin,
-{
-	use polkadot_node_subsystem_util::metrics::Metrics;
-	use std::iter::FromIterator;
-
-	let metrics = <OverseerMetrics as MetricsTrait>::register(registry)?;
-
-	let builder = Overseer::builder()
-		.availability_distribution(AvailabilityDistributionSubsystem::new(
-			keystore.clone(),
-			IncomingRequestReceivers { pov_req_receiver, chunk_req_receiver },
-			Metrics::register(registry)?,
-		))
-		.availability_recovery(AvailabilityRecoverySubsystem::with_chunks_only(
-			available_data_req_receiver,
-			Metrics::register(registry)?,
-		))
-		.availability_store(AvailabilityStoreSubsystem::new(
-			parachains_db.clone(),
-			availability_config,
-			Metrics::register(registry)?,
-		))
-		.bitfield_distribution(BitfieldDistributionSubsystem::new(Metrics::register(registry)?))
-		.bitfield_signing(BitfieldSigningSubsystem::new(
-			spawner.clone(),
-			keystore.clone(),
-			Metrics::register(registry)?,
-		))
-		.candidate_backing(CandidateBackingSubsystem::new(
-			spawner.clone(),
-			keystore.clone(),
-			Metrics::register(registry)?,
-		))
-		.candidate_validation(CandidateValidationSubsystem::with_config(
-			candidate_validation_config,
-			Metrics::register(registry)?, // candidate-validation metrics
-			Metrics::register(registry)?, // validation host metrics
-		))
-		.chain_api(ChainApiSubsystem::new(runtime_client.clone(), Metrics::register(registry)?))
-		.collation_generation(CollationGenerationSubsystem::new(Metrics::register(registry)?))
-		.collator_protocol(CollatorProtocolSubsystem::new(ProtocolSide::Validator {
-			keystore: keystore.clone(),
-			eviction_policy: Default::default(),
-			metrics: Metrics::register(registry)?,
-		}))
-		.network_bridge(NetworkBridgeSubsystem::new(
-			network_service.clone(),
-			authority_discovery_service.clone(),
-			Box::new(network_service.clone()),
-			Metrics::register(registry)?,
-		))
-		.provisioner(ProvisionerSubsystem::new(spawner.clone(), ProvisionerConfig { disputes_enabled }, Metrics::register(registry)?))
-		.runtime_api(RuntimeApiSubsystem::new(
-			runtime_client.clone(),
-			Metrics::register(registry)?,
-			spawner.clone(),
-		))
-		.statement_distribution(StatementDistributionSubsystem::new(
-			keystore.clone(),
-			statement_req_receiver,
-			Metrics::register(registry)?,
-		))
-		.approval_distribution(ApprovalDistributionSubsystem::new(Metrics::register(registry)?))
-		.approval_voting(ApprovalVotingSubsystem::with_config(
-			approval_voting_config,
-			parachains_db.clone(),
-			keystore.clone(),
-			Box::new(network_service),
-			Metrics::register(registry)?,
-		))
-		.gossip_support(GossipSupportSubsystem::new(
-			keystore.clone(),
-			authority_discovery_service.clone(),
-		))
-		.dispute_coordinator(DisputeCoordinatorSubsystem::new(
-			parachains_db.clone(),
-			dispute_coordinator_config,
-			keystore.clone(),
-			Metrics::register(registry)?,
-		))
-		.dispute_distribution(DisputeDistributionSubsystem::new(
-			keystore,
-			dispute_req_receiver,
-			authority_discovery_service,
-			Metrics::register(registry)?,
-		))
-		.chain_selection(ChainSelectionSubsystem::new(chain_selection_config, parachains_db))
-		.leaves(Vec::from_iter(
-			leaves
-				.into_iter()
-				.map(|BlockInfo { hash, parent_hash: _, number }| (hash, number)),
-		))
-		.activation_external_listeners(Default::default())
-		.span_per_active_leaf(Default::default())
-		.active_leaves(Default::default())
-		.supports_parachains(runtime_client)
-		.known_leaves(LruCache::new(KNOWN_LEAVES_CACHE_SIZE))
-		.metrics(metrics)
-		.spawner(spawner);
-	Ok(builder)
-}
-
-/// Trait for the `fn` generating the overseer.
-///
-/// Default behavior is to create an unmodified overseer, as `RealOverseerGen`
-/// would do.
-pub trait OverseerGen {
-	/// Overwrite the full generation of the overseer, including the subsystems.
-	fn generate<Spawner, RuntimeClient>(
-		&self,
-		connector: OverseerConnector,
-		args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
-	) -> Result<(Overseer<Spawner, Arc<RuntimeClient>>, OverseerHandle), Error>
-	where
-		RuntimeClient: 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block> + AuxStore,
-		RuntimeClient::Api: ParachainHost<Block> + BabeApi<Block> + AuthorityDiscoveryApi<Block>,
-		Spawner: 'static + SpawnNamed + Clone + Unpin,
-	{
-		let gen = RealOverseerGen;
-		RealOverseerGen::generate::<Spawner, RuntimeClient>(&gen, connector, args)
-	}
-	// It would be nice to make `create_subsystems` part of this trait,
-	// but the amount of generic arguments that would be required as
-	// as consequence make this rather annoying to implement and use.
-}
-
-use polkadot_overseer::KNOWN_LEAVES_CACHE_SIZE;
-
-/// The regular set of subsystems.
-pub struct RealOverseerGen;
-
-impl OverseerGen for RealOverseerGen {
-	fn generate<Spawner, RuntimeClient>(
-		&self,
-		connector: OverseerConnector,
-		args: OverseerGenArgs<'_, Spawner, RuntimeClient>,
-	) -> Result<(Overseer<Spawner, Arc<RuntimeClient>>, OverseerHandle), Error>
-	where
-		RuntimeClient: 'static + ProvideRuntimeApi<Block> + HeaderBackend<Block> + AuxStore,
-		RuntimeClient::Api: ParachainHost<Block> + BabeApi<Block> + AuthorityDiscoveryApi<Block>,
-		Spawner: 'static + SpawnNamed + Clone + Unpin,
-	{
-		prepared_overseer_builder(args)?
-			.build_with_connector(connector)
-			.map_err(|e| e.into())
-	}
-}
diff --git a/polkadot/bridges/bin/rialto/node/src/parachains_db.rs b/polkadot/bridges/bin/rialto/node/src/parachains_db.rs
deleted file mode 100644
index bf2052043c98797e5f2e594b75ada58397f4d109..0000000000000000000000000000000000000000
--- a/polkadot/bridges/bin/rialto/node/src/parachains_db.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-// 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/>.
-
-//! This is almost 1:1 copy of `node/service/parachains_db/mod.rs` file from Polkadot repository.
-//! The only exception is that we don't support db upgrades => no `upgrade.rs` module.
-
-use kvdb::KeyValueDB;
-use std::{io, path::PathBuf, sync::Arc};
-
-mod columns {
-	pub const NUM_COLUMNS: u32 = 5;
-
-	pub const COL_AVAILABILITY_DATA: u32 = 0;
-	pub const COL_AVAILABILITY_META: u32 = 1;
-	pub const COL_APPROVAL_DATA: u32 = 2;
-	pub const COL_CHAIN_SELECTION_DATA: u32 = 3;
-	pub const COL_DISPUTE_COORDINATOR_DATA: u32 = 4;
-}
-
-/// Columns used by different subsystems.
-#[derive(Debug, Clone)]
-pub struct ColumnsConfig {
-	/// The column used by the av-store for data.
-	pub col_availability_data: u32,
-	/// The column used by the av-store for meta information.
-	pub col_availability_meta: u32,
-	/// The column used by approval voting for data.
-	pub col_approval_data: u32,
-	/// The column used by chain selection for data.
-	pub col_chain_selection_data: u32,
-	/// The column used by dispute coordinator for data.
-	pub col_dispute_coordinator_data: u32,
-}
-
-/// The real columns used by the parachains DB.
-pub const REAL_COLUMNS: ColumnsConfig = ColumnsConfig {
-	col_availability_data: columns::COL_AVAILABILITY_DATA,
-	col_availability_meta: columns::COL_AVAILABILITY_META,
-	col_approval_data: columns::COL_APPROVAL_DATA,
-	col_chain_selection_data: columns::COL_CHAIN_SELECTION_DATA,
-	col_dispute_coordinator_data: columns::COL_DISPUTE_COORDINATOR_DATA,
-};
-
-/// The cache size for each column, in megabytes.
-#[derive(Debug, Clone)]
-pub struct CacheSizes {
-	/// Cache used by availability data.
-	pub availability_data: usize,
-	/// Cache used by availability meta.
-	pub availability_meta: usize,
-	/// Cache used by approval data.
-	pub approval_data: usize,
-}
-
-impl Default for CacheSizes {
-	fn default() -> Self {
-		CacheSizes { availability_data: 25, availability_meta: 1, approval_data: 5 }
-	}
-}
-
-fn other_io_error(err: String) -> io::Error {
-	io::Error::new(io::ErrorKind::Other, err)
-}
-
-/// Open the database on disk, creating it if it doesn't exist.
-pub fn open_creating(root: PathBuf, cache_sizes: CacheSizes) -> io::Result<Arc<dyn KeyValueDB>> {
-	use kvdb_rocksdb::{Database, DatabaseConfig};
-
-	let path = root.join("parachains").join("db");
-
-	let mut db_config = DatabaseConfig::with_columns(columns::NUM_COLUMNS);
-
-	let _ = db_config
-		.memory_budget
-		.insert(columns::COL_AVAILABILITY_DATA, cache_sizes.availability_data);
-	let _ = db_config
-		.memory_budget
-		.insert(columns::COL_AVAILABILITY_META, cache_sizes.availability_meta);
-	let _ = db_config
-		.memory_budget
-		.insert(columns::COL_APPROVAL_DATA, cache_sizes.approval_data);
-
-	let path_str = path
-		.to_str()
-		.ok_or_else(|| other_io_error(format!("Bad database path: {:?}", path)))?;
-
-	std::fs::create_dir_all(&path_str)?;
-	let db = Database::open(&db_config, path_str)?;
-
-	Ok(Arc::new(db))
-}
diff --git a/polkadot/bridges/bin/rialto/node/src/service.rs b/polkadot/bridges/bin/rialto/node/src/service.rs
deleted file mode 100644
index df0fc58a5b5ca9b6f8851d0c60888fd9cea3c325..0000000000000000000000000000000000000000
--- a/polkadot/bridges/bin/rialto/node/src/service.rs
+++ /dev/null
@@ -1,759 +0,0 @@
-// 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/>.
-
-//! Rialto chain node service.
-//!
-//! The code is mostly copy of `service/src/lib.rs` file from Polkadot repository
-//! without optional functions, and with BEEFY added on top.
-
-use crate::overseer::{OverseerGen, OverseerGenArgs};
-
-use polkadot_client::RuntimeApiCollection;
-use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig;
-use polkadot_node_core_av_store::Config as AvailabilityConfig;
-use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig;
-use polkadot_node_core_chain_selection::Config as ChainSelectionConfig;
-use polkadot_node_core_dispute_coordinator::Config as DisputeCoordinatorConfig;
-use polkadot_node_network_protocol::request_response::IncomingRequest;
-use polkadot_overseer::{BlockInfo, OverseerConnector};
-use polkadot_primitives::v2::BlockId;
-use rialto_runtime::{self, opaque::Block, RuntimeApi};
-use sc_client_api::ExecutorProvider;
-use sc_executor::{NativeElseWasmExecutor, NativeExecutionDispatch};
-use sc_finality_grandpa::FinalityProofProvider as GrandpaFinalityProofProvider;
-use sc_service::{config::PrometheusConfig, Configuration, TaskManager};
-use sc_telemetry::{Telemetry, TelemetryWorker};
-use sp_api::{ConstructRuntimeApi, HeaderT};
-use sp_consensus::SelectChain;
-use sp_runtime::traits::Block as BlockT;
-use std::{sync::Arc, time::Duration};
-use substrate_prometheus_endpoint::Registry;
-
-pub use polkadot_overseer::Handle;
-pub use polkadot_primitives::v2::ParachainHost;
-pub use sc_client_api::AuxStore;
-pub use sp_authority_discovery::AuthorityDiscoveryApi;
-pub use sp_blockchain::HeaderBackend;
-pub use sp_consensus_babe::BabeApi;
-
-pub type Executor = NativeElseWasmExecutor<ExecutorDispatch>;
-
-// Our native executor instance.
-pub struct ExecutorDispatch;
-
-impl sc_executor::NativeExecutionDispatch for ExecutorDispatch {
-	type ExtendHostFunctions = frame_benchmarking::benchmarking::HostFunctions;
-
-	fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
-		rialto_runtime::api::dispatch(method, data)
-	}
-
-	fn native_version() -> sc_executor::NativeVersion {
-		rialto_runtime::native_version()
-	}
-}
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
-	#[error(transparent)]
-	Io(#[from] std::io::Error),
-
-	#[error(transparent)]
-	Cli(#[from] sc_cli::Error),
-
-	#[error(transparent)]
-	Blockchain(#[from] sp_blockchain::Error),
-
-	#[error(transparent)]
-	Consensus(#[from] sp_consensus::Error),
-
-	#[error(transparent)]
-	Service(#[from] sc_service::Error),
-
-	#[error(transparent)]
-	Telemetry(#[from] sc_telemetry::Error),
-
-	#[error("Failed to create an overseer")]
-	Overseer(#[from] polkadot_overseer::SubsystemError),
-
-	#[error(transparent)]
-	Prometheus(#[from] substrate_prometheus_endpoint::PrometheusError),
-
-	#[error("Authorities require the real overseer implementation")]
-	AuthoritiesRequireRealOverseer,
-
-	#[error("Creating a custom database is required for validators")]
-	DatabasePathRequired,
-}
-
-type FullClient = sc_service::TFullClient<Block, RuntimeApi, Executor>;
-type FullBackend = sc_service::TFullBackend<Block>;
-type FullSelectChain = sc_consensus::LongestChain<FullBackend, Block>;
-type FullGrandpaBlockImport =
-	sc_finality_grandpa::GrandpaBlockImport<FullBackend, Block, FullClient, FullSelectChain>;
-type FullTransactionPool = sc_transaction_pool::FullPool<Block, FullClient>;
-type FullBabeBlockImport =
-	sc_consensus_babe::BabeBlockImport<Block, FullClient, FullGrandpaBlockImport>;
-type FullBabeLink = sc_consensus_babe::BabeLink<Block>;
-type FullGrandpaLink = sc_finality_grandpa::LinkHalf<Block, FullClient, FullSelectChain>;
-
-// If we're using prometheus, use a registry with a prefix of `polkadot`.
-fn set_prometheus_registry(config: &mut Configuration) -> Result<(), Error> {
-	if let Some(PrometheusConfig { registry, .. }) = config.prometheus_config.as_mut() {
-		*registry = Registry::new_custom(Some("polkadot".into()), None)?;
-	}
-
-	Ok(())
-}
-
-// Needed here for complex return type while `impl Trait` in type aliases is unstable.
-#[allow(clippy::type_complexity)]
-pub fn new_partial(
-	config: &mut Configuration,
-) -> Result<
-	sc_service::PartialComponents<
-		FullClient,
-		FullBackend,
-		FullSelectChain,
-		sc_consensus::DefaultImportQueue<Block, FullClient>,
-		FullTransactionPool,
-		(
-			impl Fn(
-				sc_rpc::DenyUnsafe,
-				sc_rpc::SubscriptionTaskExecutor,
-			) -> Result<jsonrpc_core::IoHandler<sc_service::RpcMetadata>, sc_service::Error>,
-			(
-				FullBabeBlockImport,
-				FullGrandpaLink,
-				FullBabeLink,
-				beefy_gadget::notification::BeefySignedCommitmentSender<Block>,
-			),
-			sc_finality_grandpa::SharedVoterState,
-			std::time::Duration,
-			Option<Telemetry>,
-		),
-	>,
-	Error,
->
-where
-	RuntimeApi: ConstructRuntimeApi<Block, FullClient> + Send + Sync + 'static,
-	<RuntimeApi as ConstructRuntimeApi<Block, FullClient>>::RuntimeApi:
-		RuntimeApiCollection<StateBackend = sc_client_api::StateBackendFor<FullBackend, Block>>,
-	ExecutorDispatch: NativeExecutionDispatch + 'static,
-{
-	set_prometheus_registry(config)?;
-
-	let telemetry = config
-		.telemetry_endpoints
-		.clone()
-		.filter(|x| !x.is_empty())
-		.map(|endpoints| -> Result<_, sc_telemetry::Error> {
-			let worker = TelemetryWorker::new(16)?;
-			let telemetry = worker.handle().new_telemetry(endpoints);
-			Ok((worker, telemetry))
-		})
-		.transpose()?;
-
-	let executor = NativeElseWasmExecutor::<ExecutorDispatch>::new(
-		config.wasm_method,
-		config.default_heap_pages,
-		config.max_runtime_instances,
-		config.runtime_cache_size,
-	);
-
-	let (client, backend, keystore_container, task_manager) =
-		sc_service::new_full_parts::<Block, RuntimeApi, Executor>(
-			config,
-			telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()),
-			executor,
-		)?;
-	let client = Arc::new(client);
-
-	let telemetry = telemetry.map(|(worker, telemetry)| {
-		task_manager.spawn_handle().spawn("telemetry", None, worker.run());
-		telemetry
-	});
-
-	let select_chain = sc_consensus::LongestChain::new(backend.clone());
-
-	let transaction_pool = sc_transaction_pool::BasicPool::new_full(
-		config.transaction_pool.clone(),
-		config.role.is_authority().into(),
-		config.prometheus_registry(),
-		task_manager.spawn_essential_handle(),
-		client.clone(),
-	);
-
-	let (grandpa_block_import, grandpa_link) =
-		sc_finality_grandpa::block_import_with_authority_set_hard_forks(
-			client.clone(),
-			&(client.clone() as Arc<_>),
-			select_chain.clone(),
-			Vec::new(),
-			telemetry.as_ref().map(|x| x.handle()),
-		)?;
-	let justification_import = grandpa_block_import.clone();
-
-	let babe_config = sc_consensus_babe::Config::get(&*client)?;
-	let (block_import, babe_link) =
-		sc_consensus_babe::block_import(babe_config.clone(), grandpa_block_import, client.clone())?;
-
-	let slot_duration = babe_link.config().slot_duration();
-	let import_queue = sc_consensus_babe::import_queue(
-		babe_link.clone(),
-		block_import.clone(),
-		Some(Box::new(justification_import)),
-		client.clone(),
-		select_chain.clone(),
-		move |_, ()| async move {
-			let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
-
-			let slot =
-				sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_duration(
-					*timestamp,
-					slot_duration,
-				);
-
-			Ok((timestamp, slot))
-		},
-		&task_manager.spawn_essential_handle(),
-		config.prometheus_registry(),
-		sp_consensus::CanAuthorWithNativeVersion::new(client.executor().clone()),
-		telemetry.as_ref().map(|x| x.handle()),
-	)?;
-
-	let justification_stream = grandpa_link.justification_stream();
-	let shared_authority_set = grandpa_link.shared_authority_set().clone();
-	let shared_voter_state = sc_finality_grandpa::SharedVoterState::empty();
-
-	let (signed_commitment_sender, signed_commitment_stream) =
-		beefy_gadget::notification::BeefySignedCommitmentStream::channel();
-
-	let import_setup = (block_import, grandpa_link, babe_link, signed_commitment_sender);
-	let rpc_setup = shared_voter_state.clone();
-
-	let slot_duration = babe_config.slot_duration();
-
-	let rpc_extensions_builder = {
-		let client = client.clone();
-		let transaction_pool = transaction_pool.clone();
-		let backend = backend.clone();
-
-		move |deny_unsafe,
-		      subscription_executor: sc_rpc::SubscriptionTaskExecutor|
-		      -> Result<jsonrpc_core::IoHandler<sc_service::RpcMetadata>, sc_service::Error> {
-			use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApi};
-			use sc_finality_grandpa_rpc::{GrandpaApi, GrandpaRpcHandler};
-			use substrate_frame_rpc_system::{FullSystem, SystemApi};
-
-			let backend = backend.clone();
-			let client = client.clone();
-			let pool = transaction_pool.clone();
-
-			let shared_voter_state = shared_voter_state.clone();
-
-			let finality_proof_provider = GrandpaFinalityProofProvider::new_for_service(
-				backend,
-				Some(shared_authority_set.clone()),
-			);
-
-			let mut io = jsonrpc_core::IoHandler::default();
-			io.extend_with(SystemApi::to_delegate(FullSystem::new(
-				client.clone(),
-				pool,
-				deny_unsafe,
-			)));
-			io.extend_with(TransactionPaymentApi::to_delegate(TransactionPayment::new(
-				client.clone(),
-			)));
-			io.extend_with(GrandpaApi::to_delegate(GrandpaRpcHandler::new(
-				shared_authority_set.clone(),
-				shared_voter_state,
-				justification_stream.clone(),
-				subscription_executor.clone(),
-				finality_proof_provider,
-			)));
-			io.extend_with(beefy_gadget_rpc::BeefyApi::to_delegate(
-				beefy_gadget_rpc::BeefyRpcHandler::new(
-					signed_commitment_stream.clone(),
-					subscription_executor,
-				),
-			));
-			io.extend_with(pallet_mmr_rpc::MmrApi::to_delegate(pallet_mmr_rpc::Mmr::new(client)));
-
-			Ok(io)
-		}
-	};
-
-	Ok(sc_service::PartialComponents {
-		client,
-		backend,
-		task_manager,
-		keystore_container,
-		select_chain,
-		import_queue,
-		transaction_pool,
-		other: (rpc_extensions_builder, import_setup, rpc_setup, slot_duration, telemetry),
-	})
-}
-
-pub struct NewFull<C> {
-	pub task_manager: TaskManager,
-	pub client: C,
-	pub overseer_handle: Option<Handle>,
-	pub network: Arc<sc_network::NetworkService<Block, <Block as BlockT>::Hash>>,
-	pub rpc_handlers: sc_service::RpcHandlers,
-	pub backend: Arc<FullBackend>,
-}
-
-/// The maximum number of active leaves we forward to the [`Overseer`] on start up.
-const MAX_ACTIVE_LEAVES: usize = 4;
-
-/// Returns the active leaves the overseer should start with.
-async fn active_leaves(
-	select_chain: &sc_consensus::LongestChain<FullBackend, Block>,
-	client: &FullClient,
-) -> Result<Vec<BlockInfo>, Error>
-where
-	RuntimeApi: ConstructRuntimeApi<Block, FullClient> + Send + Sync + 'static,
-	<RuntimeApi as ConstructRuntimeApi<Block, FullClient>>::RuntimeApi:
-		RuntimeApiCollection<StateBackend = sc_client_api::StateBackendFor<FullBackend, Block>>,
-	ExecutorDispatch: NativeExecutionDispatch + 'static,
-{
-	let best_block = select_chain.best_chain().await?;
-
-	let mut leaves = select_chain
-		.leaves()
-		.await
-		.unwrap_or_default()
-		.into_iter()
-		.filter_map(|hash| {
-			let number = client.number(hash).ok()??;
-
-			// Only consider leaves that are in maximum an uncle of the best block.
-			if number < best_block.number().saturating_sub(1) || hash == best_block.hash() {
-				return None
-			}
-
-			let parent_hash = client.header(&BlockId::Hash(hash)).ok()??.parent_hash;
-
-			Some(BlockInfo { hash, parent_hash, number })
-		})
-		.collect::<Vec<_>>();
-
-	// Sort by block number and get the maximum number of leaves
-	leaves.sort_by_key(|b| b.number);
-
-	leaves.push(BlockInfo {
-		hash: best_block.hash(),
-		parent_hash: *best_block.parent_hash(),
-		number: *best_block.number(),
-	});
-
-	Ok(leaves.into_iter().rev().take(MAX_ACTIVE_LEAVES).collect())
-}
-
-// Create a new full node.
-pub fn new_full(
-	mut config: Configuration,
-	program_path: Option<std::path::PathBuf>,
-	overseer_gen: impl OverseerGen,
-) -> Result<NewFull<Arc<FullClient>>, Error>
-where
-	RuntimeApi: ConstructRuntimeApi<Block, FullClient> + Send + Sync + 'static,
-	<RuntimeApi as ConstructRuntimeApi<Block, FullClient>>::RuntimeApi:
-		RuntimeApiCollection<StateBackend = sc_client_api::StateBackendFor<FullBackend, Block>>,
-	ExecutorDispatch: NativeExecutionDispatch + 'static,
-{
-	let is_collator = false;
-
-	let role = config.role.clone();
-	let force_authoring = config.force_authoring;
-	let backoff_authoring_blocks =
-		Some(sc_consensus_slots::BackoffAuthoringOnFinalizedHeadLagging::default());
-
-	let disable_grandpa = config.disable_grandpa;
-	let name = config.network.node_name.clone();
-
-	let sc_service::PartialComponents {
-		client,
-		backend,
-		mut task_manager,
-		keystore_container,
-		select_chain,
-		import_queue,
-		transaction_pool,
-		other: (rpc_extensions_builder, import_setup, rpc_setup, slot_duration, mut telemetry),
-	} = new_partial(&mut config)?;
-
-	let prometheus_registry = config.prometheus_registry().cloned();
-
-	let overseer_connector = OverseerConnector::default();
-
-	let shared_voter_state = rpc_setup;
-	let auth_disc_publish_non_global_ips = config.network.allow_non_globals_in_dht;
-
-	// Note: GrandPa is pushed before the Polkadot-specific protocols. This doesn't change
-	// anything in terms of behaviour, but makes the logs more consistent with the other
-	// Substrate nodes.
-	config.network.extra_sets.push(sc_finality_grandpa::grandpa_peers_set_config());
-
-	config.network.extra_sets.push(beefy_gadget::beefy_peers_set_config());
-
-	{
-		use polkadot_network_bridge::{peer_sets_info, IsAuthority};
-		let is_authority = if role.is_authority() { IsAuthority::Yes } else { IsAuthority::No };
-		config.network.extra_sets.extend(peer_sets_info(is_authority));
-	}
-
-	let (pov_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-	let (chunk_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-	let (collation_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-	let (available_data_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-	let (statement_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-	let (dispute_req_receiver, cfg) = IncomingRequest::get_config_receiver();
-	config.network.request_response_protocols.push(cfg);
-
-	let warp_sync = Arc::new(sc_finality_grandpa::warp_proof::NetworkProvider::new(
-		backend.clone(),
-		import_setup.1.shared_authority_set().clone(),
-		vec![],
-	));
-
-	let (network, system_rpc_tx, network_starter) =
-		sc_service::build_network(sc_service::BuildNetworkParams {
-			config: &config,
-			client: client.clone(),
-			transaction_pool: transaction_pool.clone(),
-			spawn_handle: task_manager.spawn_handle(),
-			import_queue,
-			block_announce_validator_builder: None,
-			warp_sync: Some(warp_sync),
-		})?;
-
-	if config.offchain_worker.enabled {
-		let _ = sc_service::build_offchain_workers(
-			&config,
-			task_manager.spawn_handle(),
-			client.clone(),
-			network.clone(),
-		);
-	}
-
-	let parachains_db = crate::parachains_db::open_creating(
-		config.database.path().ok_or(Error::DatabasePathRequired)?.into(),
-		crate::parachains_db::CacheSizes::default(),
-	)?;
-
-	let availability_config = AvailabilityConfig {
-		col_data: crate::parachains_db::REAL_COLUMNS.col_availability_data,
-		col_meta: crate::parachains_db::REAL_COLUMNS.col_availability_meta,
-	};
-
-	let approval_voting_config = ApprovalVotingConfig {
-		col_data: crate::parachains_db::REAL_COLUMNS.col_approval_data,
-		slot_duration_millis: slot_duration.as_millis() as u64,
-	};
-
-
-	let candidate_validation_config = CandidateValidationConfig {
-		artifacts_cache_path: config
-			.database
-			.path()
-			.ok_or(Error::DatabasePathRequired)?
-			.join("pvf-artifacts"),
-		program_path: match program_path {
-			None => std::env::current_exe()?,
-			Some(p) => p,
-		},
-	};
-
-	let chain_selection_config = ChainSelectionConfig {
-		col_data: crate::parachains_db::REAL_COLUMNS.col_chain_selection_data,
-		stagnant_check_interval: polkadot_node_core_chain_selection::StagnantCheckInterval::never(),
-	};
-
-	let dispute_coordinator_config = DisputeCoordinatorConfig {
-		col_data: crate::parachains_db::REAL_COLUMNS.col_dispute_coordinator_data,
-	};
-
-	let rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams {
-		config,
-		backend: backend.clone(),
-		client: client.clone(),
-		keystore: keystore_container.sync_keystore(),
-		network: network.clone(),
-		rpc_extensions_builder: Box::new(rpc_extensions_builder),
-		transaction_pool: transaction_pool.clone(),
-		task_manager: &mut task_manager,
-		system_rpc_tx,
-		telemetry: telemetry.as_mut(),
-	})?;
-
-	let (block_import, link_half, babe_link, signed_commitment_sender) = import_setup;
-
-	let overseer_client = client.clone();
-	let spawner = task_manager.spawn_handle();
-	let active_leaves = futures::executor::block_on(active_leaves(&select_chain, &*client))?;
-
-	let authority_discovery_service = if role.is_authority() || is_collator {
-		use futures::StreamExt;
-		use sc_network::Event;
-
-		let authority_discovery_role = if role.is_authority() {
-			sc_authority_discovery::Role::PublishAndDiscover(keystore_container.keystore())
-		} else {
-			// don't publish our addresses when we're only a collator
-			sc_authority_discovery::Role::Discover
-		};
-		let dht_event_stream =
-			network.event_stream("authority-discovery").filter_map(|e| async move {
-				match e {
-					Event::Dht(e) => Some(e),
-					_ => None,
-				}
-			});
-		let (worker, service) = sc_authority_discovery::new_worker_and_service_with_config(
-			sc_authority_discovery::WorkerConfig {
-				publish_non_global_ips: auth_disc_publish_non_global_ips,
-				..Default::default()
-			},
-			client.clone(),
-			network.clone(),
-			Box::pin(dht_event_stream),
-			authority_discovery_role,
-			prometheus_registry.clone(),
-		);
-
-		task_manager
-			.spawn_handle()
-			.spawn("authority-discovery-worker", None, worker.run());
-		Some(service)
-	} else {
-		None
-	};
-
-	// we'd say let overseer_handler =
-	// authority_discovery_service.map(|authority_discovery_service|, ...), but in that case we
-	// couldn't use ? to propagate errors
-	let local_keystore = keystore_container.local_keystore();
-	let maybe_params =
-		local_keystore.and_then(move |k| authority_discovery_service.map(|a| (a, k)));
-
-	let overseer_handle = if let Some((authority_discovery_service, keystore)) = maybe_params {
-		let (overseer, overseer_handle) = overseer_gen
-			.generate::<sc_service::SpawnTaskHandle, FullClient>(
-				overseer_connector,
-				OverseerGenArgs {
-					leaves: active_leaves,
-					keystore,
-					runtime_client: overseer_client.clone(),
-					parachains_db,
-					availability_config,
-					approval_voting_config,
-					network_service: network.clone(),
-					authority_discovery_service,
-					registry: prometheus_registry.as_ref(),
-					spawner,
-					candidate_validation_config,
-					available_data_req_receiver,
-					chain_selection_config,
-					chunk_req_receiver,
-					collation_req_receiver,
-					dispute_coordinator_config,
-					dispute_req_receiver,
-					pov_req_receiver,
-					statement_req_receiver,
-					disputes_enabled: false,
-				},
-			)?;
-		let handle = Handle::new(overseer_handle);
-
-		{
-			let handle = handle.clone();
-			task_manager.spawn_essential_handle().spawn_blocking(
-				"overseer",
-				None,
-				Box::pin(async move {
-					use futures::{pin_mut, select, FutureExt};
-
-					let forward = polkadot_overseer::forward_events(overseer_client, handle);
-
-					let forward = forward.fuse();
-					let overseer_fut = overseer.run().fuse();
-
-					pin_mut!(overseer_fut);
-					pin_mut!(forward);
-
-					select! {
-						_ = forward => (),
-						_ = overseer_fut => (),
-						complete => (),
-					}
-				}),
-			);
-		}
-
-		Some(handle)
-	} else {
-		None
-	};
-
-	if role.is_authority() {
-		let can_author_with =
-			sp_consensus::CanAuthorWithNativeVersion::new(client.executor().clone());
-
-		let proposer = sc_basic_authorship::ProposerFactory::new(
-			task_manager.spawn_handle(),
-			client.clone(),
-			transaction_pool,
-			prometheus_registry.as_ref(),
-			telemetry.as_ref().map(|x| x.handle()),
-		);
-
-		let client_clone = client.clone();
-		let overseer_handle =
-			overseer_handle.as_ref().ok_or(Error::AuthoritiesRequireRealOverseer)?.clone();
-		let slot_duration = babe_link.config().slot_duration();
-		let babe_config = sc_consensus_babe::BabeParams {
-			keystore: keystore_container.sync_keystore(),
-			client: client.clone(),
-			select_chain,
-			block_import,
-			env: proposer,
-			sync_oracle: network.clone(),
-			justification_sync_link: network.clone(),
-			create_inherent_data_providers: move |parent, ()| {
-				let client_clone = client_clone.clone();
-				let overseer_handle = overseer_handle.clone();
-				async move {
-					let parachain = polkadot_node_core_parachains_inherent::ParachainsInherentDataProvider::create(
-						&*client_clone,
-						overseer_handle,
-						parent,
-					)
-					.await
-					.map_err(Box::new)?;
-
-					let uncles = sc_consensus_uncles::create_uncles_inherent_data_provider(
-						&*client_clone,
-						parent,
-					)?;
-
-					let timestamp = sp_timestamp::InherentDataProvider::from_system_time();
-
-					let slot = sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_duration(
-						*timestamp,
-						slot_duration,
-					);
-
-					Ok((timestamp, slot, uncles, parachain))
-				}
-			},
-			force_authoring,
-			backoff_authoring_blocks,
-			babe_link,
-			can_author_with,
-			block_proposal_slot_portion: sc_consensus_babe::SlotProportion::new(2f32 / 3f32),
-			max_block_proposal_slot_portion: None,
-			telemetry: telemetry.as_ref().map(|x| x.handle()),
-		};
-
-		let babe = sc_consensus_babe::start_babe(babe_config)?;
-		task_manager.spawn_essential_handle().spawn_blocking("babe", None, babe);
-	}
-
-	// if the node isn't actively participating in consensus then it doesn't
-	// need a keystore, regardless of which protocol we use below.
-	let keystore_opt =
-		if role.is_authority() { Some(keystore_container.sync_keystore()) } else { None };
-
-	let beefy_params = beefy_gadget::BeefyParams {
-		client: client.clone(),
-		backend: backend.clone(),
-		key_store: keystore_opt.clone(),
-		network: network.clone(),
-		signed_commitment_sender,
-		min_block_delta: 2,
-		prometheus_registry: prometheus_registry.clone(),
-	};
-
-	// Start the BEEFY bridge gadget.
-	task_manager.spawn_essential_handle().spawn_blocking(
-		"beefy-gadget",
-		None,
-		beefy_gadget::start_beefy_gadget::<_, _, _, _>(beefy_params),
-	);
-
-	let config = sc_finality_grandpa::Config {
-		// FIXME substrate#1578 make this available through chainspec
-		gossip_duration: Duration::from_millis(1000),
-		justification_period: 512,
-		name: Some(name),
-		observer_enabled: false,
-		keystore: keystore_opt,
-		local_role: role,
-		telemetry: telemetry.as_ref().map(|x| x.handle()),
-	};
-
-	let enable_grandpa = !disable_grandpa;
-	if enable_grandpa {
-		// start the full GRANDPA voter
-		// NOTE: unlike in substrate we are currently running the full
-		// GRANDPA voter protocol for all full nodes (regardless of whether
-		// they're validators or not). at this point the full voter should
-		// provide better guarantees of block and vote data availability than
-		// the observer.
-
-		// add a custom voting rule to temporarily stop voting for new blocks
-		// after the given pause block is finalized and restarting after the
-		// given delay.
-		let builder = sc_finality_grandpa::VotingRulesBuilder::default();
-
-		let voting_rule = builder.build();
-		let grandpa_config = sc_finality_grandpa::GrandpaParams {
-			config,
-			link: link_half,
-			network: network.clone(),
-			voting_rule,
-			prometheus_registry,
-			shared_voter_state,
-			telemetry: telemetry.as_ref().map(|x| x.handle()),
-		};
-
-		task_manager.spawn_essential_handle().spawn_blocking(
-			"grandpa-voter",
-			None,
-			sc_finality_grandpa::run_grandpa_voter(grandpa_config)?,
-		);
-	}
-
-	network_starter.start_network();
-
-	Ok(NewFull { task_manager, client, overseer_handle, network, rpc_handlers, backend })
-}
-
-pub fn build_full(
-	config: Configuration,
-	overseer_gen: impl OverseerGen,
-) -> Result<NewFull<Arc<FullClient>>, Error> {
-	new_full(config, None, overseer_gen)
-}
diff --git a/polkadot/bridges/bin/rialto/runtime/Cargo.toml b/polkadot/bridges/bin/rialto/runtime/Cargo.toml
index 8298cdfbfbe835344f54af58fabbb27c2098469e..59b9a8e9b575537c04461961af4f8584f81b18aa 100644
--- a/polkadot/bridges/bin/rialto/runtime/Cargo.toml
+++ b/polkadot/bridges/bin/rialto/runtime/Cargo.toml
@@ -2,17 +2,17 @@
 name = "rialto-runtime"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false, features = ["derive"] }
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
 hex-literal = "0.3"
 libsecp256k1 = { version = "0.7", optional = true, default-features = false, features = ["hmac"] }
 log = { version = "0.4.14", default-features = false }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true, features = ["derive"] }
 
 # Bridge dependencies
@@ -73,7 +73,9 @@ polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot", bran
 polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false }
 
 [dev-dependencies]
+bridge-runtime-common = { path = "../../runtime-common", features = ["integrity-test"] }
 libsecp256k1 = { version = "0.7", features = ["hmac"] }
+static_assertions = "1.1"
 
 [build-dependencies]
 substrate-wasm-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/bin/rialto/runtime/src/lib.rs b/polkadot/bridges/bin/rialto/runtime/src/lib.rs
index b306c6eb7ebfd58447153c06af2bd182c918184a..612e1ebb5b4ccc3df9d03e4ce6ca05ec9d51b181 100644
--- a/polkadot/bridges/bin/rialto/runtime/src/lib.rs
+++ b/polkadot/bridges/bin/rialto/runtime/src/lib.rs
@@ -51,7 +51,7 @@ use sp_runtime::{
 	create_runtime_str, generic, impl_opaque_keys,
 	traits::{AccountIdLookup, Block as BlockT, Keccak256, NumberFor, OpaqueKeys},
 	transaction_validity::{TransactionSource, TransactionValidity},
-	ApplyExtrinsicResult, FixedPointNumber, MultiSignature, MultiSigner, Perquintill,
+	ApplyExtrinsicResult, FixedPointNumber, FixedU128, MultiSignature, MultiSigner, Perquintill,
 };
 use sp_std::{collections::btree_map::BTreeMap, prelude::*};
 #[cfg(feature = "std")]
@@ -140,6 +140,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	impl_version: 1,
 	apis: RUNTIME_API_VERSIONS,
 	transaction_version: 1,
+	state_version: 1,
 };
 
 /// The version information used to identify this runtime when compiled natively.
@@ -399,21 +400,7 @@ parameter_types! {
 	/// Note that once this is hit the pallet will essentially throttle incoming requests down to one
 	/// call per block.
 	pub const MaxRequests: u32 = 50;
-}
 
-#[cfg(feature = "runtime-benchmarks")]
-parameter_types! {
-	/// Number of headers to keep in benchmarks.
-	///
-	/// In benchmarks we always populate with full number of `HeadersToKeep` to make sure that
-	/// pruning is taken into account.
-	///
-	/// Note: This is lower than regular value, to speed up benchmarking setup.
-	pub const HeadersToKeep: u32 = 1024;
-}
-
-#[cfg(not(feature = "runtime-benchmarks"))]
-parameter_types! {
 	/// Number of headers to keep.
 	///
 	/// Assuming the worst case of every header being finalized, we will keep headers at least for a
@@ -426,7 +413,7 @@ impl pallet_bridge_grandpa::Config for Runtime {
 	type BridgedChain = bp_millau::Millau;
 	type MaxRequests = MaxRequests;
 	type HeadersToKeep = HeadersToKeep;
-	type WeightInfo = pallet_bridge_grandpa::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_grandpa::weights::MillauWeight<Runtime>;
 }
 
 impl pallet_shift_session_manager::Config for Runtime {}
@@ -434,9 +421,9 @@ impl pallet_shift_session_manager::Config for Runtime {}
 parameter_types! {
 	pub const MaxMessagesToPruneAtOnce: bp_messages::MessageNonce = 8;
 	pub const MaxUnrewardedRelayerEntriesAtInboundLane: bp_messages::MessageNonce =
-		bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE;
+		bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
 	pub const MaxUnconfirmedMessagesAtInboundLane: bp_messages::MessageNonce =
-		bp_rialto::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE;
+		bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
 	// `IdentityFee` is used by Rialto => we may use weight directly
 	pub const GetDeliveryConfirmationTransactionFee: Balance =
 		bp_rialto::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT as _;
@@ -449,7 +436,7 @@ pub type WithMillauMessagesInstance = ();
 
 impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 	type Event = Event;
-	type WeightInfo = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_messages::weights::MillauWeight<Runtime>;
 	type Parameter = millau_messages::RialtoToMillauMessagesParameter;
 	type MaxMessagesToPruneAtOnce = MaxMessagesToPruneAtOnce;
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
@@ -469,10 +456,9 @@ impl pallet_bridge_messages::Config<WithMillauMessagesInstance> for Runtime {
 	type MessageDeliveryAndDispatchPayment =
 		pallet_bridge_messages::instant_payments::InstantCurrencyPayments<
 			Runtime,
-			(),
+			WithMillauMessagesInstance,
 			pallet_balances::Pallet<Runtime>,
 			GetDeliveryConfirmationTransactionFee,
-			RootAccountForPayments,
 		>;
 	type OnMessageAccepted = ();
 	type OnDeliveryConfirmed = ();
@@ -568,7 +554,7 @@ pub type Executive = frame_executive::Executive<
 	Block,
 	frame_system::ChainContext<Runtime>,
 	Runtime,
-	AllPallets,
+	AllPalletsWithSystem,
 >;
 
 #[cfg(feature = "runtime-benchmarks")]
@@ -633,7 +619,7 @@ impl_runtime_apis! {
 	}
 
 	impl beefy_primitives::BeefyApi<Block> for Runtime {
-		fn validator_set() -> ValidatorSet<BeefyId> {
+		fn validator_set() -> Option<ValidatorSet<BeefyId>> {
 			Beefy::validator_set()
 		}
 	}
@@ -676,10 +662,6 @@ impl_runtime_apis! {
 			let header = BridgeMillauGrandpa::best_finalized();
 			(header.number, header.hash())
 		}
-
-		fn is_known_header(hash: bp_millau::Hash) -> bool {
-			BridgeMillauGrandpa::is_known_header(hash)
-		}
 	}
 
 	impl sp_transaction_pool::runtime_api::TaggedTransactionQueue<Block> for Runtime {
@@ -752,10 +734,7 @@ impl_runtime_apis! {
 			polkadot_runtime_parachains::runtime_api_impl::v2::validators::<Runtime>()
 		}
 
-		fn validator_groups() -> (
-			Vec<Vec<polkadot_primitives::v2::ValidatorIndex>>,
-			polkadot_primitives::v2::GroupRotationInfo<BlockNumber>,
-		) {
+		fn validator_groups() -> (Vec<Vec<polkadot_primitives::v2::ValidatorIndex>>, polkadot_primitives::v2::GroupRotationInfo<BlockNumber>) {
 			polkadot_runtime_parachains::runtime_api_impl::v2::validator_groups::<Runtime>()
 		}
 
@@ -763,10 +742,7 @@ impl_runtime_apis! {
 			polkadot_runtime_parachains::runtime_api_impl::v2::availability_cores::<Runtime>()
 		}
 
-		fn persisted_validation_data(
-			para_id: polkadot_primitives::v2::Id,
-			assumption: polkadot_primitives::v2::OccupiedCoreAssumption,
-		)
+		fn persisted_validation_data(para_id: polkadot_primitives::v2::Id, assumption: polkadot_primitives::v2::OccupiedCoreAssumption)
 			-> Option<polkadot_primitives::v2::PersistedValidationData<Hash, BlockNumber>> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::persisted_validation_data::<Runtime>(para_id, assumption)
 		}
@@ -775,7 +751,10 @@ impl_runtime_apis! {
 			para_id: polkadot_primitives::v2::Id,
 			expected_persisted_validation_data_hash: Hash,
 		) -> Option<(polkadot_primitives::v2::PersistedValidationData<Hash, BlockNumber>, polkadot_primitives::v2::ValidationCodeHash)> {
-			polkadot_runtime_parachains::runtime_api_impl::v2::assumed_validation_data::<Runtime>(para_id, expected_persisted_validation_data_hash)
+			polkadot_runtime_parachains::runtime_api_impl::v2::assumed_validation_data::<Runtime>(
+				para_id,
+				expected_persisted_validation_data_hash,
+			)
 		}
 
 		fn check_validation_outputs(
@@ -789,17 +768,12 @@ impl_runtime_apis! {
 			polkadot_runtime_parachains::runtime_api_impl::v2::session_index_for_child::<Runtime>()
 		}
 
-		fn validation_code(
-			para_id: polkadot_primitives::v2::Id,
-			assumption: polkadot_primitives::v2::OccupiedCoreAssumption,
-		)
+		fn validation_code(para_id: polkadot_primitives::v2::Id, assumption: polkadot_primitives::v2::OccupiedCoreAssumption)
 			-> Option<polkadot_primitives::v2::ValidationCode> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::validation_code::<Runtime>(para_id, assumption)
 		}
 
-		fn candidate_pending_availability(
-			para_id: polkadot_primitives::v2::Id,
-		) -> Option<polkadot_primitives::v2::CommittedCandidateReceipt<Hash>> {
+		fn candidate_pending_availability(para_id: polkadot_primitives::v2::Id) -> Option<polkadot_primitives::v2::CommittedCandidateReceipt<Hash>> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::candidate_pending_availability::<Runtime>(para_id)
 		}
 
@@ -818,9 +792,7 @@ impl_runtime_apis! {
 			polkadot_runtime_parachains::runtime_api_impl::v2::session_info::<Runtime>(index)
 		}
 
-		fn dmq_contents(
-			recipient: polkadot_primitives::v2::Id,
-		) -> Vec<polkadot_primitives::v2::InboundDownwardMessage<BlockNumber>> {
+		fn dmq_contents(recipient: polkadot_primitives::v2::Id) -> Vec<polkadot_primitives::v2::InboundDownwardMessage<BlockNumber>> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::dmq_contents::<Runtime>(recipient)
 		}
 
@@ -830,15 +802,27 @@ impl_runtime_apis! {
 			polkadot_runtime_parachains::runtime_api_impl::v2::inbound_hrmp_channels_contents::<Runtime>(recipient)
 		}
 
-		fn validation_code_by_hash(
-			hash: polkadot_primitives::v2::ValidationCodeHash,
-		) -> Option<polkadot_primitives::v2::ValidationCode> {
+		fn validation_code_by_hash(hash: polkadot_primitives::v2::ValidationCodeHash) -> Option<polkadot_primitives::v2::ValidationCode> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::validation_code_by_hash::<Runtime>(hash)
 		}
 
 		fn on_chain_votes() -> Option<polkadot_primitives::v2::ScrapedOnChainVotes<Hash>> {
 			polkadot_runtime_parachains::runtime_api_impl::v2::on_chain_votes::<Runtime>()
 		}
+
+		fn submit_pvf_check_statement(stmt: polkadot_primitives::v2::PvfCheckStatement, signature: polkadot_primitives::v2::ValidatorSignature) {
+			polkadot_runtime_parachains::runtime_api_impl::v2::submit_pvf_check_statement::<Runtime>(stmt, signature)
+		}
+
+		fn pvfs_require_precheck() -> Vec<polkadot_primitives::v2::ValidationCodeHash> {
+			polkadot_runtime_parachains::runtime_api_impl::v2::pvfs_require_precheck::<Runtime>()
+		}
+
+		fn validation_code_hash(para_id: polkadot_primitives::v2::Id, assumption: polkadot_primitives::v2::OccupiedCoreAssumption)
+			-> Option<polkadot_primitives::v2::ValidationCodeHash>
+		{
+			polkadot_runtime_parachains::runtime_api_impl::v2::validation_code_hash::<Runtime>(para_id, assumption)
+		}
 	}
 
 	impl sp_authority_discovery::AuthorityDiscoveryApi<Block> for Runtime {
@@ -910,10 +894,12 @@ impl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			_lane_id: bp_messages::LaneId,
 			payload: ToMillauMessagePayload,
+			millau_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<Balance> {
 			estimate_message_dispatch_and_delivery_fee::<WithMillauMessageBridge>(
 				&payload,
 				WithMillauMessageBridge::RELAYER_FEE_PERCENT,
+				millau_to_this_conversion_rate,
 			).ok()
 		}
 
@@ -928,243 +914,6 @@ impl_runtime_apis! {
 				WithMillauMessageBridge,
 			>(lane, begin, end)
 		}
-
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeMillauMessages::outbound_latest_received_nonce(lane)
-		}
-
-		fn latest_generated_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeMillauMessages::outbound_latest_generated_nonce(lane)
-		}
-	}
-
-	impl bp_millau::FromMillauInboundLaneApi<Block> for Runtime {
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeMillauMessages::inbound_latest_received_nonce(lane)
-		}
-
-		fn latest_confirmed_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeMillauMessages::inbound_latest_confirmed_nonce(lane)
-		}
-
-		fn unrewarded_relayers_state(lane: bp_messages::LaneId) -> bp_messages::UnrewardedRelayersState {
-			BridgeMillauMessages::inbound_unrewarded_relayers_state(lane)
-		}
-	}
-
-	#[cfg(feature = "runtime-benchmarks")]
-	impl frame_benchmarking::Benchmark<Block> for Runtime {
-		fn benchmark_metadata(extra: bool) -> (
-			Vec<frame_benchmarking::BenchmarkList>,
-			Vec<frame_support::traits::StorageInfo>,
-		) {
-			use frame_benchmarking::{Benchmarking, BenchmarkList};
-			use frame_support::traits::StorageInfoTrait;
-
-			use pallet_bridge_messages::benchmarking::Pallet as MessagesBench;
-
-			let mut list = Vec::<BenchmarkList>::new();
-			list_benchmarks!(list, extra);
-
-			let storage_info = AllPalletsWithSystem::storage_info();
-			return (list, storage_info)
-		}
-
-		fn dispatch_benchmark(
-			config: frame_benchmarking::BenchmarkConfig,
-		) -> Result<
-			Vec<frame_benchmarking::BenchmarkBatch>,
-			sp_runtime::RuntimeString,
-		> {
-			use frame_benchmarking::{Benchmarking, BenchmarkBatch, TrackedStorageKey};
-			use frame_support::traits::StorageInfoTrait;
-
-			let whitelist: Vec<TrackedStorageKey> = vec![
-				// Block Number
-				hex_literal::hex!("26aa394eea5630e07c48ae0c9558cef702a5c1b19ab7a04f536c519aca4983ac").to_vec().into(),
-				// Execution Phase
-				hex_literal::hex!("26aa394eea5630e07c48ae0c9558cef7ff553b5a9862a516939d82b3d3d8661a").to_vec().into(),
-				// Event Count
-				hex_literal::hex!("26aa394eea5630e07c48ae0c9558cef70a98fdbe9ce6c55837576c60c7af3850").to_vec().into(),
-				// System Events
-				hex_literal::hex!("26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7").to_vec().into(),
-				// Caller 0 Account
-				hex_literal::hex!("26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da946c154ffd9992e395af90b5b13cc6f295c77033fce8a9045824a6690bbf99c6db269502f0a8d1d2a008542d5690a0749").to_vec().into(),
-			];
-
-			let mut batches = Vec::<BenchmarkBatch>::new();
-			let params = (&config, &whitelist);
-
-			use crate::millau_messages::{ToMillauMessagePayload, WithMillauMessageBridge};
-			use bp_runtime::messages::DispatchFeePayment;
-			use bridge_runtime_common::messages;
-			use pallet_bridge_messages::benchmarking::{
-				Pallet as MessagesBench,
-				Config as MessagesConfig,
-				MessageDeliveryProofParams,
-				MessageParams,
-				MessageProofParams,
-				ProofSize as MessagesProofSize,
-			};
-
-			impl MessagesConfig<WithMillauMessagesInstance> for Runtime {
-				fn maximal_message_size() -> u32 {
-					messages::source::maximal_message_size::<WithMillauMessageBridge>()
-				}
-
-				fn bridged_relayer_id() -> Self::InboundRelayer {
-					Default::default()
-				}
-
-				fn account_balance(account: &Self::AccountId) -> Self::OutboundMessageFee {
-					pallet_balances::Pallet::<Runtime>::free_balance(account)
-				}
-
-				fn endow_account(account: &Self::AccountId) {
-					pallet_balances::Pallet::<Runtime>::make_free_balance_be(
-						account,
-						Balance::MAX / 100,
-					);
-				}
-
-				fn prepare_outbound_message(
-					params: MessageParams<Self::AccountId>,
-				) -> (millau_messages::ToMillauMessagePayload, Balance) {
-					let message_payload = vec![0; params.size as usize];
-					let dispatch_origin = bp_message_dispatch::CallOrigin::SourceAccount(
-						params.sender_account,
-					);
-
-					let message = ToMillauMessagePayload {
-						spec_version: 0,
-						weight: params.size as _,
-						origin: dispatch_origin,
-						call: message_payload,
-						dispatch_fee_payment: DispatchFeePayment::AtSourceChain,
-					};
-					(message, pallet_bridge_messages::benchmarking::MESSAGE_FEE.into())
-				}
-
-				fn prepare_message_proof(
-					params: MessageProofParams,
-				) -> (millau_messages::FromMillauMessagesProof, Weight) {
-					use crate::millau_messages::WithMillauMessageBridge;
-					use bp_messages::MessageKey;
-					use bridge_runtime_common::{
-						messages::MessageBridge,
-						messages_benchmarking::{ed25519_sign, prepare_message_proof},
-					};
-					use codec::Encode;
-					use frame_support::weights::GetDispatchInfo;
-					use pallet_bridge_messages::storage_keys;
-					use sp_runtime::traits::{Header, IdentifyAccount};
-
-					let remark = match params.size {
-						MessagesProofSize::Minimal(ref size) => vec![0u8; *size as _],
-						_ => vec![],
-					};
-					let call = Call::System(SystemCall::remark { remark });
-					let call_weight = call.get_dispatch_info().weight;
-
-					let millau_account_id: bp_millau::AccountId = Default::default();
-					let (rialto_raw_public, rialto_raw_signature) = ed25519_sign(
-						&call,
-						&millau_account_id,
-						VERSION.spec_version,
-						bp_runtime::MILLAU_CHAIN_ID,
-						bp_runtime::RIALTO_CHAIN_ID,
-					);
-					let rialto_public = MultiSigner::Ed25519(sp_core::ed25519::Public::from_raw(rialto_raw_public));
-					let rialto_signature = MultiSignature::Ed25519(sp_core::ed25519::Signature::from_raw(
-						rialto_raw_signature,
-					));
-
-					if params.dispatch_fee_payment == DispatchFeePayment::AtTargetChain {
-						Self::endow_account(&rialto_public.clone().into_account());
-					}
-
-					let make_millau_message_key = |message_key: MessageKey| storage_keys::message_key(
-						<WithMillauMessageBridge as MessageBridge>::BRIDGED_MESSAGES_PALLET_NAME,
-						&message_key.lane_id, message_key.nonce,
-					).0;
-					let make_millau_outbound_lane_data_key = |lane_id| storage_keys::outbound_lane_data_key(
-						<WithMillauMessageBridge as MessageBridge>::BRIDGED_MESSAGES_PALLET_NAME,
-						&lane_id,
-					).0;
-
-					let make_millau_header = |state_root| bp_millau::Header::new(
-						0,
-						Default::default(),
-						state_root,
-						Default::default(),
-						Default::default(),
-					);
-
-					let dispatch_fee_payment = params.dispatch_fee_payment.clone();
-					prepare_message_proof::<WithMillauMessageBridge, bp_millau::Hasher, Runtime, (), _, _, _>(
-						params,
-						make_millau_message_key,
-						make_millau_outbound_lane_data_key,
-						make_millau_header,
-						call_weight,
-						bp_message_dispatch::MessagePayload {
-							spec_version: VERSION.spec_version,
-							weight: call_weight,
-							origin: bp_message_dispatch::CallOrigin::<
-								bp_millau::AccountId,
-								MultiSigner,
-								Signature,
-							>::TargetAccount(
-								millau_account_id,
-								rialto_public,
-								rialto_signature,
-							),
-							dispatch_fee_payment,
-							call: call.encode(),
-						}.encode(),
-					)
-				}
-
-				fn prepare_message_delivery_proof(
-					params: MessageDeliveryProofParams<Self::AccountId>,
-				) -> millau_messages::ToMillauMessagesDeliveryProof {
-					use crate::millau_messages::WithMillauMessageBridge;
-					use bridge_runtime_common::{messages_benchmarking::prepare_message_delivery_proof};
-					use sp_runtime::traits::Header;
-
-					prepare_message_delivery_proof::<WithMillauMessageBridge, bp_millau::Hasher, Runtime, (), _, _>(
-						params,
-						|lane_id| pallet_bridge_messages::storage_keys::inbound_lane_data_key(
-							<WithMillauMessageBridge as MessageBridge>::BRIDGED_MESSAGES_PALLET_NAME,
-							&lane_id,
-						).0,
-						|state_root| bp_millau::Header::new(
-							0,
-							Default::default(),
-							state_root,
-							Default::default(),
-							Default::default(),
-						),
-					)
-				}
-
-				fn is_message_dispatched(nonce: bp_messages::MessageNonce) -> bool {
-					frame_system::Pallet::<Runtime>::events()
-						.into_iter()
-						.map(|event_record| event_record.event)
-						.any(|event| matches!(
-							event,
-							Event::BridgeDispatch(pallet_bridge_dispatch::Event::<Runtime, _>::MessageDispatched(
-								_, ([0, 0, 0, 0], nonce_from_event), _,
-							)) if nonce_from_event == nonce
-						))
-				}
-			}
-
-			add_benchmarks!(params,	batches);
-
-			Ok(batches)
-		}
 	}
 }
 
@@ -1195,55 +944,22 @@ where
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use bridge_runtime_common::messages;
 
 	#[test]
-	fn ensure_rialto_message_lane_weights_are_correct() {
-		type Weights = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
-
-		pallet_bridge_messages::ensure_weights_are_correct::<Weights>(
-			bp_rialto::DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT,
-			bp_rialto::ADDITIONAL_MESSAGE_BYTE_DELIVERY_WEIGHT,
-			bp_rialto::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
-			bp_rialto::PAY_INBOUND_DISPATCH_FEE_WEIGHT,
-			DbWeight::get(),
-		);
-
-		let max_incoming_message_proof_size = bp_millau::EXTRA_STORAGE_PROOF_SIZE.saturating_add(
-			messages::target::maximal_incoming_message_size(bp_rialto::max_extrinsic_size()),
-		);
-		pallet_bridge_messages::ensure_able_to_receive_message::<Weights>(
-			bp_rialto::max_extrinsic_size(),
-			bp_rialto::max_extrinsic_weight(),
-			max_incoming_message_proof_size,
-			messages::target::maximal_incoming_message_dispatch_weight(
-				bp_rialto::max_extrinsic_weight(),
-			),
+	fn call_size() {
+		const BRIDGES_PALLETS_MAX_CALL_SIZE: usize = 200;
+		assert!(
+			core::mem::size_of::<pallet_bridge_grandpa::Call<Runtime>>() <=
+				BRIDGES_PALLETS_MAX_CALL_SIZE
 		);
-
-		let max_incoming_inbound_lane_data_proof_size =
-			bp_messages::InboundLaneData::<()>::encoded_size_hint(
-				bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-				bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE as _,
-				bp_millau::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE as _,
-			)
-			.unwrap_or(u32::MAX);
-		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
-			bp_rialto::max_extrinsic_size(),
-			bp_rialto::max_extrinsic_weight(),
-			max_incoming_inbound_lane_data_proof_size,
-			bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-			bp_millau::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-			DbWeight::get(),
+		assert!(
+			core::mem::size_of::<pallet_bridge_messages::Call<Runtime>>() <=
+				BRIDGES_PALLETS_MAX_CALL_SIZE
 		);
-	}
-
-	#[test]
-	fn call_size() {
-		const DOT_MAX_CALL_SZ: usize = 230;
-		assert!(core::mem::size_of::<pallet_bridge_grandpa::Call<Runtime>>() <= DOT_MAX_CALL_SZ);
-		// FIXME: get this down to 230. https://github.com/paritytech/grandpa-bridge-gadget/issues/359
-		const BEEFY_MAX_CALL_SZ: usize = 232;
-		assert!(core::mem::size_of::<pallet_bridge_messages::Call<Runtime>>() <= BEEFY_MAX_CALL_SZ);
+		// Largest inner Call is `pallet_session::Call` with a size of 224 bytes. This size is a
+		// result of large `SessionKeys` struct.
+		// Total size of Rialto runtime Call is 232.
+		const MAX_CALL_SIZE: usize = 232;
+		assert!(core::mem::size_of::<Call>() <= MAX_CALL_SIZE);
 	}
 }
diff --git a/polkadot/bridges/bin/rialto/runtime/src/millau_messages.rs b/polkadot/bridges/bin/rialto/runtime/src/millau_messages.rs
index 13a1c6b06ec21a215b9d16599540c816a1c8b23b..44348383f1d52e897c52fca2ffea8a6362010aa2 100644
--- a/polkadot/bridges/bin/rialto/runtime/src/millau_messages.rs
+++ b/polkadot/bridges/bin/rialto/runtime/src/millau_messages.rs
@@ -19,11 +19,11 @@
 use crate::Runtime;
 
 use bp_messages::{
-	source_chain::TargetHeaderChain,
+	source_chain::{SenderOrigin, TargetHeaderChain},
 	target_chain::{ProvedMessages, SourceHeaderChain},
 	InboundLaneData, LaneId, Message, MessageNonce, Parameter as MessagesParameter,
 };
-use bp_runtime::{ChainId, MILLAU_CHAIN_ID, RIALTO_CHAIN_ID};
+use bp_runtime::{Chain, ChainId, MILLAU_CHAIN_ID, RIALTO_CHAIN_ID};
 use bridge_runtime_common::messages::{self, MessageBridge, MessageTransaction};
 use codec::{Decode, Encode};
 use frame_support::{
@@ -86,16 +86,19 @@ impl MessageBridge for WithMillauMessageBridge {
 	const RELAYER_FEE_PERCENT: u32 = 10;
 	const THIS_CHAIN_ID: ChainId = RIALTO_CHAIN_ID;
 	const BRIDGED_CHAIN_ID: ChainId = MILLAU_CHAIN_ID;
-	const BRIDGED_MESSAGES_PALLET_NAME: &'static str = bp_millau::WITH_RIALTO_MESSAGES_PALLET_NAME;
+	const BRIDGED_MESSAGES_PALLET_NAME: &'static str = bp_rialto::WITH_RIALTO_MESSAGES_PALLET_NAME;
 
 	type ThisChain = Rialto;
 	type BridgedChain = Millau;
 
-	fn bridged_balance_to_this_balance(bridged_balance: bp_millau::Balance) -> bp_rialto::Balance {
-		bp_rialto::Balance::try_from(
-			MillauToRialtoConversionRate::get().saturating_mul_int(bridged_balance),
-		)
-		.unwrap_or(bp_rialto::Balance::MAX)
+	fn bridged_balance_to_this_balance(
+		bridged_balance: bp_millau::Balance,
+		bridged_to_this_conversion_rate_override: Option<FixedU128>,
+	) -> bp_rialto::Balance {
+		let conversion_rate = bridged_to_this_conversion_rate_override
+			.unwrap_or_else(|| MillauToRialtoConversionRate::get());
+		bp_rialto::Balance::try_from(conversion_rate.saturating_mul_int(bridged_balance))
+			.unwrap_or(bp_rialto::Balance::MAX)
 	}
 }
 
@@ -113,10 +116,11 @@ impl messages::ChainWithMessages for Rialto {
 }
 
 impl messages::ThisChainWithMessages for Rialto {
+	type Origin = crate::Origin;
 	type Call = crate::Call;
 
-	fn is_outbound_lane_enabled(lane: &LaneId) -> bool {
-		*lane == [0, 0, 0, 0] || *lane == [0, 0, 0, 1]
+	fn is_message_accepted(send_origin: &Self::Origin, lane: &LaneId) -> bool {
+		send_origin.linked_account().is_some() && (*lane == [0, 0, 0, 0] || *lane == [0, 0, 0, 1])
 	}
 
 	fn maximal_pending_messages_at_outbound_lane() -> MessageNonce {
@@ -170,13 +174,13 @@ impl messages::ChainWithMessages for Millau {
 
 impl messages::BridgedChainWithMessages for Millau {
 	fn maximal_extrinsic_size() -> u32 {
-		bp_millau::max_extrinsic_size()
+		bp_millau::Millau::max_extrinsic_size()
 	}
 
 	fn message_weight_limits(_message_payload: &[u8]) -> RangeInclusive<Weight> {
 		// we don't want to relay too large messages + keep reserve for future upgrades
 		let upper_limit = messages::target::maximal_incoming_message_dispatch_weight(
-			bp_millau::max_extrinsic_weight(),
+			bp_millau::Millau::max_extrinsic_weight(),
 		);
 
 		// we're charging for payload bytes in `WithMillauMessageBridge::transaction_payment`
@@ -272,6 +276,19 @@ impl SourceHeaderChain<bp_millau::Balance> for Millau {
 	}
 }
 
+impl SenderOrigin<crate::AccountId> for crate::Origin {
+	fn linked_account(&self) -> Option<crate::AccountId> {
+		match self.caller {
+			crate::OriginCaller::system(frame_system::RawOrigin::Signed(ref submitter)) =>
+				Some(submitter.clone()),
+			crate::OriginCaller::system(frame_system::RawOrigin::Root) |
+			crate::OriginCaller::system(frame_system::RawOrigin::None) =>
+				crate::RootAccountForPayments::get(),
+			_ => None,
+		}
+	}
+}
+
 /// Rialto -> Millau message lane pallet parameters.
 #[derive(RuntimeDebug, Clone, Encode, Decode, PartialEq, Eq, TypeInfo)]
 pub enum RialtoToMillauMessagesParameter {
@@ -291,15 +308,23 @@ impl MessagesParameter for RialtoToMillauMessagesParameter {
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use crate::{AccountId, Call, ExistentialDeposit, Runtime, SystemCall, SystemConfig, VERSION};
+	use crate::{
+		AccountId, Call, DbWeight, ExistentialDeposit, MillauGrandpaInstance, Runtime, SystemCall,
+		SystemConfig, WithMillauMessagesInstance, VERSION,
+	};
 	use bp_message_dispatch::CallOrigin;
 	use bp_messages::{
 		target_chain::{DispatchMessage, DispatchMessageData, MessageDispatch},
 		MessageKey,
 	};
-	use bp_runtime::{derive_account_id, messages::DispatchFeePayment, SourceAccount};
-	use bridge_runtime_common::messages::target::{
-		FromBridgedChainEncodedMessageCall, FromBridgedChainMessagePayload,
+	use bp_runtime::{derive_account_id, messages::DispatchFeePayment, Chain, SourceAccount};
+	use bridge_runtime_common::{
+		assert_complete_bridge_types,
+		integrity::{
+			assert_complete_bridge_constants, AssertBridgeMessagesPalletConstants,
+			AssertBridgePalletNames, AssertChainConstants, AssertCompleteBridgeConstants,
+		},
+		messages::target::{FromBridgedChainEncodedMessageCall, FromBridgedChainMessagePayload},
 	};
 	use frame_support::{
 		traits::Currency,
@@ -316,7 +341,7 @@ mod tests {
 			SystemConfig::default().build_storage::<Runtime>().unwrap().into();
 		ext.execute_with(|| {
 			let bridge = MILLAU_CHAIN_ID;
-			let call: Call = SystemCall::remark { remark: vec![] }.into();
+			let call: Call = SystemCall::set_heap_pages { pages: 64 }.into();
 			let dispatch_weight = call.get_dispatch_info().weight;
 			let dispatch_fee = <Runtime as pallet_transaction_payment::Config>::WeightToFee::calc(
 				&dispatch_weight,
@@ -377,4 +402,157 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn ensure_rialto_message_lane_weights_are_correct() {
+		type Weights = pallet_bridge_messages::weights::MillauWeight<Runtime>;
+
+		pallet_bridge_messages::ensure_weights_are_correct::<Weights>(
+			bp_rialto::DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT,
+			bp_rialto::ADDITIONAL_MESSAGE_BYTE_DELIVERY_WEIGHT,
+			bp_rialto::MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT,
+			bp_rialto::PAY_INBOUND_DISPATCH_FEE_WEIGHT,
+			DbWeight::get(),
+		);
+
+		let max_incoming_message_proof_size = bp_millau::EXTRA_STORAGE_PROOF_SIZE.saturating_add(
+			messages::target::maximal_incoming_message_size(bp_rialto::Rialto::max_extrinsic_size()),
+		);
+		pallet_bridge_messages::ensure_able_to_receive_message::<Weights>(
+			bp_rialto::Rialto::max_extrinsic_size(),
+			bp_rialto::Rialto::max_extrinsic_weight(),
+			max_incoming_message_proof_size,
+			messages::target::maximal_incoming_message_dispatch_weight(
+				bp_rialto::Rialto::max_extrinsic_weight(),
+			),
+		);
+
+		let max_incoming_inbound_lane_data_proof_size =
+			bp_messages::InboundLaneData::<()>::encoded_size_hint(
+				bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
+				bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as _,
+				bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as _,
+			)
+			.unwrap_or(u32::MAX);
+		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
+			bp_rialto::Rialto::max_extrinsic_size(),
+			bp_rialto::Rialto::max_extrinsic_weight(),
+			max_incoming_inbound_lane_data_proof_size,
+			bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+			bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
+			DbWeight::get(),
+		);
+	}
+
+	#[test]
+	fn ensure_bridge_integrity() {
+		assert_complete_bridge_types!(
+			runtime: Runtime,
+			with_bridged_chain_grandpa_instance: MillauGrandpaInstance,
+			with_bridged_chain_messages_instance: WithMillauMessagesInstance,
+			bridge: WithMillauMessageBridge,
+			this_chain: bp_rialto::Rialto,
+			bridged_chain: bp_millau::Millau,
+			this_chain_account_id_converter: bp_rialto::AccountIdConverter
+		);
+
+		assert_complete_bridge_constants::<
+			Runtime,
+			MillauGrandpaInstance,
+			WithMillauMessagesInstance,
+			WithMillauMessageBridge,
+			bp_rialto::Rialto,
+		>(AssertCompleteBridgeConstants {
+			this_chain_constants: AssertChainConstants {
+				block_length: bp_rialto::BlockLength::get(),
+				block_weights: bp_rialto::BlockWeights::get(),
+			},
+			messages_pallet_constants: AssertBridgeMessagesPalletConstants {
+				max_unrewarded_relayers_in_bridged_confirmation_tx:
+					bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+				max_unconfirmed_messages_in_bridged_confirmation_tx:
+					bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
+				bridged_chain_id: bp_runtime::MILLAU_CHAIN_ID,
+			},
+			pallet_names: AssertBridgePalletNames {
+				with_this_chain_messages_pallet_name: bp_rialto::WITH_RIALTO_MESSAGES_PALLET_NAME,
+				with_bridged_chain_grandpa_pallet_name: bp_millau::WITH_MILLAU_GRANDPA_PALLET_NAME,
+				with_bridged_chain_messages_pallet_name:
+					bp_millau::WITH_MILLAU_MESSAGES_PALLET_NAME,
+			},
+		});
+
+		assert_eq!(
+			MillauToRialtoConversionRate::key().to_vec(),
+			bp_runtime::storage_parameter_key(
+				bp_rialto::MILLAU_TO_RIALTO_CONVERSION_RATE_PARAMETER_NAME
+			)
+			.0,
+		);
+	}
+
+	#[test]
+	#[ignore]
+	fn no_stack_overflow_when_decoding_nested_call_during_dispatch() {
+		// this test is normally ignored, because it only makes sense to run it in release mode
+
+		let mut ext: sp_io::TestExternalities =
+			SystemConfig::default().build_storage::<Runtime>().unwrap().into();
+		ext.execute_with(|| {
+			let bridge = MILLAU_CHAIN_ID;
+
+			let mut call: Call = SystemCall::set_heap_pages { pages: 64 }.into();
+
+			for _i in 0..3000 {
+				call = Call::Sudo(pallet_sudo::Call::sudo { call: Box::new(call) });
+			}
+
+			let dispatch_weight = 500;
+			let dispatch_fee = <Runtime as pallet_transaction_payment::Config>::WeightToFee::calc(
+				&dispatch_weight,
+			);
+			assert!(dispatch_fee > 0);
+
+			// create relayer account with minimal balance
+			let relayer_account: AccountId = [1u8; 32].into();
+			let initial_amount = ExistentialDeposit::get();
+			let _ = <pallet_balances::Pallet<Runtime> as Currency<AccountId>>::deposit_creating(
+				&relayer_account,
+				initial_amount,
+			);
+
+			// create dispatch account with minimal balance + dispatch fee
+			let dispatch_account = derive_account_id::<
+				<Runtime as pallet_bridge_dispatch::Config>::SourceChainAccountId,
+			>(bridge, SourceAccount::Root);
+			let dispatch_account =
+				<Runtime as pallet_bridge_dispatch::Config>::AccountIdConverter::convert(
+					dispatch_account,
+				);
+			let _ = <pallet_balances::Pallet<Runtime> as Currency<AccountId>>::deposit_creating(
+				&dispatch_account,
+				initial_amount + dispatch_fee,
+			);
+
+			// dispatch message with intention to pay dispatch fee at the target chain
+			//
+			// this is where the stack overflow has happened before the fix has been applied
+			FromMillauMessageDispatch::dispatch(
+				&relayer_account,
+				DispatchMessage {
+					key: MessageKey { lane_id: Default::default(), nonce: 0 },
+					data: DispatchMessageData {
+						payload: Ok(FromBridgedChainMessagePayload::<WithMillauMessageBridge> {
+							spec_version: VERSION.spec_version,
+							weight: dispatch_weight,
+							origin: CallOrigin::SourceRoot,
+							dispatch_fee_payment: DispatchFeePayment::AtTargetChain,
+							call: FromBridgedChainEncodedMessageCall::new(call.encode()),
+						}),
+						fee: 1,
+					},
+				},
+			);
+		});
+	}
 }
diff --git a/polkadot/bridges/bin/rialto/runtime/src/parachains.rs b/polkadot/bridges/bin/rialto/runtime/src/parachains.rs
index 60c5cf449ca224dbfb6b78418ee7112a0e0d1975..20a9aeb28c0dfed04731d5d26343213c08ad3112 100644
--- a/polkadot/bridges/bin/rialto/runtime/src/parachains.rs
+++ b/polkadot/bridges/bin/rialto/runtime/src/parachains.rs
@@ -16,7 +16,10 @@
 
 //! Parachains support in Rialto runtime.
 
-use crate::{AccountId, Balance, Balances, BlockNumber, Event, Origin, Registrar, Runtime, Slots};
+use crate::{
+	AccountId, Babe, Balance, Balances, BlockNumber, Call, Event, Origin, Registrar, Runtime,
+	Slots, UncheckedExtrinsic,
+};
 
 use frame_support::{parameter_types, weights::Weight};
 use frame_system::EnsureRoot;
@@ -29,6 +32,15 @@ use polkadot_runtime_parachains::{
 	paras_inherent as parachains_paras_inherent, scheduler as parachains_scheduler,
 	session_info as parachains_session_info, shared as parachains_shared, ump as parachains_ump,
 };
+use sp_runtime::transaction_validity::TransactionPriority;
+
+impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
+where
+	Call: From<C>,
+{
+	type Extrinsic = UncheckedExtrinsic;
+	type OverarchingCall = Call;
+}
 
 /// Special `RewardValidators` that does nothing ;)
 pub struct RewardValidators;
@@ -49,6 +61,7 @@ impl parachains_hrmp::Config for Runtime {
 	type Event = Event;
 	type Origin = Origin;
 	type Currency = Balances;
+	type WeightInfo = parachains_hrmp::TestWeightInfo;
 }
 
 impl parachains_inclusion::Config for Runtime {
@@ -65,10 +78,15 @@ impl parachains_initializer::Config for Runtime {
 
 impl parachains_origin::Config for Runtime {}
 
+parameter_types! {
+	pub const ParasUnsignedPriority: TransactionPriority = TransactionPriority::max_value();
+}
+
 impl parachains_paras::Config for Runtime {
-	type Origin = Origin;
 	type Event = Event;
 	type WeightInfo = parachains_paras::TestWeightInfo;
+	type UnsignedPriority = ParasUnsignedPriority;
+	type NextSessionRotation = Babe;
 }
 
 impl parachains_paras_inherent::Config for Runtime {
@@ -90,6 +108,7 @@ impl parachains_ump::Config for Runtime {
 	type UmpSink = ();
 	type FirstMessageFactorPercent = FirstMessageFactorPercent;
 	type ExecuteOverweightOrigin = EnsureRoot<AccountId>;
+	type WeightInfo = parachains_ump::TestWeightInfo;
 }
 
 // required onboarding pallets. We're not going to use auctions or crowdloans, so they're missing
@@ -120,6 +139,7 @@ impl slots::Config for Runtime {
 	type LeasePeriod = LeasePeriod;
 	type WeightInfo = slots::TestWeightInfo;
 	type LeaseOffset = ();
+	type ForceOrigin = EnsureRoot<AccountId>;
 }
 
 impl paras_sudo_wrapper::Config for Runtime {}
diff --git a/polkadot/bridges/bin/runtime-common/Cargo.toml b/polkadot/bridges/bin/runtime-common/Cargo.toml
index c7885f617482bd34d1d31b93250f341f0204c64a..d1ec30c0aa4b027c0fd62b8e13fff1912f99ebe9 100644
--- a/polkadot/bridges/bin/runtime-common/Cargo.toml
+++ b/polkadot/bridges/bin/runtime-common/Cargo.toml
@@ -2,7 +2,7 @@
 name = "bridge-runtime-common"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 homepage = "https://substrate.dev"
 repository = "https://github.com/paritytech/parity-bridges-common/"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
@@ -11,7 +11,8 @@ license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
 ed25519-dalek = { version = "1.0", default-features = false, optional = true }
 hash-db = { version = "0.15.2", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
+static_assertions = { version = "1.1", optional = true }
 
 # Bridge dependencies
 
@@ -25,12 +26,16 @@ pallet-bridge-messages = { path = "../../modules/messages", default-features = f
 # Substrate dependencies
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
 pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-state-machine = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-version = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
 
 [features]
 default = ["std"]
@@ -40,6 +45,7 @@ std = [
 	"bp-runtime/std",
 	"codec/std",
 	"frame-support/std",
+	"frame-system/std",
 	"hash-db/std",
 	"scale-info/std",
 	"pallet-bridge-dispatch/std",
@@ -47,6 +53,7 @@ std = [
 	"pallet-bridge-messages/std",
 	"pallet-transaction-payment/std",
 	"scale-info/std",
+	"sp-api/std",
 	"sp-core/std",
 	"sp-runtime/std",
 	"sp-state-machine/std",
@@ -55,7 +62,12 @@ std = [
 ]
 runtime-benchmarks = [
 	"ed25519-dalek/u64_backend",
+	"pallet-balances",
 	"pallet-bridge-grandpa/runtime-benchmarks",
 	"pallet-bridge-messages/runtime-benchmarks",
 	"sp-state-machine",
+	"sp-version",
+]
+integrity-test = [
+	"static_assertions",
 ]
diff --git a/polkadot/bridges/bin/runtime-common/README.md b/polkadot/bridges/bin/runtime-common/README.md
index 38a47bfdcc9d9f72da8c94770e33be1c2890af11..5f2298cd787d3f0dd0e35eecc28ba877b83a80c9 100644
--- a/polkadot/bridges/bin/runtime-common/README.md
+++ b/polkadot/bridges/bin/runtime-common/README.md
@@ -62,8 +62,11 @@ corresponding chain. There is single exception, though (it may be changed in the
 
 This trait represents this chain from bridge point of view. Let's review every method of this trait:
 
-- `ThisChainWithMessages::is_outbound_lane_enabled`: is used to check whether given lane accepts
-  outbound messages.
+- `ThisChainWithMessages::is_message_accepted`: is used to check whether given lane accepts
+  messages. The send-message origin is passed to the function, so you may e.g. verify that only
+  given pallet is able to send messages over selected lane. **IMPORTANT**: if you assume that the
+  message must be paid by the sender, you must ensure that the sender origin has linked the account
+  for paying message delivery and dispatch fee.
 
 - `ThisChainWithMessages::maximal_pending_messages_at_outbound_lane`: you should return maximal
   number of pending (undelivered) messages from this function. Returning small values would require
diff --git a/polkadot/bridges/bin/runtime-common/src/integrity.rs b/polkadot/bridges/bin/runtime-common/src/integrity.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ab517566a0fe6e882802b1cb0b77bd1f5d4503bd
--- /dev/null
+++ b/polkadot/bridges/bin/runtime-common/src/integrity.rs
@@ -0,0 +1,331 @@
+// 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/>.
+
+//! Integrity tests for chain constants and pallets configuration.
+//!
+//! Most of the tests in this module assume that the bridge is using standard (see `crate::messages`
+//! module for details) configuration.
+
+use crate::messages::MessageBridge;
+
+use bp_messages::MessageNonce;
+use bp_runtime::{Chain, ChainId};
+use codec::Encode;
+use frame_support::{storage::generator::StorageValue, traits::Get};
+use frame_system::limits;
+
+/// Macro that ensures that the runtime configuration and chain primitives crate are sharing
+/// the same types (index, block number, hash, hasher, account id and header).
+#[macro_export]
+macro_rules! assert_chain_types(
+	( runtime: $r:path, this_chain: $this:path ) => {
+		{
+			// if one of asserts fail, then either bridge isn't configured properly (or alternatively - non-standard
+			// configuration is used), or something has broke existing configuration (meaning that all bridged chains
+			// and relays will stop functioning)
+			use frame_system::Config as SystemConfig;
+			use static_assertions::assert_type_eq_all;
+
+			assert_type_eq_all!(<$r as SystemConfig>::Index, bp_runtime::IndexOf<$this>);
+			assert_type_eq_all!(<$r as SystemConfig>::BlockNumber, bp_runtime::BlockNumberOf<$this>);
+			assert_type_eq_all!(<$r as SystemConfig>::Hash, bp_runtime::HashOf<$this>);
+			assert_type_eq_all!(<$r as SystemConfig>::Hashing, bp_runtime::HasherOf<$this>);
+			assert_type_eq_all!(<$r as SystemConfig>::AccountId, bp_runtime::AccountIdOf<$this>);
+			assert_type_eq_all!(<$r as SystemConfig>::Header, bp_runtime::HeaderOf<$this>);
+		}
+	}
+);
+
+/// Macro that ensures that the bridge configuration and chain primitives crates are sharing
+/// the same types (hash, account id, ...).
+#[macro_export]
+macro_rules! assert_bridge_types(
+	( bridge: $bridge:path, this_chain: $this:path, bridged_chain: $bridged:path ) => {
+		{
+			// if one of this asserts fail, then all chains, bridged with this chain and bridge relays are now broken
+			//
+			// `frame_support::weights::Weight` is used here directly, because all chains we know are using this
+			// primitive (may be changed in the future)
+			use $crate::messages::{
+				AccountIdOf, BalanceOf, BridgedChain, HashOf, SignatureOf, SignerOf, ThisChain, WeightOf,
+			};
+			use static_assertions::assert_type_eq_all;
+
+			assert_type_eq_all!(HashOf<ThisChain<$bridge>>, bp_runtime::HashOf<$this>);
+			assert_type_eq_all!(AccountIdOf<ThisChain<$bridge>>, bp_runtime::AccountIdOf<$this>);
+			assert_type_eq_all!(SignerOf<ThisChain<$bridge>>, bp_runtime::AccountPublicOf<$this>);
+			assert_type_eq_all!(SignatureOf<ThisChain<$bridge>>, bp_runtime::SignatureOf<$this>);
+			assert_type_eq_all!(WeightOf<ThisChain<$bridge>>, frame_support::weights::Weight);
+			assert_type_eq_all!(BalanceOf<ThisChain<$bridge>>, bp_runtime::BalanceOf<$this>);
+
+			assert_type_eq_all!(HashOf<BridgedChain<$bridge>>, bp_runtime::HashOf<$bridged>);
+			assert_type_eq_all!(AccountIdOf<BridgedChain<$bridge>>, bp_runtime::AccountIdOf<$bridged>);
+			assert_type_eq_all!(SignerOf<BridgedChain<$bridge>>, bp_runtime::AccountPublicOf<$bridged>);
+			assert_type_eq_all!(SignatureOf<BridgedChain<$bridge>>, bp_runtime::SignatureOf<$bridged>);
+			assert_type_eq_all!(WeightOf<BridgedChain<$bridge>>, frame_support::weights::Weight);
+			assert_type_eq_all!(BalanceOf<BridgedChain<$bridge>>, bp_runtime::BalanceOf<$bridged>);
+		}
+	}
+);
+
+/// Macro that ensures that the bridge GRANDPA pallet is configured properly to bridge with given
+/// chain.
+#[macro_export]
+macro_rules! assert_bridge_grandpa_pallet_types(
+	( runtime: $r:path, with_bridged_chain_grandpa_instance: $i:path, bridged_chain: $bridged:path ) => {
+		{
+			// if one of asserts fail, then either bridge isn't configured properly (or alternatively - non-standard
+			// configuration is used), or something has broke existing configuration (meaning that all bridged chains
+			// and relays will stop functioning)
+			use pallet_bridge_grandpa::Config as GrandpaConfig;
+			use static_assertions::assert_type_eq_all;
+
+			assert_type_eq_all!(<$r as GrandpaConfig<$i>>::BridgedChain, $bridged);
+		}
+	}
+);
+
+/// Macro that ensures that the bridge messages pallet is configured properly to bridge using given
+/// configuration.
+#[macro_export]
+macro_rules! assert_bridge_messages_pallet_types(
+	(
+		runtime: $r:path,
+		with_bridged_chain_messages_instance: $i:path,
+		bridge: $bridge:path,
+		this_chain_account_id_converter: $this_converter:path
+	) => {
+		{
+			// if one of asserts fail, then either bridge isn't configured properly (or alternatively - non-standard
+			// configuration is used), or something has broke existing configuration (meaning that all bridged chains
+			// and relays will stop functioning)
+			use $crate::messages::{
+				source::FromThisChainMessagePayload,
+				target::FromBridgedChainMessagePayload,
+				AccountIdOf, BalanceOf, BridgedChain, ThisChain, WeightOf,
+			};
+			use pallet_bridge_messages::Config as MessagesConfig;
+			use static_assertions::assert_type_eq_all;
+
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::OutboundPayload, FromThisChainMessagePayload<$bridge>);
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::OutboundMessageFee, BalanceOf<ThisChain<$bridge>>);
+
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::InboundPayload, FromBridgedChainMessagePayload<$bridge>);
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::InboundMessageFee, BalanceOf<BridgedChain<$bridge>>);
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::InboundRelayer, AccountIdOf<BridgedChain<$bridge>>);
+
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::AccountIdConverter, $this_converter);
+
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::TargetHeaderChain, BridgedChain<$bridge>);
+			assert_type_eq_all!(<$r as MessagesConfig<$i>>::SourceHeaderChain, BridgedChain<$bridge>);
+		}
+	}
+);
+
+/// Macro that combines four other macro calls - `assert_chain_types`, `assert_bridge_types`,
+/// `assert_bridge_grandpa_pallet_types` and `assert_bridge_messages_pallet_types`. It may be used
+/// at the chain that is implemeting complete standard messages bridge (i.e. with bridge GRANDPA and
+/// messages pallets deployed).
+#[macro_export]
+macro_rules! assert_complete_bridge_types(
+	(
+		runtime: $r:path,
+		with_bridged_chain_grandpa_instance: $gi:path,
+		with_bridged_chain_messages_instance: $mi:path,
+		bridge: $bridge:path,
+		this_chain: $this:path,
+		bridged_chain: $bridged:path,
+		this_chain_account_id_converter: $this_converter:path
+	) => {
+		$crate::assert_chain_types!(runtime: $r, this_chain: $this);
+		$crate::assert_bridge_types!(bridge: $bridge, this_chain: $this, bridged_chain: $bridged);
+		$crate::assert_bridge_grandpa_pallet_types!(
+			runtime: $r,
+			with_bridged_chain_grandpa_instance: $gi,
+			bridged_chain: $bridged
+		);
+		$crate::assert_bridge_messages_pallet_types!(
+			runtime: $r,
+			with_bridged_chain_messages_instance: $mi,
+			bridge: $bridge,
+			this_chain_account_id_converter: $this_converter
+		);
+	}
+);
+
+/// Parameters for asserting chain-related constants.
+#[derive(Debug)]
+pub struct AssertChainConstants {
+	/// Block length limits of the chain.
+	pub block_length: limits::BlockLength,
+	/// Block weight limits of the chain.
+	pub block_weights: limits::BlockWeights,
+}
+
+/// Test that our hardcoded, chain-related constants, are matching chain runtime configuration.
+///
+/// In particular, this test ensures that:
+///
+/// 1) block weight limits are matching;
+/// 2) block size limits are matching.
+pub fn assert_chain_constants<R, C>(params: AssertChainConstants)
+where
+	R: frame_system::Config,
+	C: Chain,
+{
+	// we don't check runtime version here, because in our case we'll be building relay from one
+	// repo and runtime will live in another repo, along with outdated relay version. To avoid
+	// unneeded commits, let's not raise an error in case of version mismatch.
+
+	// if one of following assert fails, it means that we may need to upgrade bridged chain and
+	// relay to use updated constants. If constants are now smaller than before, it may lead to
+	// undeliverable messages.
+
+	// `BlockLength` struct is not implementing `PartialEq`, so we compare encoded values here.
+	assert_eq!(
+		R::BlockLength::get().encode(),
+		params.block_length.encode(),
+		"BlockLength from runtime ({:?}) differ from hardcoded: {:?}",
+		R::BlockLength::get(),
+		params.block_length,
+	);
+	// `BlockWeights` struct is not implementing `PartialEq`, so we compare encoded values here
+	assert_eq!(
+		R::BlockWeights::get().encode(),
+		params.block_weights.encode(),
+		"BlockWeights from runtime ({:?}) differ from hardcoded: {:?}",
+		R::BlockWeights::get(),
+		params.block_weights,
+	);
+}
+
+/// Test that the constants, used in GRANDPA pallet configuration are valid.
+pub fn assert_bridge_grandpa_pallet_constants<R, GI>()
+where
+	R: pallet_bridge_grandpa::Config<GI>,
+	GI: 'static,
+{
+	assert!(
+		R::MaxRequests::get() > 0,
+		"MaxRequests ({}) must be larger than zero",
+		R::MaxRequests::get(),
+	);
+}
+
+/// Parameters for asserting messages pallet constants.
+#[derive(Debug)]
+pub struct AssertBridgeMessagesPalletConstants {
+	/// Maximal number of unrewarded relayer entries in a confirmation transaction at the bridged
+	/// chain.
+	pub max_unrewarded_relayers_in_bridged_confirmation_tx: MessageNonce,
+	/// Maximal number of unconfirmed messages in a confirmation transaction at the bridged chain.
+	pub max_unconfirmed_messages_in_bridged_confirmation_tx: MessageNonce,
+	/// Identifier of the bridged chain.
+	pub bridged_chain_id: ChainId,
+}
+
+/// Test that the constants, used in messages pallet configuration are valid.
+pub fn assert_bridge_messages_pallet_constants<R, MI>(params: AssertBridgeMessagesPalletConstants)
+where
+	R: pallet_bridge_messages::Config<MI>,
+	MI: 'static,
+{
+	assert!(
+		R::MaxMessagesToPruneAtOnce::get() > 0,
+		"MaxMessagesToPruneAtOnce ({}) must be larger than zero",
+		R::MaxMessagesToPruneAtOnce::get(),
+	);
+	assert!(
+		R::MaxUnrewardedRelayerEntriesAtInboundLane::get() <= params.max_unrewarded_relayers_in_bridged_confirmation_tx,
+		"MaxUnrewardedRelayerEntriesAtInboundLane ({}) must be <= than the hardcoded value for bridged chain: {}",
+		R::MaxUnrewardedRelayerEntriesAtInboundLane::get(),
+		params.max_unrewarded_relayers_in_bridged_confirmation_tx,
+	);
+	assert!(
+		R::MaxUnconfirmedMessagesAtInboundLane::get() <= params.max_unconfirmed_messages_in_bridged_confirmation_tx,
+		"MaxUnrewardedRelayerEntriesAtInboundLane ({}) must be <= than the hardcoded value for bridged chain: {}",
+		R::MaxUnconfirmedMessagesAtInboundLane::get(),
+		params.max_unconfirmed_messages_in_bridged_confirmation_tx,
+	);
+	assert_eq!(R::BridgedChainId::get(), params.bridged_chain_id);
+}
+
+/// Parameters for asserting bridge pallet names.
+#[derive(Debug)]
+pub struct AssertBridgePalletNames<'a> {
+	/// Name of the messages pallet, deployed at the bridged chain and used to bridge with this
+	/// chain.
+	pub with_this_chain_messages_pallet_name: &'a str,
+	/// Name of the GRANDPA pallet, deployed at this chain and used to bridge with the bridged
+	/// chain.
+	pub with_bridged_chain_grandpa_pallet_name: &'a str,
+	/// Name of the messages pallet, deployed at this chain and used to bridge with the bridged
+	/// chain.
+	pub with_bridged_chain_messages_pallet_name: &'a str,
+}
+
+/// Tests that bridge pallet names used in `construct_runtime!()` macro call are matching constants
+/// from chain primitives crates.
+pub fn assert_bridge_pallet_names<B, R, GI, MI>(params: AssertBridgePalletNames)
+where
+	B: MessageBridge,
+	R: pallet_bridge_grandpa::Config<GI> + pallet_bridge_messages::Config<MI>,
+	GI: 'static,
+	MI: 'static,
+{
+	assert_eq!(B::BRIDGED_MESSAGES_PALLET_NAME, params.with_this_chain_messages_pallet_name);
+	assert_eq!(
+		pallet_bridge_grandpa::PalletOwner::<R, GI>::storage_value_final_key().to_vec(),
+		bp_runtime::storage_value_key(params.with_bridged_chain_grandpa_pallet_name, "PalletOwner",).0,
+	);
+	assert_eq!(
+		pallet_bridge_messages::PalletOwner::<R, MI>::storage_value_final_key().to_vec(),
+		bp_runtime::storage_value_key(
+			params.with_bridged_chain_messages_pallet_name,
+			"PalletOwner",
+		)
+		.0,
+	);
+}
+
+/// Parameters for asserting complete standard messages bridge.
+#[derive(Debug)]
+pub struct AssertCompleteBridgeConstants<'a> {
+	/// Parameters to assert this chain constants.
+	pub this_chain_constants: AssertChainConstants,
+	/// Parameters to assert messages pallet constants.
+	pub messages_pallet_constants: AssertBridgeMessagesPalletConstants,
+	/// Parameters to assert pallet names constants.
+	pub pallet_names: AssertBridgePalletNames<'a>,
+}
+
+/// All bridge-related constants tests for the complete standard messages bridge (i.e. with bridge
+/// GRANDPA and messages pallets deployed).
+pub fn assert_complete_bridge_constants<R, GI, MI, B, This>(params: AssertCompleteBridgeConstants)
+where
+	R: frame_system::Config
+		+ pallet_bridge_grandpa::Config<GI>
+		+ pallet_bridge_messages::Config<MI>,
+	GI: 'static,
+	MI: 'static,
+	B: MessageBridge,
+	This: Chain,
+{
+	assert_chain_constants::<R, This>(params.this_chain_constants);
+	assert_bridge_grandpa_pallet_constants::<R, GI>();
+	assert_bridge_messages_pallet_constants::<R, MI>(params.messages_pallet_constants);
+	assert_bridge_pallet_names::<B, R, GI, MI>(params.pallet_names);
+}
diff --git a/polkadot/bridges/bin/runtime-common/src/lib.rs b/polkadot/bridges/bin/runtime-common/src/lib.rs
index 66f2c6c3a01f1e8178a73ecdbc67404d90db6ddf..c7fb98aba767dd0eb1b1040c9bafdd036dfbeb77 100644
--- a/polkadot/bridges/bin/runtime-common/src/lib.rs
+++ b/polkadot/bridges/bin/runtime-common/src/lib.rs
@@ -21,3 +21,6 @@
 pub mod messages;
 pub mod messages_api;
 pub mod messages_benchmarking;
+
+#[cfg(feature = "integrity-test")]
+pub mod integrity;
diff --git a/polkadot/bridges/bin/runtime-common/src/messages.rs b/polkadot/bridges/bin/runtime-common/src/messages.rs
index b34cbb85540d49e2169655d324a0aa614c1e6ad4..250b68ceffe2fd7bfa7ddf45905a9b21abb1dae0 100644
--- a/polkadot/bridges/bin/runtime-common/src/messages.rs
+++ b/polkadot/bridges/bin/runtime-common/src/messages.rs
@@ -22,7 +22,7 @@
 
 use bp_message_dispatch::MessageDispatch as _;
 use bp_messages::{
-	source_chain::{LaneMessageVerifier, Sender},
+	source_chain::LaneMessageVerifier,
 	target_chain::{DispatchMessage, MessageDispatch, ProvedLaneMessages, ProvedMessages},
 	InboundLaneData, LaneId, Message, MessageData, MessageKey, MessageNonce, OutboundLaneData,
 };
@@ -30,7 +30,7 @@ use bp_runtime::{
 	messages::{DispatchFeePayment, MessageDispatchResult},
 	ChainId, Size, StorageProofChecker,
 };
-use codec::{Decode, Encode};
+use codec::{Decode, DecodeLimit, Encode};
 use frame_support::{
 	traits::{Currency, ExistenceRequirement},
 	weights::{Weight, WeightToFeePolynomial},
@@ -70,6 +70,7 @@ pub trait MessageBridge {
 	/// Convert Bridged chain balance into This chain balance.
 	fn bridged_balance_to_this_balance(
 		bridged_balance: BalanceOf<BridgedChain<Self>>,
+		bridged_to_this_conversion_rate_override: Option<FixedU128>,
 	) -> BalanceOf<ThisChain<Self>>;
 }
 
@@ -110,11 +111,13 @@ pub struct MessageTransaction<Weight> {
 
 /// This chain that has `pallet-bridge-messages` and `dispatch` modules.
 pub trait ThisChainWithMessages: ChainWithMessages {
+	/// Call origin on the chain.
+	type Origin;
 	/// Call type on the chain.
 	type Call: Encode + Decode;
 
-	/// Are we accepting any messages to the given lane?
-	fn is_outbound_lane_enabled(lane: &LaneId) -> bool;
+	/// Do we accept message sent by given origin to given lane?
+	fn is_message_accepted(origin: &Self::Origin, lane: &LaneId) -> bool;
 
 	/// Maximal number of pending (not yet delivered) messages at This chain.
 	///
@@ -172,11 +175,13 @@ pub type SignatureOf<C> = <C as ChainWithMessages>::Signature;
 pub type WeightOf<C> = <C as ChainWithMessages>::Weight;
 /// Type of balances that is used on the chain.
 pub type BalanceOf<C> = <C as ChainWithMessages>::Balance;
+/// Type of origin that is used on the chain.
+pub type OriginOf<C> = <C as ThisChainWithMessages>::Origin;
 /// Type of call that is used on this chain.
 pub type CallOf<C> = <C as ThisChainWithMessages>::Call;
 
 /// Raw storage proof type (just raw trie nodes).
-type RawStorageProof = Vec<Vec<u8>>;
+pub type RawStorageProof = Vec<Vec<u8>>;
 
 /// Compute fee of transaction at runtime where regular transaction payment pallet is being used.
 ///
@@ -269,7 +274,8 @@ pub mod source {
 	pub struct FromThisChainMessageVerifier<B>(PhantomData<B>);
 
 	/// The error message returned from LaneMessageVerifier when outbound lane is disabled.
-	pub const OUTBOUND_LANE_DISABLED: &str = "The outbound message lane is disabled.";
+	pub const MESSAGE_REJECTED_BY_OUTBOUND_LANE: &str =
+		"The outbound message lane has rejected the message.";
 	/// The error message returned from LaneMessageVerifier when too many pending messages at the
 	/// lane.
 	pub const TOO_MANY_PENDING_MESSAGES: &str = "Too many pending messages at the lane.";
@@ -280,26 +286,30 @@ pub mod source {
 
 	impl<B>
 		LaneMessageVerifier<
+			OriginOf<ThisChain<B>>,
 			AccountIdOf<ThisChain<B>>,
 			FromThisChainMessagePayload<B>,
 			BalanceOf<ThisChain<B>>,
 		> for FromThisChainMessageVerifier<B>
 	where
 		B: MessageBridge,
+		// matches requirements from the `frame_system::Config::Origin`
+		OriginOf<ThisChain<B>>: Clone
+			+ Into<Result<frame_system::RawOrigin<AccountIdOf<ThisChain<B>>>, OriginOf<ThisChain<B>>>>,
 		AccountIdOf<ThisChain<B>>: PartialEq + Clone,
 	{
 		type Error = &'static str;
 
 		fn verify_message(
-			submitter: &Sender<AccountIdOf<ThisChain<B>>>,
+			submitter: &OriginOf<ThisChain<B>>,
 			delivery_and_dispatch_fee: &BalanceOf<ThisChain<B>>,
 			lane: &LaneId,
 			lane_outbound_data: &OutboundLaneData,
 			payload: &FromThisChainMessagePayload<B>,
 		) -> Result<(), Self::Error> {
 			// reject message if lane is blocked
-			if !ThisChain::<B>::is_outbound_lane_enabled(lane) {
-				return Err(OUTBOUND_LANE_DISABLED)
+			if !ThisChain::<B>::is_message_accepted(submitter, lane) {
+				return Err(MESSAGE_REJECTED_BY_OUTBOUND_LANE)
 			}
 
 			// reject message if there are too many pending messages at this lane
@@ -313,11 +323,29 @@ pub mod source {
 
 			// Do the dispatch-specific check. We assume that the target chain uses
 			// `Dispatch`, so we verify the message accordingly.
-			pallet_bridge_dispatch::verify_message_origin(submitter, payload)
-				.map_err(|_| BAD_ORIGIN)?;
+			let raw_origin_or_err: Result<
+				frame_system::RawOrigin<AccountIdOf<ThisChain<B>>>,
+				OriginOf<ThisChain<B>>,
+			> = submitter.clone().into();
+			match raw_origin_or_err {
+				Ok(raw_origin) =>
+					pallet_bridge_dispatch::verify_message_origin(&raw_origin, payload)
+						.map(drop)
+						.map_err(|_| BAD_ORIGIN)?,
+				Err(_) => {
+					// so what it means that we've failed to convert origin to the
+					// `frame_system::RawOrigin`? now it means that the custom pallet origin has
+					// been used to send the message. Do we need to verify it? The answer is no,
+					// because pallet may craft any origin (e.g. root) && we can't verify whether it
+					// is valid, or not.
+				},
+			};
 
-			let minimal_fee_in_this_tokens =
-				estimate_message_dispatch_and_delivery_fee::<B>(payload, B::RELAYER_FEE_PERCENT)?;
+			let minimal_fee_in_this_tokens = estimate_message_dispatch_and_delivery_fee::<B>(
+				payload,
+				B::RELAYER_FEE_PERCENT,
+				None,
+			)?;
 
 			// compare with actual fee paid
 			if *delivery_and_dispatch_fee < minimal_fee_in_this_tokens {
@@ -371,6 +399,7 @@ pub mod source {
 	pub fn estimate_message_dispatch_and_delivery_fee<B: MessageBridge>(
 		payload: &FromThisChainMessagePayload<B>,
 		relayer_fee_percent: u32,
+		bridged_to_this_conversion_rate: Option<FixedU128>,
 	) -> Result<BalanceOf<ThisChain<B>>, &'static str> {
 		// the fee (in Bridged tokens) of all transactions that are made on the Bridged chain
 		//
@@ -391,8 +420,11 @@ pub mod source {
 			ThisChain::<B>::transaction_payment(confirmation_transaction);
 
 		// minimal fee (in This tokens) is a sum of all required fees
-		let minimal_fee = B::bridged_balance_to_this_balance(delivery_transaction_fee)
-			.checked_add(&confirmation_transaction_fee);
+		let minimal_fee = B::bridged_balance_to_this_balance(
+			delivery_transaction_fee,
+			bridged_to_this_conversion_rate,
+		)
+		.checked_add(&confirmation_transaction_fee);
 
 		// before returning, add extra fee that is paid to the relayer (relayer interest)
 		minimal_fee
@@ -428,7 +460,7 @@ pub mod source {
 				// Messages delivery proof is just proof of single storage key read => any error
 				// is fatal.
 				let storage_inbound_lane_data_key =
-					pallet_bridge_messages::storage_keys::inbound_lane_data_key(B::BRIDGED_MESSAGES_PALLET_NAME, &lane);
+					bp_messages::storage_keys::inbound_lane_data_key(B::BRIDGED_MESSAGES_PALLET_NAME, &lane);
 				let raw_inbound_lane_data = storage
 					.read_value(storage_inbound_lane_data_key.0.as_ref())
 					.map_err(|_| "Failed to read inbound lane state from storage proof")?
@@ -513,7 +545,11 @@ pub mod target {
 		for Result<DecodedCall, ()>
 	{
 		fn from(encoded_call: FromBridgedChainEncodedMessageCall<DecodedCall>) -> Self {
-			DecodedCall::decode(&mut &encoded_call.encoded_call[..]).map_err(drop)
+			DecodedCall::decode_with_depth_limit(
+				sp_api::MAX_EXTRINSIC_DEPTH,
+				&mut &encoded_call.encoded_call[..],
+			)
+			.map_err(drop)
 		}
 	}
 
@@ -674,16 +710,15 @@ pub mod target {
 		B: MessageBridge,
 	{
 		fn read_raw_outbound_lane_data(&self, lane_id: &LaneId) -> Option<Vec<u8>> {
-			let storage_outbound_lane_data_key =
-				pallet_bridge_messages::storage_keys::outbound_lane_data_key(
-					B::BRIDGED_MESSAGES_PALLET_NAME,
-					lane_id,
-				);
+			let storage_outbound_lane_data_key = bp_messages::storage_keys::outbound_lane_data_key(
+				B::BRIDGED_MESSAGES_PALLET_NAME,
+				lane_id,
+			);
 			self.storage.read_value(storage_outbound_lane_data_key.0.as_ref()).ok()?
 		}
 
 		fn read_raw_message(&self, message_key: &MessageKey) -> Option<Vec<u8>> {
-			let storage_message_key = pallet_bridge_messages::storage_keys::message_key(
+			let storage_message_key = bp_messages::storage_keys::message_key(
 				B::BRIDGED_MESSAGES_PALLET_NAME,
 				&message_key.lane_id,
 				message_key.nonce,
@@ -799,8 +834,12 @@ mod tests {
 
 		fn bridged_balance_to_this_balance(
 			bridged_balance: BridgedChainBalance,
+			bridged_to_this_conversion_rate_override: Option<FixedU128>,
 		) -> ThisChainBalance {
-			ThisChainBalance(bridged_balance.0 * BRIDGED_CHAIN_TO_THIS_CHAIN_BALANCE_RATE as u32)
+			let conversion_rate = bridged_to_this_conversion_rate_override
+				.map(|r| r.to_float() as u32)
+				.unwrap_or(BRIDGED_CHAIN_TO_THIS_CHAIN_BALANCE_RATE);
+			ThisChainBalance(bridged_balance.0 * conversion_rate)
 		}
 	}
 
@@ -818,7 +857,10 @@ mod tests {
 		type ThisChain = BridgedChain;
 		type BridgedChain = ThisChain;
 
-		fn bridged_balance_to_this_balance(_this_balance: ThisChainBalance) -> BridgedChainBalance {
+		fn bridged_balance_to_this_balance(
+			_this_balance: ThisChainBalance,
+			_bridged_to_this_conversion_rate_override: Option<FixedU128>,
+		) -> BridgedChainBalance {
 			unreachable!()
 		}
 	}
@@ -836,6 +878,18 @@ mod tests {
 		#[codec(index = 84)]
 		Mint,
 	}
+	#[derive(Clone, Debug)]
+	struct ThisChainOrigin(Result<frame_system::RawOrigin<ThisChainAccountId>, ()>);
+
+	impl From<ThisChainOrigin>
+		for Result<frame_system::RawOrigin<ThisChainAccountId>, ThisChainOrigin>
+	{
+		fn from(
+			origin: ThisChainOrigin,
+		) -> Result<frame_system::RawOrigin<ThisChainAccountId>, ThisChainOrigin> {
+			origin.clone().0.map_err(|_| origin)
+		}
+	}
 
 	#[derive(Debug, PartialEq, Decode, Encode)]
 	struct BridgedChainAccountId(u32);
@@ -845,6 +899,18 @@ mod tests {
 	struct BridgedChainSignature(u32);
 	#[derive(Debug, PartialEq, Decode, Encode)]
 	enum BridgedChainCall {}
+	#[derive(Clone, Debug)]
+	struct BridgedChainOrigin;
+
+	impl From<BridgedChainOrigin>
+		for Result<frame_system::RawOrigin<BridgedChainAccountId>, BridgedChainOrigin>
+	{
+		fn from(
+			_origin: BridgedChainOrigin,
+		) -> Result<frame_system::RawOrigin<BridgedChainAccountId>, BridgedChainOrigin> {
+			unreachable!()
+		}
+	}
 
 	macro_rules! impl_wrapped_balance {
 		($name:ident) => {
@@ -922,9 +988,10 @@ mod tests {
 	}
 
 	impl ThisChainWithMessages for ThisChain {
+		type Origin = ThisChainOrigin;
 		type Call = ThisChainCall;
 
-		fn is_outbound_lane_enabled(lane: &LaneId) -> bool {
+		fn is_message_accepted(_send_origin: &Self::Origin, lane: &LaneId) -> bool {
 			lane == TEST_LANE_ID
 		}
 
@@ -982,9 +1049,10 @@ mod tests {
 	}
 
 	impl ThisChainWithMessages for BridgedChain {
+		type Origin = BridgedChainOrigin;
 		type Call = BridgedChainCall;
 
-		fn is_outbound_lane_enabled(_lane: &LaneId) -> bool {
+		fn is_message_accepted(_send_origin: &Self::Origin, _lane: &LaneId) -> bool {
 			unreachable!()
 		}
 
@@ -1096,6 +1164,7 @@ mod tests {
 			source::estimate_message_dispatch_and_delivery_fee::<OnThisChainBridge>(
 				&payload,
 				OnThisChainBridge::RELAYER_FEE_PERCENT,
+				None,
 			),
 			Ok(ThisChainBalance(EXPECTED_MINIMAL_FEE)),
 		);
@@ -1107,6 +1176,7 @@ mod tests {
 			source::estimate_message_dispatch_and_delivery_fee::<OnThisChainBridge>(
 				&payload_with_pay_on_target,
 				OnThisChainBridge::RELAYER_FEE_PERCENT,
+				None,
 			)
 			.expect(
 				"estimate_message_dispatch_and_delivery_fee failed for pay-at-target-chain message",
@@ -1121,7 +1191,7 @@ mod tests {
 		// and now check that the verifier checks the fee
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::Root,
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::Root)),
 				&ThisChainBalance(1),
 				TEST_LANE_ID,
 				&test_lane_outbound_data(),
@@ -1130,7 +1200,7 @@ mod tests {
 			Err(source::TOO_LOW_FEE)
 		);
 		assert!(source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-			&Sender::Root,
+			&ThisChainOrigin(Ok(frame_system::RawOrigin::Root)),
 			&ThisChainBalance(1_000_000),
 			TEST_LANE_ID,
 			&test_lane_outbound_data(),
@@ -1153,7 +1223,7 @@ mod tests {
 		// and now check that the verifier checks the fee
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::Signed(ThisChainAccountId(0)),
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::Signed(ThisChainAccountId(0)))),
 				&ThisChainBalance(1_000_000),
 				TEST_LANE_ID,
 				&test_lane_outbound_data(),
@@ -1163,7 +1233,7 @@ mod tests {
 		);
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::None,
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::None)),
 				&ThisChainBalance(1_000_000),
 				TEST_LANE_ID,
 				&test_lane_outbound_data(),
@@ -1172,7 +1242,7 @@ mod tests {
 			Err(source::BAD_ORIGIN)
 		);
 		assert!(source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-			&Sender::Root,
+			&ThisChainOrigin(Ok(frame_system::RawOrigin::Root)),
 			&ThisChainBalance(1_000_000),
 			TEST_LANE_ID,
 			&test_lane_outbound_data(),
@@ -1195,7 +1265,7 @@ mod tests {
 		// and now check that the verifier checks the fee
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::Signed(ThisChainAccountId(0)),
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::Signed(ThisChainAccountId(0)))),
 				&ThisChainBalance(1_000_000),
 				TEST_LANE_ID,
 				&test_lane_outbound_data(),
@@ -1204,7 +1274,7 @@ mod tests {
 			Err(source::BAD_ORIGIN)
 		);
 		assert!(source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-			&Sender::Signed(ThisChainAccountId(1)),
+			&ThisChainOrigin(Ok(frame_system::RawOrigin::Signed(ThisChainAccountId(1)))),
 			&ThisChainBalance(1_000_000),
 			TEST_LANE_ID,
 			&test_lane_outbound_data(),
@@ -1217,13 +1287,13 @@ mod tests {
 	fn message_is_rejected_when_sent_using_disabled_lane() {
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::Root,
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::Root)),
 				&ThisChainBalance(1_000_000),
 				b"dsbl",
 				&test_lane_outbound_data(),
 				&regular_outbound_message_payload(),
 			),
-			Err(source::OUTBOUND_LANE_DISABLED)
+			Err(source::MESSAGE_REJECTED_BY_OUTBOUND_LANE)
 		);
 	}
 
@@ -1231,7 +1301,7 @@ mod tests {
 	fn message_is_rejected_when_there_are_too_many_pending_messages_at_outbound_lane() {
 		assert_eq!(
 			source::FromThisChainMessageVerifier::<OnThisChainBridge>::verify_message(
-				&Sender::Root,
+				&ThisChainOrigin(Ok(frame_system::RawOrigin::Root)),
 				&ThisChainBalance(1_000_000),
 				TEST_LANE_ID,
 				&OutboundLaneData {
@@ -1573,4 +1643,21 @@ mod tests {
 			100 + 50 * 10 + 777,
 		);
 	}
+
+	#[test]
+	fn conversion_rate_override_works() {
+		let payload = regular_outbound_message_payload();
+		let regular_fee = source::estimate_message_dispatch_and_delivery_fee::<OnThisChainBridge>(
+			&payload,
+			OnThisChainBridge::RELAYER_FEE_PERCENT,
+			None,
+		);
+		let overrided_fee = source::estimate_message_dispatch_and_delivery_fee::<OnThisChainBridge>(
+			&payload,
+			OnThisChainBridge::RELAYER_FEE_PERCENT,
+			Some(FixedU128::from_float((BRIDGED_CHAIN_TO_THIS_CHAIN_BALANCE_RATE * 2) as f64)),
+		);
+
+		assert!(regular_fee < overrided_fee);
+	}
 }
diff --git a/polkadot/bridges/bin/runtime-common/src/messages_benchmarking.rs b/polkadot/bridges/bin/runtime-common/src/messages_benchmarking.rs
index de2d0cd1cb600c83bb78b9e7fe8a948966e38b7f..5e20078a2562069d126a41775d9e2a512d461019 100644
--- a/polkadot/bridges/bin/runtime-common/src/messages_benchmarking.rs
+++ b/polkadot/bridges/bin/runtime-common/src/messages_benchmarking.rs
@@ -20,162 +20,188 @@
 #![cfg(feature = "runtime-benchmarks")]
 
 use crate::messages::{
-	source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof,
-	AccountIdOf, BalanceOf, BridgedChain, HashOf, MessageBridge, ThisChain,
+	source::{FromBridgedChainMessagesDeliveryProof, FromThisChainMessagePayload},
+	target::FromBridgedChainMessagesProof,
+	AccountIdOf, BalanceOf, BridgedChain, CallOf, HashOf, MessageBridge, RawStorageProof,
+	SignatureOf, SignerOf, ThisChain,
 };
 
-use bp_messages::{LaneId, MessageData, MessageKey, MessagePayload};
-use bp_runtime::ChainId;
+use bp_messages::{storage_keys, MessageData, MessageKey, MessagePayload};
+use bp_runtime::{messages::DispatchFeePayment, ChainId};
 use codec::Encode;
 use ed25519_dalek::{PublicKey, SecretKey, Signer, KEYPAIR_LENGTH, SECRET_KEY_LENGTH};
-use frame_support::weights::Weight;
+use frame_support::{
+	traits::Currency,
+	weights::{GetDispatchInfo, Weight},
+};
 use pallet_bridge_messages::benchmarking::{
-	MessageDeliveryProofParams, MessageProofParams, ProofSize,
+	MessageDeliveryProofParams, MessageParams, MessageProofParams, ProofSize,
 };
 use sp_core::Hasher;
-use sp_runtime::traits::Header;
-use sp_std::prelude::*;
+use sp_runtime::traits::{Header, IdentifyAccount, MaybeSerializeDeserialize, Zero};
+use sp_std::{fmt::Debug, prelude::*};
 use sp_trie::{record_all_keys, trie_types::TrieDBMutV1, LayoutV1, MemoryDB, Recorder, TrieMut};
+use sp_version::RuntimeVersion;
 
-/// Generate ed25519 signature to be used in
-/// `pallet_brdige_call_dispatch::CallOrigin::TargetAccount`.
-///
-/// Returns public key of the signer and the signature itself.
-pub fn ed25519_sign(
-	target_call: &impl Encode,
-	source_account_id: &impl Encode,
-	target_spec_version: u32,
-	source_chain_id: ChainId,
-	target_chain_id: ChainId,
-) -> ([u8; 32], [u8; 64]) {
+/// Return this chain account, used to dispatch message.
+pub fn dispatch_account<B>() -> AccountIdOf<ThisChain<B>>
+where
+	B: MessageBridge,
+	SignerOf<ThisChain<B>>:
+		From<sp_core::ed25519::Public> + IdentifyAccount<AccountId = AccountIdOf<ThisChain<B>>>,
+{
+	let this_raw_public = PublicKey::from(&dispatch_account_secret());
+	let this_public: SignerOf<ThisChain<B>> =
+		sp_core::ed25519::Public::from_raw(this_raw_public.to_bytes()).into();
+	this_public.into_account()
+}
+
+/// Return public key of this chain account, used to dispatch message.
+pub fn dispatch_account_secret() -> SecretKey {
 	// key from the repo example (https://docs.rs/ed25519-dalek/1.0.1/ed25519_dalek/struct.SecretKey.html)
-	let target_secret = SecretKey::from_bytes(&[
+	SecretKey::from_bytes(&[
 		157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068, 073,
 		197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096,
 	])
-	.expect("harcoded key is valid");
-	let target_public: PublicKey = (&target_secret).into();
-
-	let mut target_pair_bytes = [0u8; KEYPAIR_LENGTH];
-	target_pair_bytes[..SECRET_KEY_LENGTH].copy_from_slice(&target_secret.to_bytes());
-	target_pair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&target_public.to_bytes());
-	let target_pair =
-		ed25519_dalek::Keypair::from_bytes(&target_pair_bytes).expect("hardcoded pair is valid");
+	.expect("harcoded key is valid")
+}
 
-	let signature_message = pallet_bridge_dispatch::account_ownership_digest(
-		target_call,
-		source_account_id,
-		target_spec_version,
-		source_chain_id,
-		target_chain_id,
-	);
-	let target_origin_signature = target_pair
-		.try_sign(&signature_message)
-		.expect("Ed25519 try_sign should not fail in benchmarks");
+/// Prepare outbound message for the `send_message` call.
+pub fn prepare_outbound_message<B>(
+	params: MessageParams<AccountIdOf<ThisChain<B>>>,
+) -> FromThisChainMessagePayload<B>
+where
+	B: MessageBridge,
+	BalanceOf<ThisChain<B>>: From<u64>,
+{
+	let message_payload = vec![0; params.size as usize];
+	let dispatch_origin = bp_message_dispatch::CallOrigin::SourceAccount(params.sender_account);
 
-	(target_public.to_bytes(), target_origin_signature.to_bytes())
+	FromThisChainMessagePayload::<B> {
+		spec_version: 0,
+		weight: params.size as _,
+		origin: dispatch_origin,
+		call: message_payload,
+		dispatch_fee_payment: DispatchFeePayment::AtSourceChain,
+	}
 }
 
 /// Prepare proof of messages for the `receive_messages_proof` call.
-pub fn prepare_message_proof<B, H, R, FI, MM, ML, MH>(
+///
+/// In addition to returning valid messages proof, environment is prepared to verify this message
+/// proof.
+pub fn prepare_message_proof<R, BI, FI, B, BH, BHH>(
 	params: MessageProofParams,
-	make_bridged_message_storage_key: MM,
-	make_bridged_outbound_lane_data_key: ML,
-	make_bridged_header: MH,
-	message_dispatch_weight: Weight,
-	message_payload: MessagePayload,
+	version: &RuntimeVersion,
+	endow_amount: BalanceOf<ThisChain<B>>,
 ) -> (FromBridgedChainMessagesProof<HashOf<BridgedChain<B>>>, Weight)
 where
+	R: frame_system::Config<AccountId = AccountIdOf<ThisChain<B>>>
+		+ pallet_balances::Config<BI, Balance = BalanceOf<ThisChain<B>>>
+		+ pallet_bridge_grandpa::Config<FI>,
+	R::BridgedChain: bp_runtime::Chain<Header = BH>,
 	B: MessageBridge,
-	H: Hasher,
-	R: pallet_bridge_grandpa::Config<FI>,
+	BI: 'static,
 	FI: 'static,
-	<R::BridgedChain as bp_runtime::Chain>::Hash: Into<HashOf<BridgedChain<B>>>,
-	MM: Fn(MessageKey) -> Vec<u8>,
-	ML: Fn(LaneId) -> Vec<u8>,
-	MH: Fn(H::Out) -> <R::BridgedChain as bp_runtime::Chain>::Header,
+	BH: Header<Hash = HashOf<BridgedChain<B>>>,
+	BHH: Hasher<Out = HashOf<BridgedChain<B>>>,
+	AccountIdOf<ThisChain<B>>: PartialEq + sp_std::fmt::Debug,
+	AccountIdOf<BridgedChain<B>>: From<[u8; 32]>,
+	BalanceOf<ThisChain<B>>: Debug + MaybeSerializeDeserialize,
+	CallOf<ThisChain<B>>: From<frame_system::Call<R>> + GetDispatchInfo,
+	HashOf<BridgedChain<B>>: Copy + Default,
+	SignatureOf<ThisChain<B>>: From<sp_core::ed25519::Signature>,
+	SignerOf<ThisChain<B>>: Clone
+		+ From<sp_core::ed25519::Public>
+		+ IdentifyAccount<AccountId = AccountIdOf<ThisChain<B>>>,
 {
-	// prepare Bridged chain storage with messages and (optionally) outbound lane state
-	let message_count =
-		params.message_nonces.end().saturating_sub(*params.message_nonces.start()) + 1;
-	let mut storage_keys = Vec::with_capacity(message_count as usize + 1);
-	let mut root = Default::default();
-	let mut mdb = MemoryDB::default();
-	{
-		let mut trie = TrieDBMutV1::<H>::new(&mut mdb, &mut root);
+	// we'll be dispatching the same call at This chain
+	let remark = match params.size {
+		ProofSize::Minimal(ref size) => vec![0u8; *size as _],
+		_ => vec![],
+	};
+	let call: CallOf<ThisChain<B>> = frame_system::Call::remark { remark }.into();
+	let call_weight = call.get_dispatch_info().weight;
 
-		// insert messages
-		for nonce in params.message_nonces.clone() {
-			let message_key = MessageKey { lane_id: params.lane, nonce };
-			let message_data = MessageData {
-				fee: BalanceOf::<BridgedChain<B>>::from(0),
-				payload: message_payload.clone(),
-			};
-			let storage_key = make_bridged_message_storage_key(message_key);
-			trie.insert(&storage_key, &message_data.encode())
-				.map_err(|_| "TrieMut::insert has failed")
-				.expect("TrieMut::insert should not fail in benchmarks");
-			storage_keys.push(storage_key);
-		}
+	// message payload needs to be signed, because we use `TargetAccount` call origin
+	// (which is 'heaviest' to verify)
+	let bridged_account_id: AccountIdOf<BridgedChain<B>> = [0u8; 32].into();
+	let (this_raw_public, this_raw_signature) = ed25519_sign(
+		&call,
+		&bridged_account_id,
+		version.spec_version,
+		B::BRIDGED_CHAIN_ID,
+		B::THIS_CHAIN_ID,
+	);
+	let this_public: SignerOf<ThisChain<B>> =
+		sp_core::ed25519::Public::from_raw(this_raw_public).into();
+	let this_signature: SignatureOf<ThisChain<B>> =
+		sp_core::ed25519::Signature::from_raw(this_raw_signature).into();
 
-		// insert outbound lane state
-		if let Some(outbound_lane_data) = params.outbound_lane_data {
-			let storage_key = make_bridged_outbound_lane_data_key(params.lane);
-			trie.insert(&storage_key, &outbound_lane_data.encode())
-				.map_err(|_| "TrieMut::insert has failed")
-				.expect("TrieMut::insert should not fail in benchmarks");
-			storage_keys.push(storage_key);
-		}
+	// if dispatch fee is paid at this chain, endow relayer account
+	if params.dispatch_fee_payment == DispatchFeePayment::AtTargetChain {
+		assert_eq!(this_public.clone().into_account(), dispatch_account::<B>());
+		pallet_balances::Pallet::<R, BI>::make_free_balance_be(
+			&this_public.clone().into_account(),
+			endow_amount,
+		);
 	}
-	root = grow_trie(root, &mut mdb, params.size);
 
-	// generate storage proof to be delivered to This chain
-	let mut proof_recorder = Recorder::<H::Out>::new();
-	record_all_keys::<LayoutV1<H>, _>(&mdb, &root, &mut proof_recorder)
-		.map_err(|_| "record_all_keys has failed")
-		.expect("record_all_keys should not fail in benchmarks");
-	let storage_proof = proof_recorder.drain().into_iter().map(|n| n.data.to_vec()).collect();
+	// prepare message payload that is stored in the Bridged chain storage
+	let message_payload = bp_message_dispatch::MessagePayload {
+		spec_version: version.spec_version,
+		weight: call_weight,
+		origin: bp_message_dispatch::CallOrigin::<
+			AccountIdOf<BridgedChain<B>>,
+			SignerOf<ThisChain<B>>,
+			SignatureOf<ThisChain<B>>,
+		>::TargetAccount(bridged_account_id, this_public, this_signature),
+		dispatch_fee_payment: params.dispatch_fee_payment.clone(),
+		call: call.encode(),
+	}
+	.encode();
 
-	// prepare Bridged chain header and insert it into the Substrate pallet
-	let bridged_header = make_bridged_header(root);
-	let bridged_header_hash = bridged_header.hash();
-	pallet_bridge_grandpa::initialize_for_benchmarks::<R, FI>(bridged_header);
+	// finally - prepare storage proof and update environment
+	let (state_root, storage_proof) =
+		prepare_messages_storage_proof::<B, BHH>(&params, message_payload);
+	let bridged_header_hash = insert_bridged_chain_header::<R, FI, B, BH>(state_root);
 
 	(
 		FromBridgedChainMessagesProof {
-			bridged_header_hash: bridged_header_hash.into(),
+			bridged_header_hash,
 			storage_proof,
 			lane: params.lane,
 			nonces_start: *params.message_nonces.start(),
 			nonces_end: *params.message_nonces.end(),
 		},
-		message_dispatch_weight
-			.checked_mul(message_count)
+		call_weight
+			.checked_mul(
+				params.message_nonces.end().saturating_sub(*params.message_nonces.start()) + 1,
+			)
 			.expect("too many messages requested by benchmark"),
 	)
 }
 
 /// Prepare proof of messages delivery for the `receive_messages_delivery_proof` call.
-pub fn prepare_message_delivery_proof<B, H, R, FI, ML, MH>(
+pub fn prepare_message_delivery_proof<R, FI, B, BH, BHH>(
 	params: MessageDeliveryProofParams<AccountIdOf<ThisChain<B>>>,
-	make_bridged_inbound_lane_data_key: ML,
-	make_bridged_header: MH,
 ) -> FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChain<B>>>
 where
-	B: MessageBridge,
-	H: Hasher,
 	R: pallet_bridge_grandpa::Config<FI>,
+	R::BridgedChain: bp_runtime::Chain<Header = BH>,
 	FI: 'static,
-	<R::BridgedChain as bp_runtime::Chain>::Hash: Into<HashOf<BridgedChain<B>>>,
-	ML: Fn(LaneId) -> Vec<u8>,
-	MH: Fn(H::Out) -> <R::BridgedChain as bp_runtime::Chain>::Header,
+	B: MessageBridge,
+	BH: Header<Hash = HashOf<BridgedChain<B>>>,
+	BHH: Hasher<Out = HashOf<BridgedChain<B>>>,
+	HashOf<BridgedChain<B>>: Copy + Default,
 {
 	// prepare Bridged chain storage with inbound lane state
-	let storage_key = make_bridged_inbound_lane_data_key(params.lane);
+	let storage_key =
+		storage_keys::inbound_lane_data_key(B::BRIDGED_MESSAGES_PALLET_NAME, &params.lane).0;
 	let mut root = Default::default();
 	let mut mdb = MemoryDB::default();
 	{
-		let mut trie = TrieDBMutV1::<H>::new(&mut mdb, &mut root);
+		let mut trie = TrieDBMutV1::<BHH>::new(&mut mdb, &mut root);
 		trie.insert(&storage_key, &params.inbound_lane_data.encode())
 			.map_err(|_| "TrieMut::insert has failed")
 			.expect("TrieMut::insert should not fail in benchmarks");
@@ -183,16 +209,14 @@ where
 	root = grow_trie(root, &mut mdb, params.size);
 
 	// generate storage proof to be delivered to This chain
-	let mut proof_recorder = Recorder::<H::Out>::new();
-	record_all_keys::<LayoutV1<H>, _>(&mdb, &root, &mut proof_recorder)
+	let mut proof_recorder = Recorder::<BHH::Out>::new();
+	record_all_keys::<LayoutV1<BHH>, _>(&mdb, &root, &mut proof_recorder)
 		.map_err(|_| "record_all_keys has failed")
 		.expect("record_all_keys should not fail in benchmarks");
 	let storage_proof = proof_recorder.drain().into_iter().map(|n| n.data.to_vec()).collect();
 
-	// prepare Bridged chain header and insert it into the Substrate pallet
-	let bridged_header = make_bridged_header(root);
-	let bridged_header_hash = bridged_header.hash();
-	pallet_bridge_grandpa::initialize_for_benchmarks::<R, FI>(bridged_header);
+	// finally insert header with given state root to our storage
+	let bridged_header_hash = insert_bridged_chain_header::<R, FI, B, BH>(root);
 
 	FromBridgedChainMessagesDeliveryProof {
 		bridged_header_hash: bridged_header_hash.into(),
@@ -201,6 +225,127 @@ where
 	}
 }
 
+/// Prepare storage proof of given messages.
+///
+/// Returns state trie root and nodes with prepared messages.
+fn prepare_messages_storage_proof<B, BHH>(
+	params: &MessageProofParams,
+	message_payload: MessagePayload,
+) -> (HashOf<BridgedChain<B>>, RawStorageProof)
+where
+	B: MessageBridge,
+	BHH: Hasher<Out = HashOf<BridgedChain<B>>>,
+	HashOf<BridgedChain<B>>: Copy + Default,
+{
+	// prepare Bridged chain storage with messages and (optionally) outbound lane state
+	let message_count =
+		params.message_nonces.end().saturating_sub(*params.message_nonces.start()) + 1;
+	let mut storage_keys = Vec::with_capacity(message_count as usize + 1);
+	let mut root = Default::default();
+	let mut mdb = MemoryDB::default();
+	{
+		let mut trie = TrieDBMutV1::<BHH>::new(&mut mdb, &mut root);
+
+		// insert messages
+		for nonce in params.message_nonces.clone() {
+			let message_key = MessageKey { lane_id: params.lane, nonce };
+			let message_data = MessageData {
+				fee: BalanceOf::<BridgedChain<B>>::from(0),
+				payload: message_payload.clone(),
+			};
+			let storage_key = storage_keys::message_key(
+				B::BRIDGED_MESSAGES_PALLET_NAME,
+				&message_key.lane_id,
+				message_key.nonce,
+			)
+			.0;
+			trie.insert(&storage_key, &message_data.encode())
+				.map_err(|_| "TrieMut::insert has failed")
+				.expect("TrieMut::insert should not fail in benchmarks");
+			storage_keys.push(storage_key);
+		}
+
+		// insert outbound lane state
+		if let Some(ref outbound_lane_data) = params.outbound_lane_data {
+			let storage_key =
+				storage_keys::outbound_lane_data_key(B::BRIDGED_MESSAGES_PALLET_NAME, &params.lane)
+					.0;
+			trie.insert(&storage_key, &outbound_lane_data.encode())
+				.map_err(|_| "TrieMut::insert has failed")
+				.expect("TrieMut::insert should not fail in benchmarks");
+			storage_keys.push(storage_key);
+		}
+	}
+	root = grow_trie(root, &mut mdb, params.size);
+
+	// generate storage proof to be delivered to This chain
+	let mut proof_recorder = Recorder::<BHH::Out>::new();
+	record_all_keys::<LayoutV1<BHH>, _>(&mdb, &root, &mut proof_recorder)
+		.map_err(|_| "record_all_keys has failed")
+		.expect("record_all_keys should not fail in benchmarks");
+	let storage_proof = proof_recorder.drain().into_iter().map(|n| n.data.to_vec()).collect();
+
+	(root, storage_proof)
+}
+
+/// Insert Bridged chain header with given state root into storage of GRANDPA pallet at This chain.
+fn insert_bridged_chain_header<R, FI, B, BH>(
+	state_root: HashOf<BridgedChain<B>>,
+) -> HashOf<BridgedChain<B>>
+where
+	R: pallet_bridge_grandpa::Config<FI>,
+	R::BridgedChain: bp_runtime::Chain<Header = BH>,
+	FI: 'static,
+	B: MessageBridge,
+	BH: Header<Hash = HashOf<BridgedChain<B>>>,
+	HashOf<BridgedChain<B>>: Default,
+{
+	let bridged_header = BH::new(
+		Zero::zero(),
+		Default::default(),
+		state_root,
+		Default::default(),
+		Default::default(),
+	);
+	let bridged_header_hash = bridged_header.hash();
+	pallet_bridge_grandpa::initialize_for_benchmarks::<R, FI>(bridged_header);
+	bridged_header_hash
+}
+
+/// Generate ed25519 signature to be used in
+/// `pallet_brdige_call_dispatch::CallOrigin::TargetAccount`.
+///
+/// Returns public key of the signer and the signature itself.
+fn ed25519_sign(
+	target_call: &impl Encode,
+	source_account_id: &impl Encode,
+	target_spec_version: u32,
+	source_chain_id: ChainId,
+	target_chain_id: ChainId,
+) -> ([u8; 32], [u8; 64]) {
+	let target_secret = dispatch_account_secret();
+	let target_public: PublicKey = (&target_secret).into();
+
+	let mut target_pair_bytes = [0u8; KEYPAIR_LENGTH];
+	target_pair_bytes[..SECRET_KEY_LENGTH].copy_from_slice(&target_secret.to_bytes());
+	target_pair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&target_public.to_bytes());
+	let target_pair =
+		ed25519_dalek::Keypair::from_bytes(&target_pair_bytes).expect("hardcoded pair is valid");
+
+	let signature_message = pallet_bridge_dispatch::account_ownership_digest(
+		target_call,
+		source_account_id,
+		target_spec_version,
+		source_chain_id,
+		target_chain_id,
+	);
+	let target_origin_signature = target_pair
+		.try_sign(&signature_message)
+		.expect("Ed25519 try_sign should not fail in benchmarks");
+
+	(target_public.to_bytes(), target_origin_signature.to_bytes())
+}
+
 /// Populate trie with dummy keys+values until trie has at least given size.
 fn grow_trie<H: Hasher>(mut root: H::Out, mdb: &mut MemoryDB<H>, trie_size: ProofSize) -> H::Out {
 	let (iterations, leaf_size, minimal_trie_size) = match trie_size {
@@ -222,8 +367,8 @@ fn grow_trie<H: Hasher>(mut root: H::Out, mdb: &mut MemoryDB<H>, trie_size: Proo
 		}
 
 		let mut trie = TrieDBMutV1::<H>::from_existing(mdb, &mut root)
-			.map_err(|_| "TrieDBMut::from_existing has failed")
-			.expect("TrieDBMut::from_existing should not fail in benchmarks");
+			.map_err(|_| "TrieDBMutV1::from_existing has failed")
+			.expect("TrieDBMutV1::from_existing should not fail in benchmarks");
 		for _ in 0..iterations {
 			trie.insert(&key_index.encode(), &vec![42u8; leaf_size as _])
 				.map_err(|_| "TrieMut::insert has failed")
diff --git a/polkadot/bridges/deny.toml b/polkadot/bridges/deny.toml
index d22897182af29127f2ee0994549dd1b70af2fc04..e5281e0e84919ff2811cc7095f51e402850f4547 100644
--- a/polkadot/bridges/deny.toml
+++ b/polkadot/bridges/deny.toml
@@ -48,21 +48,17 @@ notice = "warn"
 # A list of advisory IDs to ignore. Note that ignored advisories will still
 # output a note when they are encountered.
 ignore = [
-	# yaml-rust < clap. Not feasible to upgrade and also not possible to trigger in practice.
-	"RUSTSEC-2018-0006",
     "RUSTSEC-2020-0070",
     # Comes from honggfuzz via storage-proof-fuzzer: 'memmap'
     "RUSTSEC-2020-0077",
-    # Comes from time: 'stweb' (will be fixed in upcoming time 0.3)
-    "RUSTSEC-2020-0056",
     # net2 (origin: Substrate RPC crates)
     "RUSTSEC-2020-0016",
-    # Wasmtime (origin: Substrate executor crates)
-    "RUSTSEC-2021-0110",
     # time (origin: Substrate RPC + benchmarking crates)
     "RUSTSEC-2020-0071",
     # chrono (origin: Substrate benchmarking + cli + ...)
     "RUSTSEC-2020-0159",
+    # lru 0.6.6 (origin: libp2p)
+    "RUSTSEC-2021-0130",
 ]
 # Threshold for security vulnerabilities, any vulnerability with a CVSS score
 # lower than the range specified will be ignored. Note that ignored advisories
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-millau-to-rialto-messages-dashboard.json b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-millau-to-rialto-messages-dashboard.json
index 32f3e53d6671232a49f7eac990c59932cb6e554c..abce8bbc29ae1fe8a3da5c506e81bfa0c991a9b6 100644
--- a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-millau-to-rialto-messages-dashboard.json
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-millau-to-rialto-messages-dashboard.json
@@ -64,11 +64,18 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "label_replace(label_replace(Millau_to_Rialto_MessageLane_00000000_best_block_numbers{type=~\"target|target_at_source\"}, \"type\", \"At Rialto\", \"type\", \"target\"), \"type\", \"At Millau\", \"type\", \"target_at_source\")",
+          "expr": "Millau_to_Rialto_MessageLane_00000000_best_target_block_number",
           "instant": false,
           "interval": "",
-          "legendFormat": "{{type}}",
+          "legendFormat": "At Rialto",
           "refId": "A"
+        },
+        {
+          "expr": "Millau_to_Rialto_MessageLane_00000000_best_target_at_source_block_number",
+          "instant": false,
+          "interval": "",
+          "legendFormat": "At Millau",
+          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -158,10 +165,16 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "label_replace(label_replace(Millau_to_Rialto_MessageLane_00000000_best_block_numbers{type=~\"source|source_at_target\"}, \"type\", \"At Millau\", \"type\", \"source\"), \"type\", \"At Rialto\", \"type\", \"source_at_target\")",
+          "expr": "Millau_to_Rialto_MessageLane_00000000_best_source_block_number",
           "interval": "",
-          "legendFormat": "{{type}}",
+          "legendFormat": "At Millau",
           "refId": "A"
+        },
+        {
+          "expr": "Millau_to_Rialto_MessageLane_00000000_best_source_at_target_block_number",
+          "interval": "",
+          "legendFormat": "At Rialto",
+          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -531,7 +544,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Millau_to_Rialto_MessageLane_00000000_lane_state_nonces{type=\"target_latest_received\"}[1m])",
+          "expr": "increase(Millau_to_Rialto_MessageLane_00000000_lane_state_nonces{type=\"target_latest_received\"}[1m]) OR on() vector(0)",
           "interval": "",
           "legendFormat": "Messages delivered to Rialto in last 1m",
           "refId": "B"
@@ -954,7 +967,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Millau_to_Rialto_MessageLane_00000001_lane_state_nonces{type=\"target_latest_received\"}[10m])",
+          "expr": "increase(Millau_to_Rialto_MessageLane_00000001_lane_state_nonces{type=\"target_latest_received\"}[10m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "Messages generated in last 5 minutes",
@@ -1097,7 +1110,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Millau_to_Rialto_MessageLane_00000001_lane_state_nonces{type=\"source_latest_confirmed\"}[10m])",
+          "expr": "increase(Millau_to_Rialto_MessageLane_00000001_lane_state_nonces{type=\"source_latest_confirmed\"}[10m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "",
@@ -1241,7 +1254,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Millau_to_Rialto_MessageLane_73776170_lane_state_nonces{type=\"target_latest_received\"}[20m])",
+          "expr": "increase(Millau_to_Rialto_MessageLane_73776170_lane_state_nonces{type=\"target_latest_received\"}[20m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "Messages generated in last 5 minutes",
@@ -1349,7 +1362,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Millau_to_Rialto_MessageLane_73776170_lane_state_nonces{type=\"source_latest_confirmed\"}[10m])",
+          "expr": "increase(Millau_to_Rialto_MessageLane_73776170_lane_state_nonces{type=\"source_latest_confirmed\"}[10m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "",
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-rialto-to-millau-messages-dashboard.json b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-rialto-to-millau-messages-dashboard.json
index eaca8610aec7af3ad079b683a4c7b55755a7e5ca..4e3d314a3f45c1b94b17c384e701cc8fa411a9cd 100644
--- a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-rialto-to-millau-messages-dashboard.json
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/relay-rialto-to-millau-messages-dashboard.json
@@ -64,11 +64,18 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "label_replace(label_replace(Rialto_to_Millau_MessageLane_00000000_best_block_numbers{type=~\"target|target_at_source\"}, \"type\", \"At Millau\", \"type\", \"target\"), \"type\", \"At Rialto\", \"type\", \"target_at_source\")",
+          "expr": "Rialto_to_Millau_MessageLane_00000000_best_target_block_number",
           "instant": false,
           "interval": "",
-          "legendFormat": "{{type}}",
+          "legendFormat": "At Millau",
           "refId": "A"
+        },
+        {
+          "expr": "Rialto_to_Millau_MessageLane_00000000_best_target_at_source_block_number",
+          "instant": false,
+          "interval": "",
+          "legendFormat": "At Rialto",
+          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -158,10 +165,16 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "label_replace(label_replace(Rialto_to_Millau_MessageLane_00000000_best_block_numbers{type=~\"source|source_at_target\"}, \"type\", \"At Rialto\", \"type\", \"source\"), \"type\", \"At Millau\", \"type\", \"source_at_target\")",
+          "expr": "Rialto_to_Millau_MessageLane_00000000_best_source_block_number",
           "interval": "",
-          "legendFormat": "{{type}}",
+          "legendFormat": "At Rialto",
           "refId": "A"
+        },
+        {
+          "expr": "Rialto_to_Millau_MessageLane_00000000_best_source_at_target_block_number",
+          "interval": "",
+          "legendFormat": "At Millau",
+          "refId": "B"
         }
       ],
       "thresholds": [],
@@ -522,7 +535,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Rialto_to_Millau_MessageLane_00000000_lane_state_nonces{type=\"target_latest_received\"}[1m])",
+          "expr": "increase(Rialto_to_Millau_MessageLane_00000000_lane_state_nonces{type=\"target_latest_received\"}[1m]) OR on() vector(0)",
           "interval": "",
           "legendFormat": "Messages delivered to Millau in last 1m",
           "refId": "B"
@@ -945,7 +958,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Rialto_to_Millau_MessageLane_00000001_lane_state_nonces{type=\"target_latest_received\"}[10m])",
+          "expr": "increase(Rialto_to_Millau_MessageLane_00000001_lane_state_nonces{type=\"target_latest_received\"}[10m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "Messages generated in last 5 minutes",
@@ -1089,7 +1102,7 @@
           "refId": "A"
         },
         {
-          "expr": "increase(Rialto_to_Millau_MessageLane_00000001_lane_state_nonces{type=\"source_latest_confirmed\"}[10m])",
+          "expr": "increase(Rialto_to_Millau_MessageLane_00000001_lane_state_nonces{type=\"source_latest_confirmed\"}[10m]) OR on() vector(0)",
           "hide": true,
           "interval": "",
           "legendFormat": "",
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/rialto-millau-maintenance-dashboard.json b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/rialto-millau-maintenance-dashboard.json
index 5280da748502e1aca960095d41d757e7cc95848e..225e46fae3acb2b907e148f3f8b8301739787f98 100644
--- a/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/rialto-millau-maintenance-dashboard.json
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/dashboard/grafana/rialto-millau-maintenance-dashboard.json
@@ -15,7 +15,6 @@
   "editable": true,
   "gnetId": null,
   "graphTooltip": 0,
-  "id": 9,
   "links": [],
   "panels": [
     {
@@ -25,7 +24,9 @@
       "dashes": false,
       "datasource": "Prometheus",
       "fieldConfig": {
-        "defaults": {},
+        "defaults": {
+          "custom": {}
+        },
         "overrides": []
       },
       "fill": 1,
@@ -54,7 +55,7 @@
         "alertThreshold": true
       },
       "percentage": false,
-      "pluginVersion": "7.5.3",
+      "pluginVersion": "7.1.3",
       "pointradius": 2,
       "points": false,
       "renderer": "flot",
@@ -129,7 +130,9 @@
       "dashes": false,
       "datasource": "Prometheus",
       "fieldConfig": {
-        "defaults": {},
+        "defaults": {
+          "custom": {}
+        },
         "overrides": []
       },
       "fill": 1,
@@ -158,7 +161,7 @@
         "alertThreshold": true
       },
       "percentage": false,
-      "pluginVersion": "7.5.3",
+      "pluginVersion": "7.1.3",
       "pointradius": 2,
       "points": false,
       "renderer": "flot",
@@ -169,7 +172,7 @@
       "targets": [
         {
           "exemplar": true,
-          "expr": "kusama_to_base_conversion_rate{instance='relay-millau-rialto:9616'} / polkadot_to_base_conversion_rate{instance='relay-millau-rialto:9616'}",
+          "expr": "polkadot_to_base_conversion_rate{instance='relay-millau-rialto:9616'} / kusama_to_base_conversion_rate{instance='relay-millau-rialto:9616'}",
           "interval": "",
           "legendFormat": "Outside of runtime (actually Polkadot -> Kusama)",
           "refId": "A"
@@ -233,7 +236,9 @@
       "dashes": false,
       "datasource": "Prometheus",
       "fieldConfig": {
-        "defaults": {},
+        "defaults": {
+          "custom": {}
+        },
         "overrides": []
       },
       "fill": 1,
@@ -262,7 +267,7 @@
         "alertThreshold": true
       },
       "percentage": false,
-      "pluginVersion": "7.5.3",
+      "pluginVersion": "7.1.3",
       "pointradius": 2,
       "points": false,
       "renderer": "flot",
@@ -337,7 +342,9 @@
       "dashes": false,
       "datasource": "Prometheus",
       "fieldConfig": {
-        "defaults": {},
+        "defaults": {
+          "custom": {}
+        },
         "overrides": []
       },
       "fill": 1,
@@ -366,7 +373,7 @@
         "alertThreshold": true
       },
       "percentage": false,
-      "pluginVersion": "7.5.3",
+      "pluginVersion": "7.1.3",
       "pointradius": 2,
       "points": false,
       "renderer": "flot",
@@ -377,7 +384,7 @@
       "targets": [
         {
           "exemplar": true,
-          "expr": "polkadot_to_base_conversion_rate{instance='relay-millau-rialto:9616'} / kusama_to_base_conversion_rate{instance='relay-millau-rialto:9616'}",
+          "expr": "kusama_to_base_conversion_rate{instance='relay-millau-rialto:9616'} / polkadot_to_base_conversion_rate{instance='relay-millau-rialto:9616'}",
           "interval": "",
           "legendFormat": "Outside of runtime (actually Kusama -> Polkadot)",
           "refId": "A"
@@ -433,22 +440,620 @@
         "align": false,
         "alignLevel": null
       }
+    },
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                1000
+              ],
+              "type": "lt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "last"
+            },
+            "type": "query"
+          },
+          {
+            "evaluator": {
+              "params": [
+                1000
+              ],
+              "type": "lt"
+            },
+            "operator": {
+              "type": "or"
+            },
+            "query": {
+              "params": [
+                "B",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "last"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "At-Rialto relay balances are too low",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 16
+      },
+      "hiddenSeries": false,
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "at_Rialto_relay_balance",
+          "interval": "",
+          "legendFormat": "Relay account balance",
+          "refId": "A"
+        },
+        {
+          "expr": "at_Rialto_messages_pallet_owner_balance",
+          "interval": "",
+          "legendFormat": "Messages pallet owner balance",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "lt",
+          "value": 1000
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Rialto relay balances",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                1000
+              ],
+              "type": "lt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "last"
+            },
+            "type": "query"
+          },
+          {
+            "evaluator": {
+              "params": [
+                1000
+              ],
+              "type": "lt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "B",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "last"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "At-Millau relay balances are too low",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 16
+      },
+      "hiddenSeries": false,
+      "id": 9,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "at_Millau_relay_balance",
+          "interval": "",
+          "legendFormat": "Relay account balance",
+          "refId": "A"
+        },
+        {
+          "expr": "at_Millau_messages_pallet_owner_balance",
+          "interval": "",
+          "legendFormat": "Messages pallet owner balance",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "lt",
+          "value": 1000
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Millau relay balances",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                0
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "max"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Whether with-Rialto-grandpa-pallet and Rialto itself are on different forks alert",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 24
+      },
+      "hiddenSeries": false,
+      "id": 11,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "Rialto_to_Millau_MessageLane_00000000_is_source_and_source_at_target_using_different_forks",
+          "interval": "",
+          "legendFormat": "On different forks?",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 0
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Whether with-Rialto-grandpa-pallet and Rialto itself are on different forks",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                0
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "max"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Whether with-Rialto-grandpa-pallet and Rialto itself are on different forks alert",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 24
+      },
+      "hiddenSeries": false,
+      "id": 12,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "Millau_to_Rialto_MessageLane_00000000_is_source_and_source_at_target_using_different_forks",
+          "interval": "",
+          "legendFormat": "On different forks?",
+          "refId": "A"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 0
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Whether with-Millau-grandpa-pallet and Millau itself are on different forks",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     }
   ],
   "refresh": "10s",
-  "schemaVersion": 27,
+  "schemaVersion": 26,
   "style": "dark",
   "tags": [],
   "templating": {
     "list": []
   },
   "time": {
-    "from": "now-1h",
+    "from": "now-15m",
     "to": "now"
   },
   "timepicker": {},
   "timezone": "",
   "title": "Rialto+Millau maintenance dashboard",
   "uid": "7AuyrjlMz",
-  "version": 2
+  "version": 1
 }
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/docker-compose.yml b/polkadot/bridges/deployments/bridges/rialto-millau/docker-compose.yml
index 1ff93869de1cb46f3af31f93bbe606f77d6ba0a1..5d774a5780252adc9352f59336d35d4a42b4aa8e 100644
--- a/polkadot/bridges/deployments/bridges/rialto-millau/docker-compose.yml
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/docker-compose.yml
@@ -108,6 +108,7 @@ services:
       LETSENCRYPT_EMAIL: admin@parity.io
     volumes:
       - ./bridges/rialto-millau/dashboard/grafana:/etc/grafana/dashboards/rialto-millau:ro
+      - ./networks/dashboard/grafana/beefy-dashboard.json:/etc/grafana/dashboards/beefy.json
 
   prometheus-metrics:
     volumes:
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-millau-to-rialto-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-millau-to-rialto-entrypoint.sh
index 758dce2515aa8586dcddbd2738259f08fb2b4fd6..743cc47f07e766a5284fc71068bcea0d0d6c5a91 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-millau-to-rialto-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-millau-to-rialto-entrypoint.sh
@@ -1,9 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 60
-curl -v http://millau-node-bob:9933/health
-curl -v http://rialto-node-bob:9933/health
+sleep 15
 
 MESSAGE_LANE=${MSG_EXCHANGE_GEN_LANE:-00000000}
 
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-rialto-to-millau-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-rialto-to-millau-entrypoint.sh
index e0731e9058d1dea3af074a93b34d105db36443f5..2b536dbd8171b4598091271f83026b9ba3df9956 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-rialto-to-millau-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-rialto-to-millau-entrypoint.sh
@@ -1,9 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 60
-curl -v http://millau-node-bob:9933/health
-curl -v http://rialto-node-bob:9933/health
+sleep 15
 
 MESSAGE_LANE=${MSG_EXCHANGE_GEN_LANE:-00000000}
 
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-millau-generator-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-millau-generator-entrypoint.sh
index b8d051a13122bf0388e1ab5ec37dbc774a26e676..e20b3da7df80b1104a467312d378e69323cf20c0 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-millau-generator-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-millau-generator-entrypoint.sh
@@ -34,16 +34,13 @@ LARGE_MESSAGES_TIME=0
 # start sending message packs in a hour
 BUNCH_OF_MESSAGES_TIME=3600
 
-# give conversion rate updater some time to update Millau->Rialto conversion rate in Rialto
-# (initially rate=1 and rational relayer won't deliver any messages if it'll be changed to larger value)
-sleep 180
-
 while true
 do
 	rand_sleep
 	echo "Sending Remark from Rialto to Millau using Target Origin"
 	$SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Target \
 		remark
 
@@ -51,6 +48,7 @@ do
 		echo "Sending Remark from Rialto to Millau using Target Origin using secondary lane: $SECONDARY_MESSAGE_LANE"
 		$SEND_MESSAGE \
 			--lane $SECONDARY_MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-fee-payment at-target-chain \
 			remark
@@ -60,6 +58,7 @@ do
 	echo "Sending Transfer from Rialto to Millau using Target Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Target \
 		transfer \
 		--amount 1000000000 \
@@ -69,6 +68,7 @@ do
 	echo "Sending Remark from Rialto to Millau using Source Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Source \
 		remark
 
@@ -76,6 +76,7 @@ do
 	echo "Sending Transfer from Rialto to Millau using Source Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Source \
 		transfer \
 		--amount 1000000000 \
@@ -89,6 +90,7 @@ do
 		echo "Sending Maximal Size Remark from Rialto to Millau using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			remark \
 			--remark-size=max
@@ -97,6 +99,7 @@ do
 		echo "Sending Maximal Dispatch Weight Remark from Rialto to Millau using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-weight=max \
 			remark
@@ -105,6 +108,7 @@ do
 		echo "Sending Maximal Size and Dispatch Weight Remark from Rialto to Millau using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-weight=max \
 			remark \
@@ -116,10 +120,21 @@ do
 	if [ $SECONDS -ge $BUNCH_OF_MESSAGES_TIME ]; then
 		BUNCH_OF_MESSAGES_TIME=$((SECONDS + 7200))
 
+		SEND_MESSAGE_OUTPUT=`$SEND_MESSAGE --lane $MESSAGE_LANE --conversion-rate-override metric --origin Target remark 2>&1`
+		echo $SEND_MESSAGE_OUTPUT
+		ACTUAL_CONVERSION_RATE_REGEX="conversion rate override: ([0-9\.]+)"
+		if [[ $SEND_MESSAGE_OUTPUT =~ $ACTUAL_CONVERSION_RATE_REGEX ]]; then
+			ACTUAL_CONVERSION_RATE=${BASH_REMATCH[1]}
+		else
+			echo "Unable to find conversion rate in send-message output"
+			exit 1
+		fi
+
 		for i in $(seq 1 $MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE);
 		do
 			$SEND_MESSAGE \
 				--lane $MESSAGE_LANE \
+				--conversion-rate-override $ACTUAL_CONVERSION_RATE \
 				--origin Target \
 				remark
 		done
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-generator-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-generator-entrypoint.sh
index 0365ebe1d8b46b8d79ce4ce01d01ff4bfc049cc7..a8e032bbdfddfd41d880b5d1160c82fd6848aaf4 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-generator-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-generator-entrypoint.sh
@@ -34,16 +34,13 @@ LARGE_MESSAGES_TIME=0
 # start sending message packs in a hour
 BUNCH_OF_MESSAGES_TIME=3600
 
-# give conversion rate updater some time to update Rialto->Millau conversion rate in Millau
-# (initially rate=1 and rational relayer won't deliver any messages if it'll be changed to larger value)
-sleep 180
-
 while true
 do
 	rand_sleep
 	echo "Sending Remark from Millau to Rialto using Target Origin"
 	$SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Target \
 		remark
 
@@ -51,6 +48,7 @@ do
 		echo "Sending Remark from Millau to Rialto using Target Origin using secondary lane: $SECONDARY_MESSAGE_LANE"
 		$SEND_MESSAGE \
 			--lane $SECONDARY_MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-fee-payment at-target-chain \
 			remark
@@ -60,6 +58,7 @@ do
 	echo "Sending Transfer from Millau to Rialto using Target Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Target \
 		transfer \
 		--amount 1000000000 \
@@ -69,6 +68,7 @@ do
 	echo "Sending Remark from Millau to Rialto using Source Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Source \
 		remark
 
@@ -76,6 +76,7 @@ do
 	echo "Sending Transfer from Millau to Rialto using Source Origin"
 	 $SEND_MESSAGE \
 		--lane $MESSAGE_LANE \
+		--conversion-rate-override metric \
 		--origin Source \
 		transfer \
 		--amount 1000000000 \
@@ -89,6 +90,7 @@ do
 		echo "Sending Maximal Size Remark from Millau to Rialto using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			remark \
 			--remark-size=max
@@ -97,6 +99,7 @@ do
 		echo "Sending Maximal Dispatch Weight Remark from Millau to Rialto using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-weight=max \
 			remark
@@ -105,6 +108,7 @@ do
 		echo "Sending Maximal Size and Dispatch Weight Remark from Millau to Rialto using Target Origin"
 		$SEND_MESSAGE \
 			--lane $MESSAGE_LANE \
+			--conversion-rate-override metric \
 			--origin Target \
 			--dispatch-weight=max \
 			remark \
@@ -116,10 +120,21 @@ do
 	if [ $SECONDS -ge $BUNCH_OF_MESSAGES_TIME ]; then
 		BUNCH_OF_MESSAGES_TIME=$((SECONDS + 7200))
 
-		for i in $(seq 1 $MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE);
+		SEND_MESSAGE_OUTPUT=`$SEND_MESSAGE --lane $MESSAGE_LANE --conversion-rate-override metric --origin Target remark 2>&1`
+		echo $SEND_MESSAGE_OUTPUT
+		ACTUAL_CONVERSION_RATE_REGEX="conversion rate override: ([0-9\.]+)"
+		if [[ $SEND_MESSAGE_OUTPUT =~ $ACTUAL_CONVERSION_RATE_REGEX ]]; then
+			ACTUAL_CONVERSION_RATE=${BASH_REMATCH[1]}
+		else
+			echo "Unable to find conversion rate in send-message output"
+			exit 1
+		fi
+
+		for i in $(seq 2 $MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE);
 		do
 			$SEND_MESSAGE \
 				--lane $MESSAGE_LANE \
+				--conversion-rate-override $ACTUAL_CONVERSION_RATE \
 				--origin Target \
 				remark
 		done
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-resubmitter-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-resubmitter-entrypoint.sh
index ca4c9f03a8bb80672ea3d37684c63c039ddbbf10..068560e15021abcf19e3220dff4adaa04e5d387f 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-resubmitter-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-messages-to-rialto-resubmitter-entrypoint.sh
@@ -1,8 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 20
-curl -v http://millau-node-alice:9933/health
+sleep 15
 
 # //Dave is signing Millau -> Rialto message-send transactions, which are causing problems.
 #
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-millau-rialto-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-millau-rialto-entrypoint.sh
index c87591fb6dbb75d6f8b2c26ed30e2c2d55f184f6..bab0e1c4af3ea9efd92fce3fd9f639946dc2c672 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-millau-rialto-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-millau-rialto-entrypoint.sh
@@ -1,9 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 60
-curl -v http://millau-node-alice:9933/health
-curl -v http://rialto-node-alice:9933/health
+sleep 15
 
 /home/user/substrate-relay init-bridge millau-to-rialto \
 	--source-host millau-node-alice \
@@ -27,10 +25,12 @@ sleep 6
 	--millau-port 9944 \
 	--millau-signer //Charlie \
 	--millau-messages-pallet-owner=//RialtoMessagesOwner \
+	--millau-transactions-mortality=64 \
 	--rialto-host rialto-node-alice \
 	--rialto-port 9944 \
 	--rialto-signer //Charlie \
 	--rialto-messages-pallet-owner=//MillauMessagesOwner \
+	--rialto-transactions-mortality=64 \
 	--lane=00000000 \
 	--lane=73776170 \
 	--prometheus-host=0.0.0.0
diff --git a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-token-swap-generator-entrypoint.sh b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-token-swap-generator-entrypoint.sh
index 95bbe1e38fb295d80a83c30279c3c144ebd8a38c..010c0572d50ed27068d40b2d28d260c36f09c473 100755
--- a/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-token-swap-generator-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/rialto-millau/entrypoints/relay-token-swap-generator-entrypoint.sh
@@ -23,10 +23,6 @@ rand_sleep() {
 	echo "Woke up at $NOW"
 }
 
-# give conversion rate updater some time to update Rialto->Millau conversion rate in Millau
-# (initially rate=1 and rational relayer won't deliver any messages if it'll be changed to larger value)
-sleep 180
-
 while true
 do
 	rand_sleep
@@ -42,6 +38,8 @@ do
 		--target-port $TARGET_PORT \
 		--target-signer //WithMillauTokenSwap \
 		--target-balance 200000 \
+		--target-to-source-conversion-rate-override metric \
+		--source-to-target-conversion-rate-override metric \
 		lock-until-block \
 		--blocks-before-expire 32
 done
diff --git a/polkadot/bridges/deployments/bridges/westend-millau/dashboard/grafana/relay-westend-to-millau-headers-dashboard.json b/polkadot/bridges/deployments/bridges/westend-millau/dashboard/grafana/relay-westend-to-millau-headers-dashboard.json
index 1a3603512fdf056e58f2ddcfae4b4b86f7cdeec2..682ac2c77867489dd6a0d7bd5c4828008a5ca780 100644
--- a/polkadot/bridges/deployments/bridges/westend-millau/dashboard/grafana/relay-westend-to-millau-headers-dashboard.json
+++ b/polkadot/bridges/deployments/bridges/westend-millau/dashboard/grafana/relay-westend-to-millau-headers-dashboard.json
@@ -99,7 +99,7 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "max(Westend_to_Millau_Sync_best_block_numbers{node=\"source\"}) - max(Westend_to_Millau_Sync_best_block_numbers{node=\"target\"})",
+          "expr": "max(Westend_to_Millau_Sync_best_source_block_number) - max(Westend_to_Millau_Sync_best_source_at_target_block_number)",
           "format": "table",
           "instant": false,
           "interval": "",
@@ -237,7 +237,7 @@
       "steppedLine": false,
       "targets": [
         {
-          "expr": "max_over_time(Westend_to_Millau_Sync_best_block_numbers{node=\"source\"}[10m])-min_over_time(Westend_to_Millau_Sync_best_block_numbers{node=\"source\"}[10m])",
+          "expr": "max_over_time(Westend_to_Millau_Sync_best_source_block_number[10m])-min_over_time(Westend_to_Millau_Sync_best_source_block_number[10m])",
           "interval": "",
           "legendFormat": "Number of new Headers on Westend (Last 10 Mins)",
           "refId": "A"
@@ -341,13 +341,22 @@
       "pluginVersion": "7.1.3",
       "targets": [
         {
-          "expr": "Westend_to_Millau_Sync_best_block_numbers",
+          "expr": "Westend_to_Millau_Sync_best_source_block_number",
           "format": "time_series",
           "instant": true,
           "interval": "",
           "intervalFactor": 1,
-          "legendFormat": "Best Known Header on {{node}} Node",
+          "legendFormat": "Best Known Westend Header at Westend",
           "refId": "A"
+        },
+        {
+          "expr": "Westend_to_Millau_Sync_best_source_at_target_block_number",
+          "format": "time_series",
+          "instant": true,
+          "interval": "",
+          "intervalFactor": 1,
+          "legendFormat": "Best Known Westend Header at Millau",
+          "refId": "B"
         }
       ],
       "timeFrom": null,
@@ -513,61 +522,139 @@
       "type": "gauge"
     },
     {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                0
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "max"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Whether with-Westend-grandpa-pallet and Westend itself are on different forks alert",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
       "datasource": "Prometheus",
-      "description": "",
       "fieldConfig": {
         "defaults": {
-          "custom": {},
-          "mappings": [],
-          "thresholds": {
-            "mode": "absolute",
-            "steps": [
-              {
-                "color": "green",
-                "value": null
-              },
-              {
-                "color": "red",
-                "value": 80
-              }
-            ]
-          }
+          "custom": {}
         },
         "overrides": []
       },
+      "fill": 1,
+      "fillGradient": 0,
       "gridPos": {
         "h": 10,
         "w": 12,
         "x": 0,
         "y": 14
       },
-      "id": 4,
-      "options": {
-        "displayMode": "gradient",
-        "orientation": "auto",
-        "reduceOptions": {
-          "calcs": [
-            "mean"
-          ],
-          "fields": "",
-          "values": false
-        },
-        "showUnfilled": true
+      "hiddenSeries": false,
+      "id": 18,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
       },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
       "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
       "targets": [
         {
-          "expr": "Westend_to_Millau_Sync_blocks_in_state",
-          "instant": true,
+          "expr": "Westend_to_Millau_Sync_is_source_and_source_at_target_using_different_forks",
           "interval": "",
-          "legendFormat": "{{state}}",
+          "legendFormat": "On different forks?",
           "refId": "A"
         }
       ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 0
+        }
+      ],
       "timeFrom": null,
+      "timeRegions": [],
       "timeShift": null,
-      "title": "Queued Headers in Relay",
-      "type": "bargauge"
+      "title": "Whether with-Westend-grandpa-pallet and Westend itself are on different forks",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     },
     {
       "aliasColors": {},
diff --git a/polkadot/bridges/deployments/bridges/westend-millau/entrypoints/relay-headers-westend-to-millau-entrypoint.sh b/polkadot/bridges/deployments/bridges/westend-millau/entrypoints/relay-headers-westend-to-millau-entrypoint.sh
index d3b6932983fba343392ce35568ace2885b6a498c..f37ee69915cda6a754109a92b3bd7e2e11efccf8 100755
--- a/polkadot/bridges/deployments/bridges/westend-millau/entrypoints/relay-headers-westend-to-millau-entrypoint.sh
+++ b/polkadot/bridges/deployments/bridges/westend-millau/entrypoints/relay-headers-westend-to-millau-entrypoint.sh
@@ -1,9 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 60
-curl -v http://millau-node-alice:9933/health
-curl -v https://westend-rpc.polkadot.io:443/health
+sleep 15
 
 /home/user/substrate-relay init-bridge westend-to-millau \
 	--source-host westend-rpc.polkadot.io \
diff --git a/polkadot/bridges/deployments/local-scripts/relay-messages-millau-to-rialto.sh b/polkadot/bridges/deployments/local-scripts/relay-messages-millau-to-rialto.sh
index d420dc56c263f66a95401fd49a276bdcfe68bd9c..36673d31be8ce6aef07ca0072f62ffd2d8f16db5 100755
--- a/polkadot/bridges/deployments/local-scripts/relay-messages-millau-to-rialto.sh
+++ b/polkadot/bridges/deployments/local-scripts/relay-messages-millau-to-rialto.sh
@@ -10,6 +10,7 @@ RIALTO_PORT="${RIALTO_PORT:-9944}"
 
 RUST_LOG=bridge=debug \
 ./target/debug/substrate-relay relay-messages millau-to-rialto \
+	--relayer-mode=altruistic \
 	--lane 00000000 \
 	--source-host localhost \
 	--source-port $MILLAU_PORT \
diff --git a/polkadot/bridges/deployments/local-scripts/relay-messages-rialto-to-millau.sh b/polkadot/bridges/deployments/local-scripts/relay-messages-rialto-to-millau.sh
index 0cd73c00454d9ff5d5c408f49da8c66d4a68a0c3..89e2b81824558663aa2b07cd3c8a151092694e3a 100755
--- a/polkadot/bridges/deployments/local-scripts/relay-messages-rialto-to-millau.sh
+++ b/polkadot/bridges/deployments/local-scripts/relay-messages-rialto-to-millau.sh
@@ -10,6 +10,7 @@ RIALTO_PORT="${RIALTO_PORT:-9944}"
 
 RUST_LOG=bridge=debug \
 ./target/debug/substrate-relay relay-messages rialto-to-millau \
+	--relayer-mode=altruistic \
 	--lane 00000000 \
 	--source-host localhost \
 	--source-port $RIALTO_PORT \
diff --git a/polkadot/bridges/deployments/local-scripts/relay-millau-to-rialto.sh b/polkadot/bridges/deployments/local-scripts/relay-millau-to-rialto.sh
index 8b18cff2b53c22081d06732005ea8ecb50dc4528..35d88d1a643b1bd31bedd32465e835e84ac20639 100755
--- a/polkadot/bridges/deployments/local-scripts/relay-millau-to-rialto.sh
+++ b/polkadot/bridges/deployments/local-scripts/relay-millau-to-rialto.sh
@@ -15,6 +15,8 @@ RUST_LOG=bridge=debug \
 	--target-host localhost \
 	--target-port $RIALTO_PORT \
 	--target-signer //Alice \
+	--source-version-mode Bundle \
+	--target-version-mode Bundle
 
 sleep 5
 RUST_LOG=bridge=debug \
diff --git a/polkadot/bridges/deployments/networks/dashboard/grafana/beefy-dashboard.json b/polkadot/bridges/deployments/networks/dashboard/grafana/beefy-dashboard.json
new file mode 100644
index 0000000000000000000000000000000000000000..0216e145548ec7952734fbcb24bdbc1c1efcddc8
--- /dev/null
+++ b/polkadot/bridges/deployments/networks/dashboard/grafana/beefy-dashboard.json
@@ -0,0 +1,539 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+            "params": [
+              1
+            ],
+            "type": "lt"
+            },
+            "operator": {
+            "type": "and"
+            },
+            "query": {
+            "params": [
+              "C",
+              "5m",
+              "now"
+            ]
+            },
+            "reducer": {
+            "params": [],
+            "type": "max"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Beefy best blocks not advancing",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "align": null
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "green",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 14,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "hiddenSeries": false,
+      "id": 2,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "polkadot_beefy_best_block",
+          "legendFormat": "Rialto(Charlie)",
+          "refId": "A"
+        },
+        {
+          "expr": "substrate_beefy_best_block",
+          "legendFormat": "Millau(Charlie)",
+          "refId": "B"
+        },
+        {
+          "expr": "max_over_time(substrate_beefy_best_block[5m]) - min_over_time(substrate_beefy_best_block[5m])",
+          "hide": true,
+          "legendFormat": "Millau Best Beefy blocks count in last 5 minutes",
+          "refId": "C"
+        },
+        {
+          "expr": "max_over_time(polkadot_beefy_best_block[5m]) - min_over_time(polkadot_beefy_best_block[5m])",
+          "hide": true,
+          "legendFormat": "Rialto Best Beefy blocks count in last 5 minutes",
+          "refId": "D"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "lt",
+          "value": 1
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Beefy Best block",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {},
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "yellow",
+                "value": null
+              },
+              {
+                "color": "yellow",
+                "value": null
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "gridPos": {
+        "h": 14,
+        "w": 11,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "options": {
+        "colorMode": "value",
+        "graphMode": "area",
+        "justifyMode": "auto",
+        "orientation": "auto",
+        "reduceOptions": {
+          "calcs": [
+            "mean"
+          ],
+          "fields": "",
+          "values": false
+        },
+        "textMode": "auto"
+      },
+      "pluginVersion": "7.1.3",
+      "targets": [
+        {
+          "expr": "polkadot_beefy_should_vote_on",
+          "legendFormat": "Rialto(Charlie) Should-Vote-On",
+          "refId": "C"
+        },
+        {
+          "expr": "polkadot_beefy_round_concluded",
+          "legendFormat": "Rialto(Charlie) Round-Concluded",
+          "refId": "A"
+        },
+        {
+          "expr": "substrate_beefy_should_vote_on",
+          "legendFormat": "Millau(Charlie) Should-Vote-On",
+          "refId": "D"
+        },
+        {
+          "expr": "substrate_beefy_round_concluded",
+          "legendFormat": "Millau(Charlie) Round-Concluded",
+          "refId": "B"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Beefy Voting Rounds",
+      "type": "stat"
+    },
+    {
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 18,
+        "x": 0,
+        "y": 14
+      },
+      "hiddenSeries": false,
+      "id": 6,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "polkadot_beefy_votes_sent",
+          "legendFormat": "Rialto (node Charlie)",
+          "refId": "A"
+        },
+        {
+          "expr": "substrate_beefy_votes_sent",
+          "legendFormat": "Millau (node Charlie)",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Beefy Votes Sent",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "alertRuleTags": {},
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                0
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "or"
+            },
+            "query": {
+              "params": [
+                "B",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "max"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Beefy Skipped Sessions alert",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "Prometheus",
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "align": null
+          },
+          "mappings": [],
+          "thresholds": {
+            "mode": "absolute",
+            "steps": [
+              {
+                "color": "green",
+                "value": null
+              },
+              {
+                "color": "red",
+                "value": 1
+              }
+            ]
+          }
+        },
+        "overrides": []
+      },
+      "fill": 1,
+      "fillGradient": 0,
+      "gridPos": {
+        "h": 8,
+        "w": 5,
+        "x": 18,
+        "y": 14
+      },
+      "hiddenSeries": false,
+      "id": 8,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 1,
+      "nullPointMode": "null",
+      "percentage": false,
+      "pluginVersion": "7.1.3",
+      "pointradius": 2,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "expr": "polkadot_beefy_skipped_sessions",
+          "legendFormat": "Rialto(Charlie)",
+          "refId": "A"
+        },
+        {
+          "expr": "substrate_beefy_skipped_sessions",
+          "legendFormat": "Millau(Charlie)",
+          "refId": "B"
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 0
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Beefy Skipped Sessions",
+      "tooltip": {
+        "shared": true,
+        "sort": 0,
+        "value_type": "individual"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    }
+  ],
+  "refresh": "5s",
+  "schemaVersion": 26,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-5m",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": [
+      "5s",
+      "10s",
+      "30s",
+      "1m",
+      "5m",
+      "15m",
+      "30m",
+      "1h",
+      "2h",
+      "1d"
+    ]
+  },
+  "timezone": "",
+  "title": "Beefy",
+  "uid": "j6cRDRh7z",
+  "version": 1
+}
diff --git a/polkadot/bridges/deployments/networks/dashboard/prometheus/millau-targets.yml b/polkadot/bridges/deployments/networks/dashboard/prometheus/millau-targets.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c7a06509276f93b6663e8ec85e680e520e405275
--- /dev/null
+++ b/polkadot/bridges/deployments/networks/dashboard/prometheus/millau-targets.yml
@@ -0,0 +1,2 @@
+- targets:
+  - millau-node-charlie:9615
diff --git a/polkadot/bridges/deployments/networks/dashboard/prometheus/rialto-targets.yml b/polkadot/bridges/deployments/networks/dashboard/prometheus/rialto-targets.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9de26b9a2d7f396ca9fbe95031c1821185e1e175
--- /dev/null
+++ b/polkadot/bridges/deployments/networks/dashboard/prometheus/rialto-targets.yml
@@ -0,0 +1,2 @@
+- targets:
+  - rialto-node-charlie:9615
diff --git a/polkadot/bridges/deployments/networks/entrypoints/rialto-parachain-registrar-entrypoint.sh b/polkadot/bridges/deployments/networks/entrypoints/rialto-parachain-registrar-entrypoint.sh
index 172502327c9a071a3581a1a60dff955667f6e29f..519ab228e93212221cb2dc2709ff84573afef6c7 100755
--- a/polkadot/bridges/deployments/networks/entrypoints/rialto-parachain-registrar-entrypoint.sh
+++ b/polkadot/bridges/deployments/networks/entrypoints/rialto-parachain-registrar-entrypoint.sh
@@ -1,9 +1,7 @@
 #!/bin/bash
 set -xeu
 
-sleep 60
-curl -v http://rialto-node-alice:9933/health
-curl -v http://rialto-parachain-collator-alice:9933/health
+sleep 15
 
 /home/user/substrate-relay register-parachain rialto-parachain \
 	--parachain-host rialto-parachain-collator-alice \
diff --git a/polkadot/bridges/deployments/networks/millau.yml b/polkadot/bridges/deployments/networks/millau.yml
index d42c1d7d07cb6e9748eb5e55e8d04cfcbb5523ee..13ac8d4877266f4e9250c8860b01133cc0c9a52f 100644
--- a/polkadot/bridges/deployments/networks/millau.yml
+++ b/polkadot/bridges/deployments/networks/millau.yml
@@ -17,6 +17,7 @@ services:
       - --alice
       - --node-key=0f900c89f4e626f4a217302ab8c7d213737d00627115f318ad6fb169717ac8e0
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     environment:
@@ -35,6 +36,7 @@ services:
       - --bob
       - --node-key=db383639ff2905d79f8e936fd5dc4416ef46b514b2f83823ec3c42753d7557bb
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
@@ -50,11 +52,14 @@ services:
       - --bootnodes=/dns4/millau-node-alice/tcp/30333/p2p/12D3KooWFqiV73ipQ1jpfVmCfLqBCp8G9PLH3zPkY9EhmdrSGA4H
       - --charlie
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
+      - --prometheus-external
     ports:
       - "20133:9933"
       - "20144:9944"
+      - "20615:9615"
 
   millau-node-dave:
     <<: *millau-bridge-node
@@ -65,6 +70,7 @@ services:
       - --bootnodes=/dns4/millau-node-alice/tcp/30333/p2p/12D3KooWFqiV73ipQ1jpfVmCfLqBCp8G9PLH3zPkY9EhmdrSGA4H
       - --dave
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
@@ -80,8 +86,16 @@ services:
       - --bootnodes=/dns4/millau-node-alice/tcp/30333/p2p/12D3KooWFqiV73ipQ1jpfVmCfLqBCp8G9PLH3zPkY9EhmdrSGA4H
       - --eve
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
       - "20333:9933"
       - "20344:9944"
+
+  # Note: These are being overridden from the top level `monitoring` compose file.
+  prometheus-metrics:
+    volumes:
+      - ./networks/dashboard/prometheus/millau-targets.yml:/etc/prometheus/targets-millau-nodes.yml
+    depends_on:
+      - millau-node-charlie
diff --git a/polkadot/bridges/deployments/networks/rialto.yml b/polkadot/bridges/deployments/networks/rialto.yml
index 0a484b2dad7511fa34b1b7447cbfd7dd0f0f92db..40e881a37c199feb4b9379983fe795842d62cb96 100644
--- a/polkadot/bridges/deployments/networks/rialto.yml
+++ b/polkadot/bridges/deployments/networks/rialto.yml
@@ -17,6 +17,7 @@ services:
       - --alice
       - --node-key=79cf382988364291a7968ae7825c01f68c50d679796a8983237d07fe0ccf363b
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     environment:
@@ -35,6 +36,7 @@ services:
       - --bob
       - --node-key=4f9d0146dd9b7b3bf5a8089e3880023d1df92057f89e96e07bb4d8c2ead75bbd
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
@@ -50,11 +52,14 @@ services:
       - --bootnodes=/dns4/rialto-node-alice/tcp/30333/p2p/12D3KooWMF6JvV319a7kJn5pqkKbhR3fcM2cvK5vCbYZHeQhYzFE
       - --charlie
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
+      - --prometheus-external
     ports:
       - "10133:9933"
       - "10144:9944"
+      - "10615:9615"
 
   rialto-node-dave:
     <<: *rialto-bridge-node
@@ -65,6 +70,7 @@ services:
       - --bootnodes=/dns4/rialto-node-alice/tcp/30333/p2p/12D3KooWMF6JvV319a7kJn5pqkKbhR3fcM2cvK5vCbYZHeQhYzFE
       - --dave
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
@@ -80,6 +86,7 @@ services:
       - --bootnodes=/dns4/rialto-node-alice/tcp/30333/p2p/12D3KooWMF6JvV319a7kJn5pqkKbhR3fcM2cvK5vCbYZHeQhYzFE
       - --eve
       - --rpc-cors=all
+      - --enable-offchain-indexing=true
       - --unsafe-rpc-external
       - --unsafe-ws-external
     ports:
@@ -93,6 +100,13 @@ services:
       - ./networks/entrypoints:/entrypoints
       - rialto-share:/rialto-share:z
 
+  # Note: These are being overridden from the top level `monitoring` compose file.
+  prometheus-metrics:
+    volumes:
+      - ./networks/dashboard/prometheus/rialto-targets.yml:/etc/prometheus/targets-rialto-nodes.yml
+    depends_on:
+      - rialto-node-charlie
+
 # we're using `/rialto-share` to expose Rialto chain spec to those who are interested. Right
 # now it is Rialto Parachain collator nodes. Local + tmpfs combination allows sharing writable
 # in-memory volumes, which are dropped when containers are stopped.
diff --git a/polkadot/bridges/fuzz/storage-proof/Cargo.toml b/polkadot/bridges/fuzz/storage-proof/Cargo.toml
index c4da57b255c83de4c8c14d77b2cd46e06785dcf7..b406054bc6e484caedd82557497abb75945c94d8 100644
--- a/polkadot/bridges/fuzz/storage-proof/Cargo.toml
+++ b/polkadot/bridges/fuzz/storage-proof/Cargo.toml
@@ -2,7 +2,7 @@
 name = "storage-proof-fuzzer"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -19,6 +19,7 @@ bp-runtime = { path = "../../primitives/runtime" }
 # Substrate Dependencies
 
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-state-machine = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/fuzz/storage-proof/src/main.rs b/polkadot/bridges/fuzz/storage-proof/src/main.rs
index 9d9262908616733e0e03d0f0983c6d76070b2f02..185d0e336c4d97a2f65d29afcc6f15ec97c80dcb 100644
--- a/polkadot/bridges/fuzz/storage-proof/src/main.rs
+++ b/polkadot/bridges/fuzz/storage-proof/src/main.rs
@@ -31,8 +31,9 @@ fn craft_known_storage_proof(input_vec: Vec<(Vec<u8>, Vec<u8>)>) -> (H256, Stora
 	let storage_proof_vec =
 		vec![(None, input_vec.iter().map(|x| (x.0.clone(), Some(x.1.clone()))).collect())];
 	log::info!("Storage proof vec {:?}", storage_proof_vec);
-	let backend = <InMemoryBackend<Blake2Hasher>>::from(storage_proof_vec);
-	let root = backend.storage_root(std::iter::empty()).0;
+	let state_version = sp_runtime::StateVersion::default();
+	let backend = <InMemoryBackend<Blake2Hasher>>::from((storage_proof_vec, state_version));
+	let root = backend.storage_root(std::iter::empty(), state_version).0;
 	let vector_element_proof = StorageProof::new(
 		prove_read(backend, input_vec.iter().map(|x| x.0.as_slice()))
 			.unwrap()
diff --git a/polkadot/bridges/modules/dispatch/Cargo.toml b/polkadot/bridges/modules/dispatch/Cargo.toml
index f6d9ff457f5a971385fd7031107d5a2bb6156566..98164452b836998e1e9a7191ccf4e78a3743839e 100644
--- a/polkadot/bridges/modules/dispatch/Cargo.toml
+++ b/polkadot/bridges/modules/dispatch/Cargo.toml
@@ -3,13 +3,13 @@ name = "pallet-bridge-dispatch"
 description = "A Substrate Runtime module that dispatches a bridge message, treating it simply as encoded Call"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
 log = { version = "0.4.14", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Bridge dependencies
 
diff --git a/polkadot/bridges/modules/dispatch/src/lib.rs b/polkadot/bridges/modules/dispatch/src/lib.rs
index 6765cd86604a1cacc43246d1e9a0fc48834f38cc..1e030b733205931e1fbba515241cd6c1ccfda74f 100644
--- a/polkadot/bridges/modules/dispatch/src/lib.rs
+++ b/polkadot/bridges/modules/dispatch/src/lib.rs
@@ -649,8 +649,7 @@ mod tests {
 	fn should_fail_on_weight_mismatch() {
 		new_test_ext().execute_with(|| {
 			let id = [0; 4];
-			let call =
-				Call::System(frame_system::Call::remark_with_event { remark: vec![1, 2, 3] });
+			let call = Call::System(frame_system::Call::set_heap_pages { pages: 42 });
 			let call_weight = call.get_dispatch_info().weight;
 			let mut message = prepare_root_message(call);
 			message.weight = 7;
diff --git a/polkadot/bridges/modules/grandpa/Cargo.toml b/polkadot/bridges/modules/grandpa/Cargo.toml
index 5f4057ceb099755fc71e1ec28668244df813c9fc..eac80375da1282f0577bbac1c771b12c03250038 100644
--- a/polkadot/bridges/modules/grandpa/Cargo.toml
+++ b/polkadot/bridges/modules/grandpa/Cargo.toml
@@ -2,7 +2,7 @@
 name = "pallet-bridge-grandpa"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -12,7 +12,7 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features =
 finality-grandpa = { version = "0.15.0", default-features = false }
 log = { version = "0.4.14", default-features = false }
 num-traits = { version = "0.2", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true }
 
 # Bridge Dependencies
diff --git a/polkadot/bridges/modules/grandpa/src/lib.rs b/polkadot/bridges/modules/grandpa/src/lib.rs
index bac2b19fb2aee145570613283d80c88758f6041a..947bfdc7f634c2836fef17d97e33074edb0c7ba0 100644
--- a/polkadot/bridges/modules/grandpa/src/lib.rs
+++ b/polkadot/bridges/modules/grandpa/src/lib.rs
@@ -300,7 +300,7 @@ pub mod pallet {
 	/// runtime methods may still be used to do that (i.e. democracy::referendum to update halt
 	/// flag directly or call the `halt_operations`).
 	#[pallet::storage]
-	pub(super) type PalletOwner<T: Config<I>, I: 'static = ()> =
+	pub type PalletOwner<T: Config<I>, I: 'static = ()> =
 		StorageValue<_, T::AccountId, OptionQuery>;
 
 	/// If true, all pallet transactions are failed immediately.
@@ -627,7 +627,10 @@ mod tests {
 		JustificationGeneratorParams, ALICE, BOB,
 	};
 	use codec::Encode;
-	use frame_support::{assert_err, assert_noop, assert_ok, weights::PostDispatchInfo};
+	use frame_support::{
+		assert_err, assert_noop, assert_ok, storage::generator::StorageValue,
+		weights::PostDispatchInfo,
+	};
 	use sp_runtime::{Digest, DigestItem, DispatchError};
 
 	fn initialize_substrate_bridge() {
@@ -1146,4 +1149,17 @@ mod tests {
 			);
 		})
 	}
+
+	#[test]
+	fn storage_keys_computed_properly() {
+		assert_eq!(
+			IsHalted::<TestRuntime>::storage_value_final_key().to_vec(),
+			bp_header_chain::storage_keys::is_halted_key("Grandpa").0,
+		);
+
+		assert_eq!(
+			BestFinalized::<TestRuntime>::storage_value_final_key().to_vec(),
+			bp_header_chain::storage_keys::best_finalized_hash_key("Grandpa").0,
+		);
+	}
 }
diff --git a/polkadot/bridges/modules/grandpa/src/mock.rs b/polkadot/bridges/modules/grandpa/src/mock.rs
index 4807624f396ee455ae2dc920d789a8e96c6a2855..bfc749d5230c69fc7428a5db2d426ccb6edb4111 100644
--- a/polkadot/bridges/modules/grandpa/src/mock.rs
+++ b/polkadot/bridges/modules/grandpa/src/mock.rs
@@ -107,6 +107,13 @@ impl Chain for TestBridgedChain {
 	type Balance = u64;
 	type Index = u64;
 	type Signature = Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		unreachable!()
+	}
+	fn max_extrinsic_weight() -> Weight {
+		unreachable!()
+	}
 }
 
 pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
diff --git a/polkadot/bridges/modules/grandpa/src/weights.rs b/polkadot/bridges/modules/grandpa/src/weights.rs
index c0cce2c5258d126246c14c3a228bca3354b074a7..2c4660160a0045150e8db092e691c13f0fb3dadf 100644
--- a/polkadot/bridges/modules/grandpa/src/weights.rs
+++ b/polkadot/bridges/modules/grandpa/src/weights.rs
@@ -16,14 +16,14 @@
 
 //! Autogenerated weights for `pallet_bridge_grandpa`
 //!
-//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
-//! DATE: 2021-06-03, STEPS: [50, ], REPEAT: 20
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2021-12-28, STEPS: 50, REPEAT: 20
 //! LOW RANGE: [], HIGH RANGE: []
 //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled
 //! CHAIN: Some("dev"), DB CACHE: 128
 
 // Executed Command:
-// target/release/rialto-bridge-node
+// target/release/millau-bridge-node
 // benchmark
 // --chain=dev
 // --steps=50
@@ -34,7 +34,7 @@
 // --wasm-execution=Compiled
 // --heap-pages=4096
 // --output=./modules/grandpa/src/weights.rs
-// --template=./.maintain/rialto-weight-template.hbs
+// --template=./.maintain/millau-weight-template.hbs
 
 #![allow(clippy::all)]
 #![allow(unused_parens)]
@@ -51,13 +51,13 @@ pub trait WeightInfo {
 	fn submit_finality_proof(p: u32, v: u32) -> Weight;
 }
 
-/// Weights for `pallet_bridge_grandpa` using the Rialto node and recommended hardware.
-pub struct RialtoWeight<T>(PhantomData<T>);
-impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
+/// Weights for `pallet_bridge_grandpa` using the Millau node and recommended hardware.
+pub struct MillauWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for MillauWeight<T> {
 	fn submit_finality_proof(p: u32, v: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((59_692_000 as Weight).saturating_mul(p as Weight))
-			.saturating_add((6_876_000 as Weight).saturating_mul(v as Weight))
+		(115_651_000 as Weight)
+			.saturating_add((61_465_000 as Weight).saturating_mul(p as Weight))
+			.saturating_add((3_438_000 as Weight).saturating_mul(v as Weight))
 			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
 	}
@@ -66,9 +66,9 @@ impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
 // For backwards compatibility and tests
 impl WeightInfo for () {
 	fn submit_finality_proof(p: u32, v: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((59_692_000 as Weight).saturating_mul(p as Weight))
-			.saturating_add((6_876_000 as Weight).saturating_mul(v as Weight))
+		(115_651_000 as Weight)
+			.saturating_add((61_465_000 as Weight).saturating_mul(p as Weight))
+			.saturating_add((3_438_000 as Weight).saturating_mul(v as Weight))
 			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
 	}
diff --git a/polkadot/bridges/modules/messages/Cargo.toml b/polkadot/bridges/modules/messages/Cargo.toml
index ef7bf5535543ac69ddf37c0d4cc7524f5605e75f..804f323f10b6f6b8b2e99d3a0b978e57e3f0ca9c 100644
--- a/polkadot/bridges/modules/messages/Cargo.toml
+++ b/polkadot/bridges/modules/messages/Cargo.toml
@@ -3,15 +3,15 @@ name = "pallet-bridge-messages"
 description = "Module that allows bridged chains to exchange messages using lane concept."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
+bitvec = { version = "1", default-features = false, features = ["alloc"] }
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
 log = { version = "0.4.14", default-features = false }
 num-traits = { version = "0.2", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0.101", optional = true, features = ["derive"] }
 
 # Bridge dependencies
@@ -30,8 +30,6 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
 [dev-dependencies]
-hex = "0.4"
-hex-literal = "0.3"
 sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
 pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
 
diff --git a/polkadot/bridges/modules/messages/README.md b/polkadot/bridges/modules/messages/README.md
index 062a966fad70a3ccfef72f93a67884ab76e8a535..2dc5629684242c90ace48c216b4b0cbde456e217 100644
--- a/polkadot/bridges/modules/messages/README.md
+++ b/polkadot/bridges/modules/messages/README.md
@@ -294,9 +294,8 @@ Where:
 *\* - In all benchmarks all received messages are dispatched and their dispatch cost is near to zero*
 
 *\*\* - Trie leafs are assumed to have minimal values. The proof is derived from the minimal proof
-by including more trie nodes. That's because according to `receive_message_proofs_with_large_leaf`
-and `receive_message_proofs_with_extra_nodes` benchmarks, increasing proof by including more nodes
-has slightly larger impact on performance than increasing values stored in leafs*.
+by including more trie nodes. That's because according to our additioal benchmarks, increasing proof
+by including more nodes has slightly larger impact on performance than increasing values stored in leafs*.
 
 #### Weight formula
 
diff --git a/polkadot/bridges/modules/messages/src/benchmarking.rs b/polkadot/bridges/modules/messages/src/benchmarking.rs
index 788ccc070310ea288f264b082798209fdbb5da3c..46a8150d034bde2c5fabd83012a6a2010776ca66 100644
--- a/polkadot/bridges/modules/messages/src/benchmarking.rs
+++ b/polkadot/bridges/modules/messages/src/benchmarking.rs
@@ -30,15 +30,7 @@ use bp_runtime::messages::DispatchFeePayment;
 use frame_benchmarking::{account, benchmarks_instance_pallet};
 use frame_support::{traits::Get, weights::Weight};
 use frame_system::RawOrigin;
-use sp_std::{
-	collections::{btree_map::BTreeMap, vec_deque::VecDeque},
-	convert::TryInto,
-	ops::RangeInclusive,
-	prelude::*,
-};
-
-/// Fee paid by submitter for single message delivery.
-pub const MESSAGE_FEE: u64 = 100_000_000_000;
+use sp_std::{collections::vec_deque::VecDeque, convert::TryInto, ops::RangeInclusive, prelude::*};
 
 const SEED: u32 = 0;
 
@@ -46,6 +38,7 @@ const SEED: u32 = 0;
 pub struct Pallet<T: Config<I>, I: 'static>(crate::Pallet<T, I>);
 
 /// Proof size requirements.
+#[derive(Clone, Copy, Debug)]
 pub enum ProofSize {
 	/// The proof is expected to be minimal. If value size may be changed, then it is expected to
 	/// have given size.
@@ -59,6 +52,7 @@ pub enum ProofSize {
 }
 
 /// Benchmark-specific message parameters.
+#[derive(Debug)]
 pub struct MessageParams<ThisAccountId> {
 	/// Size of the message payload.
 	pub size: u32,
@@ -67,6 +61,7 @@ pub struct MessageParams<ThisAccountId> {
 }
 
 /// Benchmark-specific message proof parameters.
+#[derive(Debug)]
 pub struct MessageProofParams {
 	/// Id of the lane.
 	pub lane: LaneId,
@@ -81,6 +76,7 @@ pub struct MessageProofParams {
 }
 
 /// Benchmark-specific message delivery proof parameters.
+#[derive(Debug)]
 pub struct MessageDeliveryProofParams<ThisChainAccountId> {
 	/// Id of the lane.
 	pub lane: LaneId,
@@ -104,6 +100,10 @@ pub trait Config<I: 'static>: crate::Config<I> {
 	fn account_balance(account: &Self::AccountId) -> Self::OutboundMessageFee;
 	/// Create given account and give it enough balance for test purposes.
 	fn endow_account(account: &Self::AccountId);
+	/// Fee paid by submitter for single message delivery.
+	fn message_fee() -> Self::OutboundMessageFee {
+		100_000_000_000_000.into()
+	}
 	/// Prepare message to send over lane.
 	fn prepare_outbound_message(
 		params: MessageParams<Self::AccountId>,
@@ -139,8 +139,10 @@ benchmarks_instance_pallet! {
 	// added.
 	send_minimal_message_worst_case {
 		let lane_id = T::bench_lane_id();
+		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
 		let sender = account("sender", 0, SEED);
 		T::endow_account(&sender);
+		T::endow_account(&relayers_fund_id);
 
 		// 'send' messages that are to be pruned when our message is sent
 		for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
@@ -155,7 +157,7 @@ benchmarks_instance_pallet! {
 	}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
+			crate::OutboundLanes::<T, I>::get(&T::bench_lane_id()).latest_generated_nonce,
 			T::MaxMessagesToPruneAtOnce::get() + 1,
 		);
 	}
@@ -170,8 +172,10 @@ benchmarks_instance_pallet! {
 	// `(send_16_kb_message_worst_case - send_1_kb_message_worst_case) / 15`.
 	send_1_kb_message_worst_case {
 		let lane_id = T::bench_lane_id();
+		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
 		let sender = account("sender", 0, SEED);
 		T::endow_account(&sender);
+		T::endow_account(&relayers_fund_id);
 
 		// 'send' messages that are to be pruned when our message is sent
 		for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
@@ -192,7 +196,7 @@ benchmarks_instance_pallet! {
 	}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
+			crate::OutboundLanes::<T, I>::get(&T::bench_lane_id()).latest_generated_nonce,
 			T::MaxMessagesToPruneAtOnce::get() + 1,
 		);
 	}
@@ -207,8 +211,10 @@ benchmarks_instance_pallet! {
 	// `(send_16_kb_message_worst_case - send_1_kb_message_worst_case) / 15`.
 	send_16_kb_message_worst_case {
 		let lane_id = T::bench_lane_id();
+		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
 		let sender = account("sender", 0, SEED);
 		T::endow_account(&sender);
+		T::endow_account(&relayers_fund_id);
 
 		// 'send' messages that are to be pruned when our message is sent
 		for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
@@ -229,7 +235,7 @@ benchmarks_instance_pallet! {
 	}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
+			crate::OutboundLanes::<T, I>::get(&T::bench_lane_id()).latest_generated_nonce,
 			T::MaxMessagesToPruneAtOnce::get() + 1,
 		);
 	}
@@ -240,8 +246,10 @@ benchmarks_instance_pallet! {
 	//
 	// Result of this benchmark is directly used by weight formula of the call.
 	maximal_increase_message_fee {
+		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
 		let sender = account("sender", 42, SEED);
 		T::endow_account(&sender);
+		T::endow_account(&relayers_fund_id);
 
 		let additional_fee = T::account_balance(&sender);
 		let lane_id = T::bench_lane_id();
@@ -259,8 +267,10 @@ benchmarks_instance_pallet! {
 	increase_message_fee {
 		let i in 0..T::maximal_message_size().try_into().unwrap_or_default();
 
+		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
 		let sender = account("sender", 42, SEED);
 		T::endow_account(&sender);
+		T::endow_account(&relayers_fund_id);
 
 		let additional_fee = T::account_balance(&sender);
 		let lane_id = T::bench_lane_id();
@@ -283,6 +293,7 @@ benchmarks_instance_pallet! {
 	receive_single_message_proof {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -297,7 +308,7 @@ benchmarks_instance_pallet! {
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
+			crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).last_delivered_nonce(),
 			21,
 		);
 		assert!(T::is_message_dispatched(21));
@@ -317,6 +328,7 @@ benchmarks_instance_pallet! {
 	receive_two_messages_proof {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -331,7 +343,7 @@ benchmarks_instance_pallet! {
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 2, dispatch_weight)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
+			crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).last_delivered_nonce(),
 			22,
 		);
 		assert!(T::is_message_dispatched(22));
@@ -351,6 +363,7 @@ benchmarks_instance_pallet! {
 	receive_single_message_proof_with_outbound_lane_state {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -368,14 +381,9 @@ benchmarks_instance_pallet! {
 		});
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
 	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
-			21,
-		);
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_confirmed_nonce(T::bench_lane_id()),
-			20,
-		);
+		let lane_state = crate::InboundLanes::<T, I>::get(&T::bench_lane_id());
+		assert_eq!(lane_state.last_delivered_nonce(), 21);
+		assert_eq!(lane_state.last_confirmed_nonce, 20);
 		assert!(T::is_message_dispatched(21));
 	}
 
@@ -391,6 +399,7 @@ benchmarks_instance_pallet! {
 	receive_single_message_proof_1_kb {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -405,7 +414,7 @@ benchmarks_instance_pallet! {
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
+			crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).last_delivered_nonce(),
 			21,
 		);
 		assert!(T::is_message_dispatched(21));
@@ -425,6 +434,7 @@ benchmarks_instance_pallet! {
 	receive_single_message_proof_16_kb {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -439,7 +449,7 @@ benchmarks_instance_pallet! {
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
+			crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).last_delivered_nonce(),
 			21,
 		);
 		assert!(T::is_message_dispatched(21));
@@ -458,6 +468,7 @@ benchmarks_instance_pallet! {
 	receive_single_prepaid_message_proof {
 		let relayer_id_on_source = T::bridged_relayer_id();
 		let relayer_id_on_target = account("relayer", 0, SEED);
+		T::endow_account(&relayer_id_on_target);
 
 		// mark messages 1..=20 as delivered
 		receive_messages::<T, I>(20);
@@ -472,7 +483,7 @@ benchmarks_instance_pallet! {
 	}: receive_messages_proof(RawOrigin::Signed(relayer_id_on_target), relayer_id_on_source, proof, 1, dispatch_weight)
 	verify {
 		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
+			crate::InboundLanes::<T, I>::get(&T::bench_lane_id()).last_delivered_nonce(),
 			21,
 		);
 		assert!(T::is_message_dispatched(21));
@@ -512,7 +523,7 @@ benchmarks_instance_pallet! {
 	verify {
 		assert_eq!(
 			T::account_balance(&relayer_id),
-			relayer_balance + MESSAGE_FEE.into(),
+			relayer_balance + T::message_fee(),
 		);
 	}
 
@@ -602,310 +613,16 @@ benchmarks_instance_pallet! {
 		ensure_relayer_rewarded::<T, I>(&relayer1_id, &relayer1_balance);
 		ensure_relayer_rewarded::<T, I>(&relayer2_id, &relayer2_balance);
 	}
-
-	//
-	// Benchmarks for manual checks.
-	//
-
-	// Benchmark `send_message` extrinsic with following conditions:
-	// * outbound lane already has state, so it needs to be read and decoded;
-	// * relayers fund account does not exists (in practice it needs to exist in production environment);
-	// * maximal number of messages is being pruned during the call;
-	// * message size varies from minimal to maximal for the target chain.
-	//
-	// Results of this benchmark may be used to check how message size affects `send_message` performance.
-	send_messages_of_various_lengths {
-		let i in 0..T::maximal_message_size().try_into().unwrap_or_default();
-
-		let lane_id = T::bench_lane_id();
-		let sender = account("sender", 0, SEED);
-		T::endow_account(&sender);
-
-		// 'send' messages that are to be pruned when our message is sent
-		for _nonce in 1..=T::MaxMessagesToPruneAtOnce::get() {
-			send_regular_message::<T, I>();
-		}
-		confirm_message_delivery::<T, I>(T::MaxMessagesToPruneAtOnce::get());
-
-		let (payload, fee) = T::prepare_outbound_message(MessageParams {
-			size: i as _,
-			sender_account: sender.clone(),
-		});
-	}: send_message(RawOrigin::Signed(sender), lane_id, payload, fee)
-	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::outbound_latest_generated_nonce(T::bench_lane_id()),
-			T::MaxMessagesToPruneAtOnce::get() + 1,
-		);
-	}
-
-	// Benchmark `receive_messages_proof` extrinsic with multiple minimal-weight messages and following conditions:
-	// * proof does not include outbound lane state proof;
-	// * inbound lane already has state, so it needs to be read and decoded;
-	// * message is successfully dispatched;
-	// * message requires all heavy checks done by dispatcher.
-	//
-	// This benchmarks gives us an approximation of single message delivery weight. It is similar to the
-	// `weight(receive_two_messages_proof) - weight(receive_single_message_proof)`. So it may be used
-	// to verify that the other approximation is correct.
-	receive_multiple_messages_proof {
-		let i in 1..64;
-
-		let relayer_id_on_source = T::bridged_relayer_id();
-		let relayer_id_on_target = account("relayer", 0, SEED);
-		let messages_count = i as _;
-
-		// mark messages 1..=20 as delivered
-		receive_messages::<T, I>(20);
-
-		let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
-			lane: T::bench_lane_id(),
-			message_nonces: 21..=(20 + i as MessageNonce),
-			outbound_lane_data: None,
-			size: ProofSize::Minimal(EXPECTED_DEFAULT_MESSAGE_LENGTH),
-			dispatch_fee_payment: DispatchFeePayment::AtTargetChain,
-		});
-	}: receive_messages_proof(
-		RawOrigin::Signed(relayer_id_on_target),
-		relayer_id_on_source,
-		proof,
-		messages_count,
-		dispatch_weight
-	)
-	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
-			20 + i as MessageNonce,
-		);
-	}
-
-	// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following conditions:
-	// * proof does not include outbound lane state proof;
-	// * inbound lane already has state, so it needs to be read and decoded;
-	// * message is successfully dispatched;
-	// * message requires all heavy checks done by dispatcher.
-	//
-	// Results of this benchmark may be used to check how proof size affects `receive_message_proof` performance.
-	receive_message_proofs_with_extra_nodes {
-		let i in 0..T::maximal_message_size();
-
-		let relayer_id_on_source = T::bridged_relayer_id();
-		let relayer_id_on_target = account("relayer", 0, SEED);
-		let messages_count = 1u32;
-
-		// mark messages 1..=20 as delivered
-		receive_messages::<T, I>(20);
-
-		let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
-			lane: T::bench_lane_id(),
-			message_nonces: 21..=21,
-			outbound_lane_data: None,
-			size: ProofSize::HasExtraNodes(i as _),
-			dispatch_fee_payment: DispatchFeePayment::AtTargetChain,
-		});
-	}: receive_messages_proof(
-		RawOrigin::Signed(relayer_id_on_target),
-		relayer_id_on_source,
-		proof,
-		messages_count,
-		dispatch_weight
-	)
-	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
-			21,
-		);
-	}
-
-	// Benchmark `receive_messages_proof` extrinsic with single minimal-weight message and following conditions:
-	// * proof does not include outbound lane state proof;
-	// * inbound lane already has state, so it needs to be read and decoded;
-	// * message is successfully dispatched;
-	// * message requires all heavy checks done by dispatcher.
-	//
-	// Results of this benchmark may be used to check how message size affects `receive_message_proof` performance.
-	receive_message_proofs_with_large_leaf {
-		let i in 0..T::maximal_message_size();
-
-		let relayer_id_on_source = T::bridged_relayer_id();
-		let relayer_id_on_target = account("relayer", 0, SEED);
-		let messages_count = 1u32;
-
-		// mark messages 1..=20 as delivered
-		receive_messages::<T, I>(20);
-
-		let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
-			lane: T::bench_lane_id(),
-			message_nonces: 21..=21,
-			outbound_lane_data: None,
-			size: ProofSize::HasLargeLeaf(i as _),
-			dispatch_fee_payment: DispatchFeePayment::AtTargetChain,
-		});
-	}: receive_messages_proof(
-		RawOrigin::Signed(relayer_id_on_target),
-		relayer_id_on_source,
-		proof,
-		messages_count,
-		dispatch_weight
-	)
-	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
-			21,
-		);
-	}
-
-	// Benchmark `receive_messages_proof` extrinsic with multiple minimal-weight messages and following conditions:
-	// * proof includes outbound lane state proof;
-	// * inbound lane already has state, so it needs to be read and decoded;
-	// * message is successfully dispatched;
-	// * message requires all heavy checks done by dispatcher.
-	//
-	// This benchmarks gives us an approximation of outbound lane state delivery weight. It is similar to the
-	// `weight(receive_single_message_proof_with_outbound_lane_state) - weight(receive_single_message_proof)`.
-	// So it may be used to verify that the other approximation is correct.
-	receive_multiple_messages_proof_with_outbound_lane_state {
-		let i in 1..128;
-
-		let relayer_id_on_source = T::bridged_relayer_id();
-		let relayer_id_on_target = account("relayer", 0, SEED);
-		let messages_count = i as _;
-
-		// mark messages 1..=20 as delivered
-		receive_messages::<T, I>(20);
-
-		let (proof, dispatch_weight) = T::prepare_message_proof(MessageProofParams {
-			lane: T::bench_lane_id(),
-			message_nonces: 21..=20 + i as MessageNonce,
-			outbound_lane_data: Some(OutboundLaneData {
-				oldest_unpruned_nonce: 21,
-				latest_received_nonce: 20,
-				latest_generated_nonce: 21,
-			}),
-			size: ProofSize::Minimal(0),
-			dispatch_fee_payment: DispatchFeePayment::AtTargetChain,
-		});
-	}: receive_messages_proof(
-		RawOrigin::Signed(relayer_id_on_target),
-		relayer_id_on_source,
-		proof,
-		messages_count,
-		dispatch_weight
-	)
-	verify {
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_received_nonce(T::bench_lane_id()),
-			20 + i as MessageNonce,
-		);
-		assert_eq!(
-			crate::Pallet::<T, I>::inbound_latest_confirmed_nonce(T::bench_lane_id()),
-			20,
-		);
-	}
-
-	// Benchmark `receive_messages_delivery_proof` extrinsic where single relayer delivers multiple messages.
-	receive_delivery_proof_for_multiple_messages_by_single_relayer {
-		// there actually should be used value of `MaxUnrewardedRelayerEntriesAtInboundLane` from the bridged
-		// chain, but we're more interested in additional weight/message than in max weight
-		let i in 1..T::MaxUnrewardedRelayerEntriesAtInboundLane::get()
-			.try_into()
-			.expect("Value of MaxUnrewardedRelayerEntriesAtInboundLane is too large");
-
-		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
-		let relayer_id: T::AccountId = account("relayer", 0, SEED);
-		let relayer_balance = T::account_balance(&relayer_id);
-		T::endow_account(&relayers_fund_id);
-
-		// send messages that we're going to confirm
-		for _ in 1..=i {
-			send_regular_message::<T, I>();
-		}
-
-		let relayers_state = UnrewardedRelayersState {
-			unrewarded_relayer_entries: 1,
-			messages_in_oldest_entry: 1,
-			total_messages: i as MessageNonce,
-		};
-		let mut delivered_messages = DeliveredMessages::new(1, true);
-		for nonce in 2..=i {
-			delivered_messages.note_dispatched_message(true);
-		}
-		let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
-			lane: T::bench_lane_id(),
-			inbound_lane_data: InboundLaneData {
-				relayers: vec![UnrewardedRelayer {
-					relayer: relayer_id.clone(),
-					messages: delivered_messages,
-				}].into_iter().collect(),
-				last_confirmed_nonce: 0,
-			},
-			size: ProofSize::Minimal(0),
-		});
-	}: receive_messages_delivery_proof(RawOrigin::Signed(relayer_id.clone()), proof, relayers_state)
-	verify {
-		ensure_relayer_rewarded::<T, I>(&relayer_id, &relayer_balance);
-	}
-
-	// Benchmark `receive_messages_delivery_proof` extrinsic where every relayer delivers single messages.
-	receive_delivery_proof_for_multiple_messages_by_multiple_relayers {
-		// there actually should be used value of `MaxUnconfirmedMessagesAtInboundLane` from the bridged
-		// chain, but we're more interested in additional weight/message than in max weight
-		let i in 1..T::MaxUnconfirmedMessagesAtInboundLane::get()
-			.try_into()
-			.expect("Value of MaxUnconfirmedMessagesAtInboundLane is too large ");
-
-		let relayers_fund_id = crate::relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>();
-		let confirmation_relayer_id = account("relayer", 0, SEED);
-		let relayers: BTreeMap<T::AccountId, T::OutboundMessageFee> = (1..=i)
-			.map(|j| {
-				let relayer_id = account("relayer", j + 1, SEED);
-				let relayer_balance = T::account_balance(&relayer_id);
-				(relayer_id, relayer_balance)
-			})
-			.collect();
-		T::endow_account(&relayers_fund_id);
-
-		// send messages that we're going to confirm
-		for _ in 1..=i {
-			send_regular_message::<T, I>();
-		}
-
-		let relayers_state = UnrewardedRelayersState {
-			unrewarded_relayer_entries: i as MessageNonce,
-			messages_in_oldest_entry: 1,
-			total_messages: i as MessageNonce,
-		};
-		let proof = T::prepare_message_delivery_proof(MessageDeliveryProofParams {
-			lane: T::bench_lane_id(),
-			inbound_lane_data: InboundLaneData {
-				relayers: relayers
-					.keys()
-					.enumerate()
-					.map(|(j, relayer)| UnrewardedRelayer {
-						relayer: relayer.clone(),
-						messages: DeliveredMessages::new(j as MessageNonce + 1, true),
-					})
-					.collect(),
-				last_confirmed_nonce: 0,
-			},
-			size: ProofSize::Minimal(0),
-		});
-	}: receive_messages_delivery_proof(RawOrigin::Signed(confirmation_relayer_id), proof, relayers_state)
-	verify {
-		for (relayer_id, prev_balance) in relayers {
-			ensure_relayer_rewarded::<T, I>(&relayer_id, &prev_balance);
-		}
-	}
 }
 
 fn send_regular_message<T: Config<I>, I: 'static>() {
 	let mut outbound_lane = outbound_lane::<T, I>(T::bench_lane_id());
-	outbound_lane.send_message(MessageData { payload: vec![], fee: MESSAGE_FEE.into() });
+	outbound_lane.send_message(MessageData { payload: vec![], fee: T::message_fee() });
 }
 
 fn send_regular_message_with_payload<T: Config<I>, I: 'static>(payload: Vec<u8>) {
 	let mut outbound_lane = outbound_lane::<T, I>(T::bench_lane_id());
-	outbound_lane.send_message(MessageData { payload, fee: MESSAGE_FEE.into() });
+	outbound_lane.send_message(MessageData { payload, fee: T::message_fee() });
 }
 
 fn confirm_message_delivery<T: Config<I>, I: 'static>(nonce: MessageNonce) {
diff --git a/polkadot/bridges/modules/messages/src/instant_payments.rs b/polkadot/bridges/modules/messages/src/instant_payments.rs
index d67b82ade8d2dab8c2e6bd4a8f68db0946c562e9..2a620a952225848a43b1133615e9cfbc1ddec22e 100644
--- a/polkadot/bridges/modules/messages/src/instant_payments.rs
+++ b/polkadot/bridges/modules/messages/src/instant_payments.rs
@@ -22,7 +22,7 @@
 use crate::OutboundMessages;
 
 use bp_messages::{
-	source_chain::{MessageDeliveryAndDispatchPayment, RelayersRewards, Sender},
+	source_chain::{MessageDeliveryAndDispatchPayment, RelayersRewards, SenderOrigin},
 	LaneId, MessageKey, MessageNonce, UnrewardedRelayer,
 };
 use codec::Encode;
@@ -31,6 +31,10 @@ use num_traits::{SaturatingAdd, Zero};
 use sp_runtime::traits::Saturating;
 use sp_std::{collections::vec_deque::VecDeque, fmt::Debug, ops::RangeInclusive};
 
+/// Error that occurs when message fee is non-zero, but payer is not defined.
+const NON_ZERO_MESSAGE_FEE_CANT_BE_PAID_BY_NONE: &str =
+	"Non-zero message fee can't be paid by <None>";
+
 /// Instant message payments made in given currency.
 ///
 /// The balance is initially reserved in a special `relayers-fund` account, and transferred
@@ -44,42 +48,50 @@ use sp_std::{collections::vec_deque::VecDeque, fmt::Debug, ops::RangeInclusive};
 /// to the relayer account.
 /// NOTE It's within relayer's interest to keep their balance above ED as well, to make sure they
 /// can receive the payment.
-pub struct InstantCurrencyPayments<T, I, Currency, GetConfirmationFee, RootAccount> {
-	_phantom: sp_std::marker::PhantomData<(T, I, Currency, GetConfirmationFee, RootAccount)>,
+pub struct InstantCurrencyPayments<T, I, Currency, GetConfirmationFee> {
+	_phantom: sp_std::marker::PhantomData<(T, I, Currency, GetConfirmationFee)>,
 }
 
-impl<T, I, Currency, GetConfirmationFee, RootAccount>
-	MessageDeliveryAndDispatchPayment<T::AccountId, Currency::Balance>
-	for InstantCurrencyPayments<T, I, Currency, GetConfirmationFee, RootAccount>
+impl<T, I, Currency, GetConfirmationFee>
+	MessageDeliveryAndDispatchPayment<T::Origin, T::AccountId, Currency::Balance>
+	for InstantCurrencyPayments<T, I, Currency, GetConfirmationFee>
 where
 	T: frame_system::Config + crate::Config<I>,
 	I: 'static,
+	T::Origin: SenderOrigin<T::AccountId>,
 	Currency: CurrencyT<T::AccountId, Balance = T::OutboundMessageFee>,
 	Currency::Balance: From<MessageNonce>,
 	GetConfirmationFee: Get<Currency::Balance>,
-	RootAccount: Get<Option<T::AccountId>>,
 {
 	type Error = &'static str;
 
 	fn pay_delivery_and_dispatch_fee(
-		submitter: &Sender<T::AccountId>,
+		submitter: &T::Origin,
 		fee: &Currency::Balance,
 		relayer_fund_account: &T::AccountId,
 	) -> Result<(), Self::Error> {
+		let submitter_account = match submitter.linked_account() {
+			Some(submitter_account) => submitter_account,
+			None if !fee.is_zero() => {
+				// if we'll accept some message that has declared that the `fee` has been paid but
+				// it isn't actually paid, then it'll lead to problems with delivery confirmation
+				// payments (see `pay_relayer_rewards` && `confirmation_relayer` in particular)
+				return Err(NON_ZERO_MESSAGE_FEE_CANT_BE_PAID_BY_NONE)
+			},
+			None => {
+				// message lane verifier has accepted the message before, so this message
+				// is unpaid **by design**
+				// => let's just do nothing
+				return Ok(())
+			},
+		};
+
 		if !frame_system::Pallet::<T>::account_exists(relayer_fund_account) {
 			return Err("The relayer fund account must exist for the message lanes pallet to work correctly.");
 		}
 
-		let root_account = RootAccount::get();
-		let account = match submitter {
-			Sender::Signed(submitter) => submitter,
-			Sender::Root | Sender::None => root_account
-				.as_ref()
-				.ok_or("Sending messages using Root or None origin is disallowed.")?,
-		};
-
 		Currency::transfer(
-			account,
+			&submitter_account,
 			relayer_fund_account,
 			*fee,
 			// it's fine for the submitter to go below Existential Deposit and die.
@@ -226,7 +238,9 @@ fn pay_relayer_reward<Currency, AccountId>(
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use crate::mock::{run_test, AccountId as TestAccountId, Balance as TestBalance, TestRuntime};
+	use crate::mock::{
+		run_test, AccountId as TestAccountId, Balance as TestBalance, Origin, TestRuntime,
+	};
 	use bp_messages::source_chain::RelayerRewards;
 
 	type Balances = pallet_balances::Pallet<TestRuntime>;
@@ -245,6 +259,48 @@ mod tests {
 		.collect()
 	}
 
+	#[test]
+	fn pay_delivery_and_dispatch_fee_fails_on_non_zero_fee_and_unknown_payer() {
+		frame_support::parameter_types! {
+			const GetConfirmationFee: TestBalance = 0;
+		};
+
+		run_test(|| {
+			let result = InstantCurrencyPayments::<
+				TestRuntime,
+				(),
+				Balances,
+				GetConfirmationFee,
+			>::pay_delivery_and_dispatch_fee(
+				&Origin::root(),
+				&100,
+				&RELAYERS_FUND_ACCOUNT,
+			);
+			assert_eq!(result, Err(NON_ZERO_MESSAGE_FEE_CANT_BE_PAID_BY_NONE));
+		});
+	}
+
+	#[test]
+	fn pay_delivery_and_dispatch_succeeds_on_zero_fee_and_unknown_payer() {
+		frame_support::parameter_types! {
+			const GetConfirmationFee: TestBalance = 0;
+		};
+
+		run_test(|| {
+			let result = InstantCurrencyPayments::<
+				TestRuntime,
+				(),
+				Balances,
+				GetConfirmationFee,
+			>::pay_delivery_and_dispatch_fee(
+				&Origin::root(),
+				&0,
+				&RELAYERS_FUND_ACCOUNT,
+			);
+			assert!(result.is_ok());
+		});
+	}
+
 	#[test]
 	fn confirmation_relayer_is_rewarded_if_it_has_also_delivered_messages() {
 		run_test(|| {
diff --git a/polkadot/bridges/modules/messages/src/lib.rs b/polkadot/bridges/modules/messages/src/lib.rs
index 498960b6459fa0fcddf57863f52f67b9b851c31f..9f5f9d438c4168e82e9d267032ca2403aa2140c7 100644
--- a/polkadot/bridges/modules/messages/src/lib.rs
+++ b/polkadot/bridges/modules/messages/src/lib.rs
@@ -170,12 +170,14 @@ pub mod pallet {
 		type TargetHeaderChain: TargetHeaderChain<Self::OutboundPayload, Self::AccountId>;
 		/// Message payload verifier.
 		type LaneMessageVerifier: LaneMessageVerifier<
+			Self::Origin,
 			Self::AccountId,
 			Self::OutboundPayload,
 			Self::OutboundMessageFee,
 		>;
 		/// Message delivery payment.
 		type MessageDeliveryAndDispatchPayment: MessageDeliveryAndDispatchPayment<
+			Self::Origin,
 			Self::AccountId,
 			Self::OutboundMessageFee,
 		>;
@@ -276,16 +278,12 @@ pub mod pallet {
 			payload: T::OutboundPayload,
 			delivery_and_dispatch_fee: T::OutboundMessageFee,
 		) -> DispatchResultWithPostInfo {
-			crate::send_message::<T, I>(
-				origin.into().map_err(|_| BadOrigin)?,
-				lane_id,
-				payload,
-				delivery_and_dispatch_fee,
+			crate::send_message::<T, I>(origin, lane_id, payload, delivery_and_dispatch_fee).map(
+				|sent_message| PostDispatchInfo {
+					actual_weight: Some(sent_message.weight),
+					pays_fee: Pays::Yes,
+				},
 			)
-			.map(|sent_message| PostDispatchInfo {
-				actual_weight: Some(sent_message.weight),
-				pays_fee: Pays::Yes,
-			})
 		}
 
 		/// Pay additional fee for the message.
@@ -313,17 +311,15 @@ pub mod pallet {
 			);
 
 			// withdraw additional fee from submitter
-			let submitter = origin.into().map_err(|_| BadOrigin)?;
 			T::MessageDeliveryAndDispatchPayment::pay_delivery_and_dispatch_fee(
-				&submitter,
+				&origin,
 				&additional_fee,
 				&relayer_fund_account_id::<T::AccountId, T::AccountIdConverter>(),
 			)
 			.map_err(|err| {
 				log::trace!(
 					target: "runtime::bridge-messages",
-					"Submitter {:?} can't pay additional fee {:?} for the message {:?}/{:?} to {:?}: {:?}",
-					submitter,
+					"Submitter can't pay additional fee {:?} for the message {:?}/{:?} to {:?}: {:?}",
 					additional_fee,
 					lane_id,
 					nonce,
@@ -764,67 +760,6 @@ pub mod pallet {
 		) -> Option<MessageData<T::OutboundMessageFee>> {
 			OutboundMessages::<T, I>::get(MessageKey { lane_id: lane, nonce })
 		}
-
-		/// Get nonce of the latest generated message at given outbound lane.
-		pub fn outbound_latest_generated_nonce(lane: LaneId) -> MessageNonce {
-			OutboundLanes::<T, I>::get(&lane).latest_generated_nonce
-		}
-
-		/// Get nonce of the latest confirmed message at given outbound lane.
-		pub fn outbound_latest_received_nonce(lane: LaneId) -> MessageNonce {
-			OutboundLanes::<T, I>::get(&lane).latest_received_nonce
-		}
-
-		/// Get nonce of the latest received message at given inbound lane.
-		pub fn inbound_latest_received_nonce(lane: LaneId) -> MessageNonce {
-			InboundLanes::<T, I>::get(&lane).last_delivered_nonce()
-		}
-
-		/// Get nonce of the latest confirmed message at given inbound lane.
-		pub fn inbound_latest_confirmed_nonce(lane: LaneId) -> MessageNonce {
-			InboundLanes::<T, I>::get(&lane).last_confirmed_nonce
-		}
-
-		/// Get state of unrewarded relayers set.
-		pub fn inbound_unrewarded_relayers_state(
-			lane: bp_messages::LaneId,
-		) -> bp_messages::UnrewardedRelayersState {
-			let relayers = InboundLanes::<T, I>::get(&lane).relayers;
-			bp_messages::UnrewardedRelayersState {
-				unrewarded_relayer_entries: relayers.len() as _,
-				messages_in_oldest_entry: relayers
-					.front()
-					.map(|entry| 1 + entry.messages.end - entry.messages.begin)
-					.unwrap_or(0),
-				total_messages: total_unrewarded_messages(&relayers).unwrap_or(MessageNonce::MAX),
-			}
-		}
-	}
-}
-
-/// Getting storage keys for messages and lanes states. These keys are normally used when building
-/// messages and lanes states proofs.
-pub mod storage_keys {
-	use super::*;
-	use sp_core::storage::StorageKey;
-
-	/// Storage key of the outbound message in the runtime storage.
-	pub fn message_key(pallet_prefix: &str, lane: &LaneId, nonce: MessageNonce) -> StorageKey {
-		bp_runtime::storage_map_final_key_blake2_128concat(
-			pallet_prefix,
-			"OutboundMessages",
-			&MessageKey { lane_id: *lane, nonce }.encode(),
-		)
-	}
-
-	/// Storage key of the outbound message lane state in the runtime storage.
-	pub fn outbound_lane_data_key(pallet_prefix: &str, lane: &LaneId) -> StorageKey {
-		bp_runtime::storage_map_final_key_blake2_128concat(pallet_prefix, "OutboundLanes", lane)
-	}
-
-	/// Storage key of the inbound message lane state in the runtime storage.
-	pub fn inbound_lane_data_key(pallet_prefix: &str, lane: &LaneId) -> StorageKey {
-		bp_runtime::storage_map_final_key_blake2_128concat(pallet_prefix, "InboundLanes", lane)
 	}
 }
 
@@ -841,6 +776,7 @@ pub fn relayer_fund_account_id<AccountId, AccountIdConverter: Convert<H256, Acco
 
 impl<T, I>
 	bp_messages::source_chain::MessagesBridge<
+		T::Origin,
 		T::AccountId,
 		T::OutboundMessageFee,
 		T::OutboundPayload,
@@ -852,7 +788,7 @@ where
 	type Error = sp_runtime::DispatchErrorWithPostInfo<PostDispatchInfo>;
 
 	fn send_message(
-		sender: bp_messages::source_chain::Sender<T::AccountId>,
+		sender: T::Origin,
 		lane: LaneId,
 		message: T::OutboundPayload,
 		delivery_and_dispatch_fee: T::OutboundMessageFee,
@@ -863,7 +799,7 @@ where
 
 /// Function that actually sends message.
 fn send_message<T: Config<I>, I: 'static>(
-	submitter: bp_messages::source_chain::Sender<T::AccountId>,
+	submitter: T::Origin,
 	lane_id: LaneId,
 	payload: T::OutboundPayload,
 	delivery_and_dispatch_fee: T::OutboundMessageFee,
@@ -917,9 +853,8 @@ fn send_message<T: Config<I>, I: 'static>(
 	.map_err(|err| {
 		log::trace!(
 			target: "runtime::bridge-messages",
-			"Message to lane {:?} is rejected because submitter {:?} is unable to pay fee {:?}: {:?}",
+			"Message to lane {:?} is rejected because submitter is unable to pay fee {:?}: {:?}",
 			lane_id,
-			submitter,
 			delivery_and_dispatch_fee,
 			err,
 		);
@@ -1160,9 +1095,12 @@ mod tests {
 		REGULAR_PAYLOAD, TEST_LANE_ID, TEST_RELAYER_A, TEST_RELAYER_B,
 	};
 	use bp_messages::{UnrewardedRelayer, UnrewardedRelayersState};
-	use frame_support::{assert_noop, assert_ok, weights::Weight};
+	use frame_support::{
+		assert_noop, assert_ok,
+		storage::generator::{StorageMap, StorageValue},
+		weights::Weight,
+	};
 	use frame_system::{EventRecord, Pallet as System, Phase};
-	use hex_literal::hex;
 	use sp_runtime::DispatchError;
 
 	fn get_ready_for_events() {
@@ -1170,6 +1108,20 @@ mod tests {
 		System::<TestRuntime>::reset_events();
 	}
 
+	fn inbound_unrewarded_relayers_state(
+		lane: bp_messages::LaneId,
+	) -> bp_messages::UnrewardedRelayersState {
+		let relayers = InboundLanes::<TestRuntime, ()>::get(&lane).relayers;
+		bp_messages::UnrewardedRelayersState {
+			unrewarded_relayer_entries: relayers.len() as _,
+			messages_in_oldest_entry: relayers
+				.front()
+				.map(|entry| 1 + entry.messages.end - entry.messages.begin)
+				.unwrap_or(0),
+			total_messages: total_unrewarded_messages(&relayers).unwrap_or(MessageNonce::MAX),
+		}
+	}
+
 	fn send_regular_message() -> Weight {
 		get_ready_for_events();
 
@@ -1614,7 +1566,7 @@ mod tests {
 				},
 			);
 			assert_eq!(
-				Pallet::<TestRuntime>::inbound_unrewarded_relayers_state(TEST_LANE_ID),
+				inbound_unrewarded_relayers_state(TEST_LANE_ID),
 				UnrewardedRelayersState {
 					unrewarded_relayer_entries: 2,
 					messages_in_oldest_entry: 1,
@@ -1649,7 +1601,7 @@ mod tests {
 				},
 			);
 			assert_eq!(
-				Pallet::<TestRuntime>::inbound_unrewarded_relayers_state(TEST_LANE_ID),
+				inbound_unrewarded_relayers_state(TEST_LANE_ID),
 				UnrewardedRelayersState {
 					unrewarded_relayer_entries: 2,
 					messages_in_oldest_entry: 1,
@@ -1890,45 +1842,6 @@ mod tests {
 		});
 	}
 
-	#[test]
-	fn storage_message_key_computed_properly() {
-		// If this test fails, then something has been changed in module storage that is breaking
-		// all previously crafted messages proofs.
-		let storage_key = storage_keys::message_key("BridgeMessages", &*b"test", 42).0;
-		assert_eq!(
-			storage_key,
-			hex!("dd16c784ebd3390a9bc0357c7511ed018a395e6242c6813b196ca31ed0547ea79446af0e09063bd4a7874aef8a997cec746573742a00000000000000").to_vec(),
-			"Unexpected storage key: {}",
-			hex::encode(&storage_key),
-		);
-	}
-
-	#[test]
-	fn outbound_lane_data_key_computed_properly() {
-		// If this test fails, then something has been changed in module storage that is breaking
-		// all previously crafted outbound lane state proofs.
-		let storage_key = storage_keys::outbound_lane_data_key("BridgeMessages", &*b"test").0;
-		assert_eq!(
-			storage_key,
-			hex!("dd16c784ebd3390a9bc0357c7511ed0196c246acb9b55077390e3ca723a0ca1f44a8995dd50b6657a037a7839304535b74657374").to_vec(),
-			"Unexpected storage key: {}",
-			hex::encode(&storage_key),
-		);
-	}
-
-	#[test]
-	fn inbound_lane_data_key_computed_properly() {
-		// If this test fails, then something has been changed in module storage that is breaking
-		// all previously crafted inbound lane state proofs.
-		let storage_key = storage_keys::inbound_lane_data_key("BridgeMessages", &*b"test").0;
-		assert_eq!(
-			storage_key,
-			hex!("dd16c784ebd3390a9bc0357c7511ed01e5f83cf83f2127eb47afdc35d6e43fab44a8995dd50b6657a037a7839304535b74657374").to_vec(),
-			"Unexpected storage key: {}",
-			hex::encode(&storage_key),
-		);
-	}
-
 	#[test]
 	fn actual_dispatch_weight_does_not_overlow() {
 		run_test(|| {
@@ -2362,4 +2275,30 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn storage_keys_computed_properly() {
+		assert_eq!(
+			PalletOperatingMode::<TestRuntime>::storage_value_final_key().to_vec(),
+			bp_messages::storage_keys::operating_mode_key("Messages").0,
+		);
+
+		assert_eq!(
+			OutboundMessages::<TestRuntime>::storage_map_final_key(MessageKey {
+				lane_id: TEST_LANE_ID,
+				nonce: 42
+			}),
+			bp_messages::storage_keys::message_key("Messages", &TEST_LANE_ID, 42).0,
+		);
+
+		assert_eq!(
+			OutboundLanes::<TestRuntime>::storage_map_final_key(TEST_LANE_ID),
+			bp_messages::storage_keys::outbound_lane_data_key("Messages", &TEST_LANE_ID).0,
+		);
+
+		assert_eq!(
+			InboundLanes::<TestRuntime>::storage_map_final_key(TEST_LANE_ID),
+			bp_messages::storage_keys::inbound_lane_data_key("Messages", &TEST_LANE_ID).0,
+		);
+	}
 }
diff --git a/polkadot/bridges/modules/messages/src/mock.rs b/polkadot/bridges/modules/messages/src/mock.rs
index 01b51e6dda3af40d96e060e5ab00b7952bfd8be4..75dcce8df04495e29fb0b0a2cea742e6ee61985d 100644
--- a/polkadot/bridges/modules/messages/src/mock.rs
+++ b/polkadot/bridges/modules/messages/src/mock.rs
@@ -23,7 +23,7 @@ use bitvec::prelude::*;
 use bp_messages::{
 	source_chain::{
 		LaneMessageVerifier, MessageDeliveryAndDispatchPayment, OnDeliveryConfirmed,
-		OnMessageAccepted, Sender, TargetHeaderChain,
+		OnMessageAccepted, SenderOrigin, TargetHeaderChain,
 	},
 	target_chain::{
 		DispatchMessage, MessageDispatch, ProvedLaneMessages, ProvedMessages, SourceHeaderChain,
@@ -194,6 +194,16 @@ impl Config for TestRuntime {
 	type BridgedChainId = TestBridgedChainId;
 }
 
+impl SenderOrigin<AccountId> for Origin {
+	fn linked_account(&self) -> Option<AccountId> {
+		match self.caller {
+			OriginCaller::system(frame_system::RawOrigin::Signed(ref submitter)) =>
+				Some(submitter.clone()),
+			_ => None,
+		}
+	}
+}
+
 impl Size for TestPayload {
 	fn size_hint(&self) -> u32 {
 		16 + self.extra.len() as u32
@@ -294,11 +304,13 @@ impl TargetHeaderChain<TestPayload, TestRelayer> for TestTargetHeaderChain {
 #[derive(Debug, Default)]
 pub struct TestLaneMessageVerifier;
 
-impl LaneMessageVerifier<AccountId, TestPayload, TestMessageFee> for TestLaneMessageVerifier {
+impl LaneMessageVerifier<Origin, AccountId, TestPayload, TestMessageFee>
+	for TestLaneMessageVerifier
+{
 	type Error = &'static str;
 
 	fn verify_message(
-		_submitter: &Sender<AccountId>,
+		_submitter: &Origin,
 		delivery_and_dispatch_fee: &TestMessageFee,
 		_lane: &LaneId,
 		_lane_outbound_data: &OutboundLaneData,
@@ -324,8 +336,8 @@ impl TestMessageDeliveryAndDispatchPayment {
 
 	/// Returns true if given fee has been paid by given submitter.
 	pub fn is_fee_paid(submitter: AccountId, fee: TestMessageFee) -> bool {
-		frame_support::storage::unhashed::get(b":message-fee:") ==
-			Some((Sender::Signed(submitter), fee))
+		let raw_origin: Result<frame_system::RawOrigin<_>, _> = Origin::signed(submitter).into();
+		frame_support::storage::unhashed::get(b":message-fee:") == Some((raw_origin.unwrap(), fee))
 	}
 
 	/// Returns true if given relayer has been rewarded with given balance. The reward-paid flag is
@@ -336,13 +348,13 @@ impl TestMessageDeliveryAndDispatchPayment {
 	}
 }
 
-impl MessageDeliveryAndDispatchPayment<AccountId, TestMessageFee>
+impl MessageDeliveryAndDispatchPayment<Origin, AccountId, TestMessageFee>
 	for TestMessageDeliveryAndDispatchPayment
 {
 	type Error = &'static str;
 
 	fn pay_delivery_and_dispatch_fee(
-		submitter: &Sender<AccountId>,
+		submitter: &Origin,
 		fee: &TestMessageFee,
 		_relayer_fund_account: &AccountId,
 	) -> Result<(), Self::Error> {
@@ -350,7 +362,8 @@ impl MessageDeliveryAndDispatchPayment<AccountId, TestMessageFee>
 			return Err(TEST_ERROR)
 		}
 
-		frame_support::storage::unhashed::put(b":message-fee:", &(submitter, fee));
+		let raw_origin: Result<frame_system::RawOrigin<_>, _> = submitter.clone().into();
+		frame_support::storage::unhashed::put(b":message-fee:", &(raw_origin.unwrap(), fee));
 		Ok(())
 	}
 
diff --git a/polkadot/bridges/modules/messages/src/weights.rs b/polkadot/bridges/modules/messages/src/weights.rs
index 9dce11168fbbc2cdaa347bac2bae61989cac313f..462f768a08b383f045871bce7f82aa65b11f3e69 100644
--- a/polkadot/bridges/modules/messages/src/weights.rs
+++ b/polkadot/bridges/modules/messages/src/weights.rs
@@ -16,14 +16,14 @@
 
 //! Autogenerated weights for `pallet_bridge_messages`
 //!
-//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0
-//! DATE: 2021-06-18, STEPS: [50, ], REPEAT: 20
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2021-12-28, STEPS: 50, REPEAT: 20
 //! LOW RANGE: [], HIGH RANGE: []
 //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled
 //! CHAIN: Some("dev"), DB CACHE: 128
 
 // Executed Command:
-// target/release/rialto-bridge-node
+// target/release/millau-bridge-node
 // benchmark
 // --chain=dev
 // --steps=50
@@ -34,7 +34,7 @@
 // --wasm-execution=Compiled
 // --heap-pages=4096
 // --output=./modules/messages/src/weights.rs
-// --template=./.maintain/rialto-weight-template.hbs
+// --template=./.maintain/millau-weight-template.hbs
 
 #![allow(clippy::all)]
 #![allow(unused_parens)]
@@ -62,252 +62,155 @@ pub trait WeightInfo {
 	fn receive_delivery_proof_for_single_message() -> Weight;
 	fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight;
 	fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight;
-	fn send_messages_of_various_lengths(i: u32) -> Weight;
-	fn receive_multiple_messages_proof(i: u32) -> Weight;
-	fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight;
-	fn receive_message_proofs_with_large_leaf(i: u32) -> Weight;
-	fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight;
-	fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight;
-	fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight;
 }
 
-/// Weights for `pallet_bridge_messages` using the Rialto node and recommended hardware.
-pub struct RialtoWeight<T>(PhantomData<T>);
-impl<T: frame_system::Config> WeightInfo for RialtoWeight<T> {
+/// Weights for `pallet_bridge_messages` using the Millau node and recommended hardware.
+pub struct MillauWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for MillauWeight<T> {
 	fn send_minimal_message_worst_case() -> Weight {
-		(159_305_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(117_480_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().writes(12 as Weight))
 	}
 	fn send_1_kb_message_worst_case() -> Weight {
-		(164_394_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(128_391_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().writes(12 as Weight))
 	}
 	fn send_16_kb_message_worst_case() -> Weight {
-		(223_521_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(149_149_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().writes(12 as Weight))
 	}
 	fn maximal_increase_message_fee() -> Weight {
-		(6_781_470_000 as Weight)
+		(6_015_058_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn increase_message_fee(i: u32) -> Weight {
-		(114_963_000 as Weight)
-			.saturating_add((6_000 as Weight).saturating_mul(i as Weight))
+		(0 as Weight)
+			.saturating_add((2_000 as Weight).saturating_mul(i as Weight))
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof() -> Weight {
-		(206_769_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(179_892_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_two_messages_proof() -> Weight {
-		(343_982_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(291_793_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
-		(223_738_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(192_191_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_1_kb() -> Weight {
-		(235_369_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(202_104_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_16_kb() -> Weight {
-		(510_338_000 as Weight)
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
+		(357_144_000 as Weight)
+			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_prepaid_message_proof() -> Weight {
-		(141_536_000 as Weight)
+		(122_648_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 	fn receive_delivery_proof_for_single_message() -> Weight {
-		(128_805_000 as Weight)
+		(107_631_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(6 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
-		(137_143_000 as Weight)
+		(113_885_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
-		(193_108_000 as Weight)
+		(155_151_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(8 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
-	fn send_messages_of_various_lengths(i: u32) -> Weight {
-		(133_632_000 as Weight)
-			.saturating_add((4_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(12 as Weight))
-	}
-	fn receive_multiple_messages_proof(i: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((145_006_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-	}
-	fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight {
-		(486_301_000 as Weight)
-			.saturating_add((10_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-	}
-	fn receive_message_proofs_with_large_leaf(i: u32) -> Weight {
-		(178_139_000 as Weight)
-			.saturating_add((7_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-	}
-	fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((150_844_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-	}
-	fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight {
-		(113_140_000 as Weight)
-			.saturating_add((7_656_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-	}
-	fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight {
-		(97_424_000 as Weight)
-			.saturating_add((63_128_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(i as Weight)))
-			.saturating_add(T::DbWeight::get().writes(3 as Weight))
-			.saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
-	}
 }
 
 // For backwards compatibility and tests
 impl WeightInfo for () {
 	fn send_minimal_message_worst_case() -> Weight {
-		(159_305_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(117_480_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(12 as Weight))
 	}
 	fn send_1_kb_message_worst_case() -> Weight {
-		(164_394_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(128_391_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(12 as Weight))
 	}
 	fn send_16_kb_message_worst_case() -> Weight {
-		(223_521_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(149_149_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(12 as Weight))
 	}
 	fn maximal_increase_message_fee() -> Weight {
-		(6_781_470_000 as Weight)
+		(6_015_058_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn increase_message_fee(i: u32) -> Weight {
-		(114_963_000 as Weight)
-			.saturating_add((6_000 as Weight).saturating_mul(i as Weight))
+		(0 as Weight)
+			.saturating_add((2_000 as Weight).saturating_mul(i as Weight))
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof() -> Weight {
-		(206_769_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(179_892_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_two_messages_proof() -> Weight {
-		(343_982_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(291_793_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_with_outbound_lane_state() -> Weight {
-		(223_738_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(192_191_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_1_kb() -> Weight {
-		(235_369_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(202_104_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_message_proof_16_kb() -> Weight {
-		(510_338_000 as Weight)
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
+		(357_144_000 as Weight)
+			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_single_prepaid_message_proof() -> Weight {
-		(141_536_000 as Weight)
+		(122_648_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
 	fn receive_delivery_proof_for_single_message() -> Weight {
-		(128_805_000 as Weight)
+		(107_631_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(6 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_delivery_proof_for_two_messages_by_single_relayer() -> Weight {
-		(137_143_000 as Weight)
+		(113_885_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn receive_delivery_proof_for_two_messages_by_two_relayers() -> Weight {
-		(193_108_000 as Weight)
+		(155_151_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(8 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
-	fn send_messages_of_various_lengths(i: u32) -> Weight {
-		(133_632_000 as Weight)
-			.saturating_add((4_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(12 as Weight))
-	}
-	fn receive_multiple_messages_proof(i: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((145_006_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-	}
-	fn receive_message_proofs_with_extra_nodes(i: u32) -> Weight {
-		(486_301_000 as Weight)
-			.saturating_add((10_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-	}
-	fn receive_message_proofs_with_large_leaf(i: u32) -> Weight {
-		(178_139_000 as Weight)
-			.saturating_add((7_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-	}
-	fn receive_multiple_messages_proof_with_outbound_lane_state(i: u32) -> Weight {
-		(0 as Weight)
-			.saturating_add((150_844_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-	}
-	fn receive_delivery_proof_for_multiple_messages_by_single_relayer(i: u32) -> Weight {
-		(113_140_000 as Weight)
-			.saturating_add((7_656_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-	}
-	fn receive_delivery_proof_for_multiple_messages_by_multiple_relayers(i: u32) -> Weight {
-		(97_424_000 as Weight)
-			.saturating_add((63_128_000 as Weight).saturating_mul(i as Weight))
-			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(i as Weight)))
-			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
-			.saturating_add(RocksDbWeight::get().writes((1 as Weight).saturating_mul(i as Weight)))
-	}
 }
diff --git a/polkadot/bridges/modules/messages/src/weights_ext.rs b/polkadot/bridges/modules/messages/src/weights_ext.rs
index fef09c6cebe577a1e92a5a36c5dda9549a135a28..483a22eda1d6b8cf3079973f74375c666efbfb19 100644
--- a/polkadot/bridges/modules/messages/src/weights_ext.rs
+++ b/polkadot/bridges/modules/messages/src/weights_ext.rs
@@ -390,7 +390,7 @@ impl WeightInfoExt for () {
 	}
 }
 
-impl<T: frame_system::Config> WeightInfoExt for crate::weights::RialtoWeight<T> {
+impl<T: frame_system::Config> WeightInfoExt for crate::weights::MillauWeight<T> {
 	fn expected_extra_storage_proof_size() -> u32 {
 		EXTRA_STORAGE_PROOF_SIZE
 	}
diff --git a/polkadot/bridges/modules/shift-session-manager/Cargo.toml b/polkadot/bridges/modules/shift-session-manager/Cargo.toml
index 9e3e15fddf897365bcb5c19b4709f01a52b9934f..30a5618b115f21324aa26cd5bc624089ccc7397b 100644
--- a/polkadot/bridges/modules/shift-session-manager/Cargo.toml
+++ b/polkadot/bridges/modules/shift-session-manager/Cargo.toml
@@ -3,12 +3,12 @@ name = "pallet-shift-session-manager"
 description = "A Substrate Runtime module that selects 2/3 of initial validators for every session"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Substrate Dependencies
 
diff --git a/polkadot/bridges/modules/shift-session-manager/src/lib.rs b/polkadot/bridges/modules/shift-session-manager/src/lib.rs
index c8d415cc83deb92b0dc55c101313c2c8e0001e44..45db8840abe9af12152c3c2e60bce770ca819067 100644
--- a/polkadot/bridges/modules/shift-session-manager/src/lib.rs
+++ b/polkadot/bridges/modules/shift-session-manager/src/lib.rs
@@ -35,6 +35,7 @@ pub mod pallet {
 
 	#[pallet::pallet]
 	#[pallet::generate_store(pub(super) trait Store)]
+	#[pallet::without_storage_info]
 	pub struct Pallet<T>(PhantomData<T>);
 
 	#[pallet::hooks]
diff --git a/polkadot/bridges/modules/token-swap/Cargo.toml b/polkadot/bridges/modules/token-swap/Cargo.toml
index a6103f688c424f07017960bd241426c4ba63183c..aad395fb7a30607aa49d76718c22e50848b3f666 100644
--- a/polkadot/bridges/modules/token-swap/Cargo.toml
+++ b/polkadot/bridges/modules/token-swap/Cargo.toml
@@ -3,13 +3,13 @@ name = "pallet-bridge-token-swap"
 description = "An Substrate pallet that allows parties on different chains (bridged using messages pallet) to swap their tokens"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
 log = { version = "0.4.14", default-features = false }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true }
 
 # Bridge dependencies
@@ -55,5 +55,5 @@ std = [
 	"sp-std/std",
 ]
 runtime-benchmarks = [
-	"frame-benchmarking",
+	"frame-benchmarking/runtime-benchmarks",
 ]
diff --git a/polkadot/bridges/modules/token-swap/src/benchmarking.rs b/polkadot/bridges/modules/token-swap/src/benchmarking.rs
index bbc544a8b91dff1d35db2c3c55a2029d52d1f78f..878cb20993a9d1c081c1c053de7a05f00b402a69 100644
--- a/polkadot/bridges/modules/token-swap/src/benchmarking.rs
+++ b/polkadot/bridges/modules/token-swap/src/benchmarking.rs
@@ -18,19 +18,19 @@
 
 use crate::{
 	swap_account_id, target_account_at_this_chain, BridgedAccountIdOf, BridgedAccountPublicOf,
-	BridgedAccountSignatureOf, BridgedBalanceOf, Call, Pallet, ThisChainBalance,
+	BridgedAccountSignatureOf, BridgedBalanceOf, Call, Origin, Pallet, ThisChainBalance,
 	TokenSwapCreationOf, TokenSwapOf,
 };
 
 use bp_token_swap::{TokenSwap, TokenSwapCreation, TokenSwapState, TokenSwapType};
-use codec::Encode;
+use codec::{Decode, Encode};
 use frame_benchmarking::{account, benchmarks_instance_pallet};
 use frame_support::{traits::Currency, Parameter};
 use frame_system::RawOrigin;
 use sp_core::H256;
 use sp_io::hashing::blake2_256;
-use sp_runtime::traits::Bounded;
-use sp_std::vec::Vec;
+use sp_runtime::traits::{Bounded, TrailingZeroInput};
+use sp_std::{boxed::Box, vec::Vec};
 
 const SEED: u32 = 0;
 
@@ -43,8 +43,9 @@ pub trait Config<I: 'static>: crate::Config<I> {
 benchmarks_instance_pallet! {
 	where_clause {
 		where
-			BridgedAccountPublicOf<T, I>: Default + Parameter,
-			BridgedAccountSignatureOf<T, I>: Default,
+			Origin<T, I>: Into<T::Origin>,
+			BridgedAccountPublicOf<T, I>: Decode + Parameter,
+			BridgedAccountSignatureOf<T, I>: Decode,
 	}
 
 	//
@@ -138,8 +139,8 @@ fn test_swap_hash<T: Config<I>, I: 'static>(sender: T::AccountId, is_create: boo
 /// Returns test token swap creation params.
 fn test_swap_creation<T: Config<I>, I: 'static>() -> TokenSwapCreationOf<T, I>
 where
-	BridgedAccountPublicOf<T, I>: Default,
-	BridgedAccountSignatureOf<T, I>: Default,
+	BridgedAccountPublicOf<T, I>: Decode,
+	BridgedAccountSignatureOf<T, I>: Decode,
 {
 	TokenSwapCreation {
 		target_public_at_bridged_chain: target_public_at_bridged_chain::<T, I>(),
@@ -176,20 +177,22 @@ fn target_balance_to_swap<T: Config<I>, I: 'static>() -> BridgedBalanceOf<T, I>
 /// Public key of `target_account_at_bridged_chain`.
 fn target_public_at_bridged_chain<T: Config<I>, I: 'static>() -> BridgedAccountPublicOf<T, I>
 where
-	BridgedAccountPublicOf<T, I>: Default,
+	BridgedAccountPublicOf<T, I>: Decode,
 {
-	Default::default()
+	BridgedAccountPublicOf::<T, I>::decode(&mut TrailingZeroInput::zeroes())
+		.expect("failed to decode `BridgedAccountPublicOf` from zeroes")
 }
 
 /// Signature of `target_account_at_bridged_chain` over message.
 fn bridged_currency_transfer_signature<T: Config<I>, I: 'static>() -> BridgedAccountSignatureOf<T, I>
 where
-	BridgedAccountSignatureOf<T, I>: Default,
+	BridgedAccountSignatureOf<T, I>: Decode,
 {
-	Default::default()
+	BridgedAccountSignatureOf::<T, I>::decode(&mut TrailingZeroInput::zeroes())
+		.expect("failed to decode `BridgedAccountSignatureOf` from zeroes")
 }
 
 /// Account at the bridged chain that is participating in the swap.
 fn target_account_at_bridged_chain<T: Config<I>, I: 'static>() -> BridgedAccountIdOf<T, I> {
-	Default::default()
+	account("target_account_at_bridged_chain", 0, SEED)
 }
diff --git a/polkadot/bridges/modules/token-swap/src/lib.rs b/polkadot/bridges/modules/token-swap/src/lib.rs
index 43fa13ba4bdb869bccd82e9447004b7bd670257b..e46a4bc2dd034fe2a9394f9236e1a4c6907b918c 100644
--- a/polkadot/bridges/modules/token-swap/src/lib.rs
+++ b/polkadot/bridges/modules/token-swap/src/lib.rs
@@ -70,16 +70,18 @@ use bp_runtime::{messages::DispatchFeePayment, ChainId};
 use bp_token_swap::{
 	RawBridgedTransferCall, TokenSwap, TokenSwapCreation, TokenSwapState, TokenSwapType,
 };
-use codec::Encode;
+use codec::{Decode, Encode};
 use frame_support::{
 	fail,
 	traits::{Currency, ExistenceRequirement},
 	weights::PostDispatchInfo,
+	RuntimeDebug,
 };
+use scale_info::TypeInfo;
 use sp_core::H256;
 use sp_io::hashing::blake2_256;
 use sp_runtime::traits::{Convert, Saturating};
-use sp_std::boxed::Box;
+use sp_std::{boxed::Box, marker::PhantomData};
 use weights::WeightInfo;
 
 pub use weights_ext::WeightInfoExt;
@@ -98,6 +100,21 @@ pub use pallet::*;
 /// Name of the `PendingSwaps` storage map.
 pub const PENDING_SWAPS_MAP_NAME: &str = "PendingSwaps";
 
+/// Origin for the token swap pallet.
+#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo)]
+pub enum RawOrigin<AccountId, I> {
+	/// The call is originated by the token swap account.
+	TokenSwap {
+		/// Id of the account that has started the swap.
+		source_account_at_this_chain: AccountId,
+		/// Id of the account that holds the funds during this swap. The message fee is paid from
+		/// this account funds.
+		swap_account_at_this_chain: AccountId,
+	},
+	/// Dummy to manage the fact we have instancing.
+	_Phantom(PhantomData<I>),
+}
+
 // comes from #[pallet::event]
 #[allow(clippy::unused_unit)]
 #[frame_support::pallet]
@@ -126,6 +143,7 @@ pub mod pallet {
 		type OutboundMessageLaneId: Get<LaneId>;
 		/// Messages bridge with Bridged chain.
 		type MessagesBridge: MessagesBridge<
+			Self::Origin,
 			Self::AccountId,
 			<Self::ThisCurrency as Currency<Self::AccountId>>::Balance,
 			MessagePayloadOf<Self, I>,
@@ -182,6 +200,7 @@ pub mod pallet {
 
 	#[pallet::pallet]
 	#[pallet::generate_store(pub(super) trait Store)]
+	#[pallet::without_storage_info]
 	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
 
 	#[pallet::hooks]
@@ -191,6 +210,7 @@ pub mod pallet {
 	impl<T: Config<I>, I: 'static> Pallet<T, I>
 	where
 		BridgedAccountPublicOf<T, I>: Parameter,
+		Origin<T, I>: Into<T::Origin>,
 	{
 		/// Start token swap procedure.
 		///
@@ -312,7 +332,11 @@ pub mod pallet {
 				// `Currency::transfer` call on the bridged chain, but no checks are made - it is
 				// the transaction submitter to ensure it is valid.
 				let send_message_result = T::MessagesBridge::send_message(
-					bp_messages::source_chain::Sender::from(Some(swap_account.clone())),
+					RawOrigin::TokenSwap {
+						source_account_at_this_chain: swap.source_account_at_this_chain.clone(),
+						swap_account_at_this_chain: swap_account.clone(),
+					}
+					.into(),
 					T::OutboundMessageLaneId::get(),
 					bp_message_dispatch::MessagePayload {
 						spec_version: bridged_chain_spec_version,
@@ -515,6 +539,10 @@ pub mod pallet {
 		InvalidClaimant,
 	}
 
+	/// Origin for the token swap pallet.
+	#[pallet::origin]
+	pub type Origin<T, I = ()> = RawOrigin<<T as frame_system::Config>::AccountId, I>;
+
 	/// Pending token swaps states.
 	#[pallet::storage]
 	pub type PendingSwaps<T: Config<I>, I: 'static = ()> =
@@ -639,7 +667,7 @@ pub mod pallet {
 mod tests {
 	use super::*;
 	use crate::mock::*;
-	use frame_support::{assert_noop, assert_ok};
+	use frame_support::{assert_noop, assert_ok, storage::generator::StorageMap};
 
 	const CAN_START_BLOCK_NUMBER: u64 = 10;
 	const CAN_CLAIM_BLOCK_NUMBER: u64 = CAN_START_BLOCK_NUMBER + 1;
@@ -687,7 +715,7 @@ mod tests {
 
 	fn start_test_swap() {
 		assert_ok!(Pallet::<TestRuntime>::create_swap(
-			Origin::signed(THIS_CHAIN_ACCOUNT),
+			mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 			test_swap(),
 			Box::new(TokenSwapCreation {
 				target_public_at_bridged_chain: bridged_chain_account_public(),
@@ -712,7 +740,7 @@ mod tests {
 		run_test(|| {
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT + 1),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT + 1),
 					test_swap(),
 					Box::new(test_swap_creation()),
 				),
@@ -728,7 +756,7 @@ mod tests {
 			swap.source_balance_at_this_chain = ExistentialDeposit::get() - 1;
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 					swap,
 					Box::new(test_swap_creation()),
 				),
@@ -744,7 +772,7 @@ mod tests {
 			swap.source_balance_at_this_chain = THIS_CHAIN_ACCOUNT_BALANCE + 1;
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 					swap,
 					Box::new(test_swap_creation()),
 				),
@@ -762,7 +790,7 @@ mod tests {
 			swap_creation.bridged_currency_transfer = transfer;
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 					test_swap(),
 					Box::new(swap_creation),
 				),
@@ -775,14 +803,14 @@ mod tests {
 	fn create_swap_fails_if_swap_is_active() {
 		run_test(|| {
 			assert_ok!(Pallet::<TestRuntime>::create_swap(
-				Origin::signed(THIS_CHAIN_ACCOUNT),
+				mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 				test_swap(),
 				Box::new(test_swap_creation()),
 			));
 
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 					test_swap(),
 					Box::new(test_swap_creation()),
 				),
@@ -797,7 +825,7 @@ mod tests {
 			frame_system::Pallet::<TestRuntime>::set_block_number(CAN_START_BLOCK_NUMBER + 1);
 			assert_noop!(
 				Pallet::<TestRuntime>::create_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 					test_swap(),
 					Box::new(test_swap_creation()),
 				),
@@ -811,7 +839,7 @@ mod tests {
 		run_test(|| {
 			frame_system::Pallet::<TestRuntime>::set_block_number(CAN_START_BLOCK_NUMBER);
 			assert_ok!(Pallet::<TestRuntime>::create_swap(
-				Origin::signed(THIS_CHAIN_ACCOUNT),
+				mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 				test_swap(),
 				Box::new(test_swap_creation()),
 			));
@@ -825,7 +853,7 @@ mod tests {
 			frame_system::Pallet::<TestRuntime>::reset_events();
 
 			assert_ok!(Pallet::<TestRuntime>::create_swap(
-				Origin::signed(THIS_CHAIN_ACCOUNT),
+				mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 				test_swap(),
 				Box::new(test_swap_creation()),
 			));
@@ -857,7 +885,7 @@ mod tests {
 		run_test(|| {
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(
+					mock::Origin::signed(
 						1 + target_account_at_this_chain::<TestRuntime, ()>(&test_swap())
 					),
 					test_swap(),
@@ -874,7 +902,9 @@ mod tests {
 
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+					mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(
+						&test_swap()
+					)),
 					test_swap(),
 				),
 				Error::<TestRuntime, ()>::SwapIsPending
@@ -889,7 +919,9 @@ mod tests {
 
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+					mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(
+						&test_swap()
+					)),
 					test_swap(),
 				),
 				Error::<TestRuntime, ()>::SwapIsFailed
@@ -902,7 +934,9 @@ mod tests {
 		run_test(|| {
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+					mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(
+						&test_swap()
+					)),
 					test_swap(),
 				),
 				Error::<TestRuntime, ()>::SwapIsInactive
@@ -918,7 +952,9 @@ mod tests {
 
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+					mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(
+						&test_swap()
+					)),
 					test_swap(),
 				),
 				Error::<TestRuntime, ()>::FailedToTransferFromSwapAccount
@@ -936,7 +972,9 @@ mod tests {
 
 			assert_noop!(
 				Pallet::<TestRuntime>::claim_swap(
-					Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+					mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(
+						&test_swap()
+					)),
 					test_swap(),
 				),
 				Error::<TestRuntime, ()>::SwapIsTemporaryLocked
@@ -954,7 +992,7 @@ mod tests {
 			frame_system::Pallet::<TestRuntime>::reset_events();
 
 			assert_ok!(Pallet::<TestRuntime>::claim_swap(
-				Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
+				mock::Origin::signed(target_account_at_this_chain::<TestRuntime, ()>(&test_swap())),
 				test_swap(),
 			));
 
@@ -990,7 +1028,7 @@ mod tests {
 
 			assert_noop!(
 				Pallet::<TestRuntime>::cancel_swap(
-					Origin::signed(THIS_CHAIN_ACCOUNT + 1),
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT + 1),
 					test_swap()
 				),
 				Error::<TestRuntime, ()>::MismatchedSwapSourceOrigin
@@ -1004,7 +1042,10 @@ mod tests {
 			start_test_swap();
 
 			assert_noop!(
-				Pallet::<TestRuntime>::cancel_swap(Origin::signed(THIS_CHAIN_ACCOUNT), test_swap()),
+				Pallet::<TestRuntime>::cancel_swap(
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
+					test_swap()
+				),
 				Error::<TestRuntime, ()>::SwapIsPending
 			);
 		});
@@ -1017,7 +1058,10 @@ mod tests {
 			receive_test_swap_confirmation(true);
 
 			assert_noop!(
-				Pallet::<TestRuntime>::cancel_swap(Origin::signed(THIS_CHAIN_ACCOUNT), test_swap()),
+				Pallet::<TestRuntime>::cancel_swap(
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
+					test_swap()
+				),
 				Error::<TestRuntime, ()>::SwapIsConfirmed
 			);
 		});
@@ -1027,7 +1071,10 @@ mod tests {
 	fn cancel_swap_fails_if_swap_is_inactive() {
 		run_test(|| {
 			assert_noop!(
-				Pallet::<TestRuntime>::cancel_swap(Origin::signed(THIS_CHAIN_ACCOUNT), test_swap()),
+				Pallet::<TestRuntime>::cancel_swap(
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
+					test_swap()
+				),
 				Error::<TestRuntime, ()>::SwapIsInactive
 			);
 		});
@@ -1044,7 +1091,10 @@ mod tests {
 			);
 
 			assert_noop!(
-				Pallet::<TestRuntime>::cancel_swap(Origin::signed(THIS_CHAIN_ACCOUNT), test_swap()),
+				Pallet::<TestRuntime>::cancel_swap(
+					mock::Origin::signed(THIS_CHAIN_ACCOUNT),
+					test_swap()
+				),
 				Error::<TestRuntime, ()>::FailedToTransferFromSwapAccount
 			);
 		});
@@ -1060,7 +1110,7 @@ mod tests {
 			frame_system::Pallet::<TestRuntime>::reset_events();
 
 			assert_ok!(Pallet::<TestRuntime>::cancel_swap(
-				Origin::signed(THIS_CHAIN_ACCOUNT),
+				mock::Origin::signed(THIS_CHAIN_ACCOUNT),
 				test_swap()
 			));
 
@@ -1130,4 +1180,12 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn storage_keys_computed_properly() {
+		assert_eq!(
+			PendingSwaps::<TestRuntime>::storage_map_final_key(test_swap_hash()),
+			bp_token_swap::storage_keys::pending_swaps_key("TokenSwap", test_swap_hash()).0,
+		);
+	}
 }
diff --git a/polkadot/bridges/modules/token-swap/src/mock.rs b/polkadot/bridges/modules/token-swap/src/mock.rs
index 4e81c62faa7ae9ac209f9f1b1945e6c94886b6dd..ece7b16acc9101253d2017dc61e5d75c76a69638 100644
--- a/polkadot/bridges/modules/token-swap/src/mock.rs
+++ b/polkadot/bridges/modules/token-swap/src/mock.rs
@@ -56,7 +56,7 @@ frame_support::construct_runtime! {
 	{
 		System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
 		Balances: pallet_balances::{Pallet, Call, Event<T>},
-		TokenSwap: pallet_bridge_token_swap::{Pallet, Call, Event<T>},
+		TokenSwap: pallet_bridge_token_swap::{Pallet, Call, Event<T>, Origin<T>},
 	}
 }
 
@@ -143,22 +143,34 @@ impl bp_runtime::Chain for BridgedChain {
 	type Balance = BridgedBalance;
 	type Index = u64;
 	type Signature = BridgedAccountSignature;
+
+	fn max_extrinsic_size() -> u32 {
+		unreachable!()
+	}
+	fn max_extrinsic_weight() -> Weight {
+		unreachable!()
+	}
 }
 
 pub struct TestMessagesBridge;
 
-impl MessagesBridge<AccountId, Balance, MessagePayloadOf<TestRuntime, ()>> for TestMessagesBridge {
+impl MessagesBridge<Origin, AccountId, Balance, MessagePayloadOf<TestRuntime, ()>>
+	for TestMessagesBridge
+{
 	type Error = ();
 
 	fn send_message(
-		sender: frame_system::RawOrigin<AccountId>,
+		sender: Origin,
 		lane: LaneId,
 		message: MessagePayloadOf<TestRuntime, ()>,
 		delivery_and_dispatch_fee: Balance,
 	) -> Result<SendMessageArtifacts, Self::Error> {
-		assert_ne!(sender, frame_system::RawOrigin::Signed(THIS_CHAIN_ACCOUNT));
 		assert_eq!(lane, OutboundMessageLaneId::get());
 		assert_eq!(delivery_and_dispatch_fee, SWAP_DELIVERY_AND_DISPATCH_FEE);
+		match sender.caller {
+			OriginCaller::TokenSwap(_) => (),
+			_ => panic!("unexpected origin"),
+		}
 		match message.call[0] {
 			OK_TRANSFER_CALL => Ok(SendMessageArtifacts { nonce: MESSAGE_NONCE, weight: 0 }),
 			BAD_TRANSFER_CALL => Err(()),
diff --git a/polkadot/bridges/modules/token-swap/src/weights.rs b/polkadot/bridges/modules/token-swap/src/weights.rs
index 06cb6b85cf336d4d308309c4aac45f6f2712b495..51c5d99de9c5cc92dc9159f0736c445ce85663cb 100644
--- a/polkadot/bridges/modules/token-swap/src/weights.rs
+++ b/polkadot/bridges/modules/token-swap/src/weights.rs
@@ -17,7 +17,7 @@
 //! Autogenerated weights for `pallet_bridge_token_swap`
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2021-10-06, STEPS: 50, REPEAT: 20
+//! DATE: 2021-12-28, STEPS: 50, REPEAT: 20
 //! LOW RANGE: [], HIGH RANGE: []
 //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled
 //! CHAIN: Some("dev"), DB CACHE: 128
@@ -57,17 +57,17 @@ pub trait WeightInfo {
 pub struct MillauWeight<T>(PhantomData<T>);
 impl<T: frame_system::Config> WeightInfo for MillauWeight<T> {
 	fn create_swap() -> Weight {
-		(116_040_000 as Weight)
+		(90_368_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
 	fn claim_swap() -> Weight {
-		(102_882_000 as Weight)
+		(88_397_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
 	fn cancel_swap() -> Weight {
-		(99_434_000 as Weight)
+		(91_253_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
@@ -76,17 +76,17 @@ impl<T: frame_system::Config> WeightInfo for MillauWeight<T> {
 // For backwards compatibility and tests
 impl WeightInfo for () {
 	fn create_swap() -> Weight {
-		(116_040_000 as Weight)
+		(90_368_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
 	fn claim_swap() -> Weight {
-		(102_882_000 as Weight)
+		(88_397_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
 	fn cancel_swap() -> Weight {
-		(99_434_000 as Weight)
+		(91_253_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
diff --git a/polkadot/bridges/primitives/chain-kusama/Cargo.toml b/polkadot/bridges/primitives/chain-kusama/Cargo.toml
index 6ff860357c7c451524b106be643d0bbe6e38ebb1..a676b565c33dce3918c7540bed48384e1f9d7e95 100644
--- a/polkadot/bridges/primitives/chain-kusama/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-kusama/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-kusama"
 description = "Primitives of Kusama runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -19,6 +19,7 @@ bp-runtime = { path = "../runtime", default-features = false }
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-version = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
@@ -30,6 +31,7 @@ std = [
 	"bp-runtime/std",
 	"frame-support/std",
 	"sp-api/std",
+	"sp-runtime/std",
 	"sp-std/std",
 	"sp-version/std",
 ]
diff --git a/polkadot/bridges/primitives/chain-kusama/src/lib.rs b/polkadot/bridges/primitives/chain-kusama/src/lib.rs
index c01f5691d4080867bb9241968ef365d1b4935523..a0a5990ca08ad42a8890b0570792f4037af0509a 100644
--- a/polkadot/bridges/primitives/chain-kusama/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-kusama/src/lib.rs
@@ -18,10 +18,11 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
 use frame_support::weights::{
 	WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial,
 };
+use sp_runtime::FixedU128;
 use sp_std::prelude::*;
 use sp_version::RuntimeVersion;
 
@@ -35,10 +36,11 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: sp_version::create_runtime_str!("kusama"),
 	impl_name: sp_version::create_runtime_str!("parity-kusama"),
 	authoring_version: 2,
-	spec_version: 9100,
+	spec_version: 9180,
 	impl_version: 0,
 	apis: sp_version::create_apis_vec![[]],
-	transaction_version: 5,
+	transaction_version: 11,
+	state_version: 0,
 };
 
 // NOTE: This needs to be kept up to date with the Kusama runtime found in the Polkadot repo.
@@ -79,17 +81,22 @@ pub const EXISTENTIAL_DEPOSIT: Balance = 1_000_000_000_000 / 30_000;
 /// conditions.
 pub const SESSION_LENGTH: BlockNumber = time_units::HOURS;
 
-/// Name of the With-Polkadot messages pallet instance in the Kusama runtime.
-pub const WITH_POLKADOT_MESSAGES_PALLET_NAME: &str = "BridgePolkadotMessages";
+/// Name of the With-Kusama GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_KUSAMA_GRANDPA_PALLET_NAME: &str = "BridgeKusamaGrandpa";
+/// Name of the With-Kusama messages pallet instance that is deployed at bridged chains.
+pub const WITH_KUSAMA_MESSAGES_PALLET_NAME: &str = "BridgeKusamaMessages";
+
+/// Name of the transaction payment pallet at the Kusama runtime.
+pub const TRANSACTION_PAYMENT_PALLET_NAME: &str = "TransactionPayment";
 
 /// Name of the DOT->KSM conversion rate stored in the Kusama runtime.
 pub const POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME: &str =
 	"PolkadotToKusamaConversionRate";
+/// Name of the Polkadot fee multiplier parameter, stored in the Polkadot runtime.
+pub const POLKADOT_FEE_MULTIPLIER_PARAMETER_NAME: &str = "PolkadotFeeMultiplier";
 
 /// Name of the `KusamaFinalityApi::best_finalized` runtime method.
 pub const BEST_FINALIZED_KUSAMA_HEADER_METHOD: &str = "KusamaFinalityApi_best_finalized";
-/// Name of the `KusamaFinalityApi::is_known_header` runtime method.
-pub const IS_KNOWN_KUSAMA_HEADER_METHOD: &str = "KusamaFinalityApi_is_known_header";
 
 /// Name of the `ToKusamaOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime
 /// method.
@@ -97,22 +104,6 @@ pub const TO_KUSAMA_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToKusamaOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToKusamaOutboundLaneApi::message_details` runtime method.
 pub const TO_KUSAMA_MESSAGE_DETAILS_METHOD: &str = "ToKusamaOutboundLaneApi_message_details";
-/// Name of the `ToKusamaOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_KUSAMA_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToKusamaOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToKusamaOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_KUSAMA_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToKusamaOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromKusamaInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_KUSAMA_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromKusamaInboundLaneApi_latest_received_nonce";
-/// Name of the `FromKusamaInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_KUSAMA_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromKusamaInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromKusamaInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_KUSAMA_UNREWARDED_RELAYERS_STATE: &str =
-	"FromKusamaInboundLaneApi_unrewarded_relayers_state";
 
 sp_api::decl_runtime_apis! {
 	/// API for querying information about the finalized Kusama headers.
@@ -122,8 +113,6 @@ sp_api::decl_runtime_apis! {
 	pub trait KusamaFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Kusama chain.
@@ -143,6 +132,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			kusama_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -154,22 +144,5 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Kusama chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Kusama chain, not the
-	/// Kusama runtime itself.
-	pub trait FromKusamaInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
diff --git a/polkadot/bridges/primitives/chain-millau/Cargo.toml b/polkadot/bridges/primitives/chain-millau/Cargo.toml
index f1e17fe96f5ac713214c6d730aae24de87c9c907..0aaeb5b6bf9d4ebd2f48055682960233307f3915 100644
--- a/polkadot/bridges/primitives/chain-millau/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-millau/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-millau"
 description = "Primitives of Millau runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -14,10 +14,10 @@ bp-messages = { path = "../messages", default-features = false }
 bp-runtime = { path = "../runtime", default-features = false }
 fixed-hash = { version = "0.7.0", default-features = false }
 hash256-std-hasher = { version = "0.15.2", default-features = false }
-impl-codec = { version = "0.5.1", default-features = false }
+impl-codec = { version = "0.6", default-features = false }
 impl-serde = { version = "0.3.1", optional = true }
-parity-util-mem = { version = "0.10", default-features = false, features = ["primitive-types"] }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+parity-util-mem = { version = "0.11", default-features = false, features = ["primitive-types"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true, features = ["derive"] }
 
 # Substrate Based Dependencies
diff --git a/polkadot/bridges/primitives/chain-millau/src/lib.rs b/polkadot/bridges/primitives/chain-millau/src/lib.rs
index f86430fe9b236f969b2133e33e16170fccec3c75..ff8d53859535b46a55a05743a69a1c78f600bfe6 100644
--- a/polkadot/bridges/primitives/chain-millau/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-millau/src/lib.rs
@@ -20,7 +20,7 @@
 
 mod millau_hash;
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
 use bp_runtime::Chain;
 use frame_support::{
 	weights::{constants::WEIGHT_PER_SECOND, DispatchClass, IdentityFee, Weight},
@@ -28,13 +28,13 @@ use frame_support::{
 };
 use frame_system::limits;
 use scale_info::TypeInfo;
-use sp_core::Hasher as HasherT;
+use sp_core::{storage::StateVersion, Hasher as HasherT};
 use sp_runtime::{
 	traits::{Convert, IdentifyAccount, Verify},
-	MultiSignature, MultiSigner, Perbill,
+	FixedU128, MultiSignature, MultiSigner, Perbill,
 };
 use sp_std::prelude::*;
-use sp_trie::{trie_types::Layout, TrieConfiguration};
+use sp_trie::{LayoutV0, LayoutV1, TrieConfiguration};
 
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
@@ -66,11 +66,11 @@ pub const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10);
 /// Represents the portion of a block that will be used by Normal extrinsics.
 pub const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75);
 
-/// Maximal number of unrewarded relayer entries at inbound lane.
-pub const MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE: MessageNonce = 1024;
+/// Maximal number of unrewarded relayer entries in Millau confirmation transaction.
+pub const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 128;
 
-/// Maximal number of unconfirmed messages at inbound lane.
-pub const MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE: MessageNonce = 1024;
+/// Maximal number of unconfirmed messages in Millau confirmation transaction.
+pub const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 128;
 
 /// Weight of single regular message delivery transaction on Millau chain.
 ///
@@ -101,7 +101,7 @@ pub const MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT: Weight = 2_000_000
 /// chain. Don't put too much reserve there, because it is used to **decrease**
 /// `DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT` cost. So putting large reserve would make delivery
 /// transactions cheaper.
-pub const PAY_INBOUND_DISPATCH_FEE_WEIGHT: Weight = 600_000_000;
+pub const PAY_INBOUND_DISPATCH_FEE_WEIGHT: Weight = 700_000_000;
 
 /// The target length of a session (how often authorities change) on Millau measured in of number of
 /// blocks.
@@ -170,6 +170,17 @@ impl Chain for Millau {
 	type Balance = Balance;
 	type Index = Index;
 	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)
+	}
 }
 
 /// Millau Hasher (Blake2-256 ++ Keccak-256) implementation.
@@ -193,12 +204,18 @@ impl sp_core::Hasher for BlakeTwoAndKeccak256 {
 impl sp_runtime::traits::Hash for BlakeTwoAndKeccak256 {
 	type Output = MillauHash;
 
-	fn trie_root(input: Vec<(Vec<u8>, Vec<u8>)>) -> Self::Output {
-		Layout::<BlakeTwoAndKeccak256>::trie_root(input)
+	fn trie_root(input: Vec<(Vec<u8>, Vec<u8>)>, state_version: StateVersion) -> Self::Output {
+		match state_version {
+			StateVersion::V0 => LayoutV0::<BlakeTwoAndKeccak256>::trie_root(input),
+			StateVersion::V1 => LayoutV1::<BlakeTwoAndKeccak256>::trie_root(input),
+		}
 	}
 
-	fn ordered_trie_root(input: Vec<Vec<u8>>) -> Self::Output {
-		Layout::<BlakeTwoAndKeccak256>::ordered_trie_root(input)
+	fn ordered_trie_root(input: Vec<Vec<u8>>, state_version: StateVersion) -> Self::Output {
+		match state_version {
+			StateVersion::V0 => LayoutV0::<BlakeTwoAndKeccak256>::ordered_trie_root(input),
+			StateVersion::V1 => LayoutV1::<BlakeTwoAndKeccak256>::ordered_trie_root(input),
+		}
 	}
 }
 
@@ -244,21 +261,14 @@ frame_support::parameter_types! {
 		.build_or_panic();
 }
 
-/// Get the maximum weight (compute time) that a Normal extrinsic on the Millau chain can use.
-pub fn max_extrinsic_weight() -> Weight {
-	BlockWeights::get()
-		.get(DispatchClass::Normal)
-		.max_extrinsic
-		.unwrap_or(Weight::MAX)
-}
+/// Name of the With-Millau GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_MILLAU_GRANDPA_PALLET_NAME: &str = "BridgeMillauGrandpa";
+/// Name of the With-Millau messages pallet instance that is deployed at bridged chains.
+pub const WITH_MILLAU_MESSAGES_PALLET_NAME: &str = "BridgeMillauMessages";
 
-/// Get the maximum length in bytes that a Normal extrinsic on the Millau chain requires.
-pub fn max_extrinsic_size() -> u32 {
-	*BlockLength::get().max.get(DispatchClass::Normal)
-}
+/// Name of the Rialto->Millau (actually DOT->KSM) conversion rate stored in the Millau runtime.
+pub const RIALTO_TO_MILLAU_CONVERSION_RATE_PARAMETER_NAME: &str = "RialtoToMillauConversionRate";
 
-/// Name of the With-Rialto messages pallet instance in the Millau runtime.
-pub const WITH_RIALTO_MESSAGES_PALLET_NAME: &str = "BridgeRialtoMessages";
 /// Name of the With-Rialto token swap pallet instance in the Millau runtime.
 pub const WITH_RIALTO_TOKEN_SWAP_PALLET_NAME: &str = "BridgeRialtoTokenSwap";
 
@@ -271,22 +281,6 @@ pub const TO_MILLAU_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToMillauOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToMillauOutboundLaneApi::message_details` runtime method.
 pub const TO_MILLAU_MESSAGE_DETAILS_METHOD: &str = "ToMillauOutboundLaneApi_message_details";
-/// Name of the `ToMillauOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_MILLAU_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToMillauOutboundLaneApi_latest_received_nonce";
-/// Name of the `ToMillauOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_MILLAU_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToMillauOutboundLaneApi_latest_generated_nonce";
-
-/// Name of the `FromMillauInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_MILLAU_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromMillauInboundLaneApi_latest_received_nonce";
-/// Name of the `FromMillauInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_MILLAU_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromMillauInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromMillauInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_MILLAU_UNREWARDED_RELAYERS_STATE: &str =
-	"FromMillauInboundLaneApi_unrewarded_relayers_state";
 
 sp_api::decl_runtime_apis! {
 	/// API for querying information about the finalized Millau headers.
@@ -296,8 +290,6 @@ sp_api::decl_runtime_apis! {
 	pub trait MillauFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Millau chain.
@@ -317,6 +309,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			millau_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -328,23 +321,6 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Millau chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Millau chain, not the
-	/// Millau runtime itself.
-	pub trait FromMillauInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
 
@@ -356,9 +332,9 @@ mod tests {
 	#[test]
 	fn maximal_account_size_does_not_overflow_constant() {
 		assert!(
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::default().encode().len(),
+			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::from([0u8; 32]).encode().len(),
 			"Actual maximal size of encoded AccountId ({}) overflows expected ({})",
-			AccountId::default().encode().len(),
+			AccountId::from([0u8; 32]).encode().len(),
 			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
 		);
 	}
diff --git a/polkadot/bridges/primitives/chain-polkadot/Cargo.toml b/polkadot/bridges/primitives/chain-polkadot/Cargo.toml
index 917c7f97478390864791e907d9d5bcc8cceb8321..738899b658cdf133be64a05b365c9b5923d171e3 100644
--- a/polkadot/bridges/primitives/chain-polkadot/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-polkadot/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-polkadot"
 description = "Primitives of Polkadot runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -19,6 +19,7 @@ bp-runtime = { path = "../runtime", default-features = false }
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-api = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-version = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
@@ -30,6 +31,7 @@ std = [
 	"bp-runtime/std",
 	"frame-support/std",
 	"sp-api/std",
+	"sp-runtime/std",
 	"sp-std/std",
 	"sp-version/std",
 ]
diff --git a/polkadot/bridges/primitives/chain-polkadot/src/lib.rs b/polkadot/bridges/primitives/chain-polkadot/src/lib.rs
index 2e2bdaa90cb5a87b615bd4e2cb8d1be0937c50f0..d95e29c8b0ce849375dd0b0523f4c05cc48e96b2 100644
--- a/polkadot/bridges/primitives/chain-polkadot/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-polkadot/src/lib.rs
@@ -18,10 +18,11 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
 use frame_support::weights::{
 	WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial,
 };
+use sp_runtime::FixedU128;
 use sp_std::prelude::*;
 use sp_version::RuntimeVersion;
 
@@ -35,10 +36,11 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: sp_version::create_runtime_str!("polkadot"),
 	impl_name: sp_version::create_runtime_str!("parity-polkadot"),
 	authoring_version: 0,
-	spec_version: 9100,
+	spec_version: 9180,
 	impl_version: 0,
 	apis: sp_version::create_apis_vec![[]],
-	transaction_version: 7,
+	transaction_version: 12,
+	state_version: 0,
 };
 
 // NOTE: This needs to be kept up to date with the Polkadot runtime found in the Polkadot repo.
@@ -79,17 +81,22 @@ pub const EXISTENTIAL_DEPOSIT: Balance = 10_000_000_000;
 /// conditions.
 pub const SESSION_LENGTH: BlockNumber = 4 * time_units::HOURS;
 
-/// Name of the With-Kusama messages pallet instance in the Polkadot runtime.
-pub const WITH_KUSAMA_MESSAGES_PALLET_NAME: &str = "BridgeKusamaMessages";
+/// Name of the With-Polkadot GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_POLKADOT_GRANDPA_PALLET_NAME: &str = "BridgePolkadotGrandpa";
+/// Name of the With-Polkadot messages pallet instance that is deployed at bridged chains.
+pub const WITH_POLKADOT_MESSAGES_PALLET_NAME: &str = "BridgePolkadotMessages";
 
-/// Name of the KSM->DOT conversion rate stored in the Polkadot runtime.
+/// Name of the transaction payment pallet at the Polkadot runtime.
+pub const TRANSACTION_PAYMENT_PALLET_NAME: &str = "TransactionPayment";
+
+/// Name of the KSM->DOT conversion rate parameter, stored in the Polkadot runtime.
 pub const KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME: &str =
 	"KusamaToPolkadotConversionRate";
+/// Name of the Kusama fee multiplier parameter, stored in the Polkadot runtime.
+pub const KUSAMA_FEE_MULTIPLIER_PARAMETER_NAME: &str = "KusamaFeeMultiplier";
 
 /// Name of the `PolkadotFinalityApi::best_finalized` runtime method.
 pub const BEST_FINALIZED_POLKADOT_HEADER_METHOD: &str = "PolkadotFinalityApi_best_finalized";
-/// Name of the `PolkadotFinalityApi::is_known_header` runtime method.
-pub const IS_KNOWN_POLKADOT_HEADER_METHOD: &str = "PolkadotFinalityApi_is_known_header";
 
 /// Name of the `ToPolkadotOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime
 /// method.
@@ -97,22 +104,6 @@ pub const TO_POLKADOT_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToPolkadotOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToPolkadotOutboundLaneApi::message_details` runtime method.
 pub const TO_POLKADOT_MESSAGE_DETAILS_METHOD: &str = "ToPolkadotOutboundLaneApi_message_details";
-/// Name of the `ToPolkadotOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_POLKADOT_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToPolkadotOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToPolkadotOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_POLKADOT_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToPolkadotOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromPolkadotInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_POLKADOT_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromPolkadotInboundLaneApi_latest_received_nonce";
-/// Name of the `FromPolkadotInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_POLKADOT_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromPolkadotInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromPolkadotInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_POLKADOT_UNREWARDED_RELAYERS_STATE: &str =
-	"FromPolkadotInboundLaneApi_unrewarded_relayers_state";
 
 sp_api::decl_runtime_apis! {
 	/// API for querying information about the finalized Polkadot headers.
@@ -122,8 +113,6 @@ sp_api::decl_runtime_apis! {
 	pub trait PolkadotFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Polkadot chain.
@@ -143,6 +132,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			polkadot_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -154,22 +144,5 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Polkadot chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Polkadot chain, not the
-	/// Polkadot runtime itself.
-	pub trait FromPolkadotInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
diff --git a/polkadot/bridges/primitives/chain-rialto-parachain/Cargo.toml b/polkadot/bridges/primitives/chain-rialto-parachain/Cargo.toml
index 034188631b8cde608025ee64baa5b6de1b9be698..a15c40929579bfeac9435d9bb747112f71ef610e 100644
--- a/polkadot/bridges/primitives/chain-rialto-parachain/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-rialto-parachain/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-rialto-parachain"
 description = "Primitives of Rialto parachain runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/primitives/chain-rialto-parachain/src/lib.rs b/polkadot/bridges/primitives/chain-rialto-parachain/src/lib.rs
index 70da878ff9076c389398d2e5d5b25f24ee61f753..f3f449c7af3e1f4254b8be4d4b3a4d402021eeb9 100644
--- a/polkadot/bridges/primitives/chain-rialto-parachain/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-rialto-parachain/src/lib.rs
@@ -90,6 +90,17 @@ impl Chain for RialtoParachain {
 	type Balance = Balance;
 	type Index = Index;
 	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)
+	}
 }
 
 frame_support::parameter_types! {
@@ -111,16 +122,3 @@ frame_support::parameter_types! {
 		.avg_block_initialization(AVERAGE_ON_INITIALIZE_RATIO)
 		.build_or_panic();
 }
-
-/// Get the maximum weight (compute time) that a Normal extrinsic on the Millau chain can use.
-pub fn max_extrinsic_weight() -> Weight {
-	BlockWeights::get()
-		.get(DispatchClass::Normal)
-		.max_extrinsic
-		.unwrap_or(Weight::MAX)
-}
-
-/// Get the maximum length in bytes that a Normal extrinsic on the Millau chain requires.
-pub fn max_extrinsic_size() -> u32 {
-	*BlockLength::get().max.get(DispatchClass::Normal)
-}
diff --git a/polkadot/bridges/primitives/chain-rialto/Cargo.toml b/polkadot/bridges/primitives/chain-rialto/Cargo.toml
index d16ac59484fb5da33c5f0d23d6eeadbe5a04bee0..663f9076657dd5e4d8dad11b77eb0ad7d5acdfc5 100644
--- a/polkadot/bridges/primitives/chain-rialto/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-rialto/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-rialto"
 description = "Primitives of Rialto runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/primitives/chain-rialto/src/lib.rs b/polkadot/bridges/primitives/chain-rialto/src/lib.rs
index 30269a65b821298293d07ca45c0455cede1a9ba1..4bf20489bc8517d833f0e76653f3fdacd0777ef5 100644
--- a/polkadot/bridges/primitives/chain-rialto/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-rialto/src/lib.rs
@@ -18,7 +18,7 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
 use bp_runtime::Chain;
 use frame_support::{
 	weights::{constants::WEIGHT_PER_SECOND, DispatchClass, IdentityFee, Weight},
@@ -28,7 +28,7 @@ use frame_system::limits;
 use sp_core::Hasher as HasherT;
 use sp_runtime::{
 	traits::{BlakeTwo256, Convert, IdentifyAccount, Verify},
-	MultiSignature, MultiSigner, Perbill,
+	FixedU128, MultiSignature, MultiSigner, Perbill,
 };
 use sp_std::prelude::*;
 
@@ -57,11 +57,11 @@ pub const AVERAGE_ON_INITIALIZE_RATIO: Perbill = Perbill::from_percent(10);
 /// Represents the portion of a block that will be used by Normal extrinsics.
 pub const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75);
 
-/// Maximal number of unrewarded relayer entries at inbound lane.
-pub const MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE: MessageNonce = 128;
+/// Maximal number of unrewarded relayer entries in Rialto confirmation transaction.
+pub const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 1024;
 
-/// Maximal number of unconfirmed messages at inbound lane.
-pub const MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE: MessageNonce = 128;
+/// Maximal number of unconfirmed messages in Rialto confirmation transaction.
+pub const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 1024;
 
 /// Weight of single regular message delivery transaction on Rialto chain.
 ///
@@ -92,7 +92,7 @@ pub const MAX_SINGLE_MESSAGE_DELIVERY_CONFIRMATION_TX_WEIGHT: Weight = 2_000_000
 /// chain. Don't put too much reserve there, because it is used to **decrease**
 /// `DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT` cost. So putting large reserve would make delivery
 /// transactions cheaper.
-pub const PAY_INBOUND_DISPATCH_FEE_WEIGHT: Weight = 600_000_000;
+pub const PAY_INBOUND_DISPATCH_FEE_WEIGHT: Weight = 700_000_000;
 
 /// The target length of a session (how often authorities change) on Rialto measured in of number of
 /// blocks.
@@ -169,6 +169,17 @@ impl Chain for Rialto {
 	type Balance = Balance;
 	type Index = Index;
 	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)
+	}
 }
 
 /// Convert a 256-bit hash into an AccountId.
@@ -213,21 +224,13 @@ frame_support::parameter_types! {
 		.build_or_panic();
 }
 
-/// Get the maximum weight (compute time) that a Normal extrinsic on the Millau chain can use.
-pub fn max_extrinsic_weight() -> Weight {
-	BlockWeights::get()
-		.get(DispatchClass::Normal)
-		.max_extrinsic
-		.unwrap_or(Weight::MAX)
-}
+/// Name of the With-Rialto GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_RIALTO_GRANDPA_PALLET_NAME: &str = "BridgeRialtoGrandpa";
+/// Name of the With-Rialto messages pallet instance that is deployed at bridged chains.
+pub const WITH_RIALTO_MESSAGES_PALLET_NAME: &str = "BridgeRialtoMessages";
 
-/// Get the maximum length in bytes that a Normal extrinsic on the Millau chain requires.
-pub fn max_extrinsic_size() -> u32 {
-	*BlockLength::get().max.get(DispatchClass::Normal)
-}
-
-/// Name of the With-Millau messages pallet instance in the Rialto runtime.
-pub const WITH_MILLAU_MESSAGES_PALLET_NAME: &str = "BridgeMillauMessages";
+/// Name of the Millau->Rialto (actually KSM->DOT) conversion rate stored in the Rialto runtime.
+pub const MILLAU_TO_RIALTO_CONVERSION_RATE_PARAMETER_NAME: &str = "MillauToRialtoConversionRate";
 
 /// Name of the parachain registrar pallet in the Rialto runtime.
 pub const PARAS_REGISTRAR_PALLET_NAME: &str = "Registrar";
@@ -244,22 +247,6 @@ pub const TO_RIALTO_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToRialtoOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToRialtoOutboundLaneApi::message_details` runtime method.
 pub const TO_RIALTO_MESSAGE_DETAILS_METHOD: &str = "ToRialtoOutboundLaneApi_message_details";
-/// Name of the `ToRialtoOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_RIALTO_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToRialtoOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToRialtoOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_RIALTO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToRialtoOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromRialtoInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_RIALTO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromRialtoInboundLaneApi_latest_received_nonce";
-/// Name of the `FromRialtoInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_RIALTO_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromRialtoInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromRialtoInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_RIALTO_UNREWARDED_RELAYERS_STATE: &str =
-	"FromRialtoInboundLaneApi_unrewarded_relayers_state";
 
 sp_api::decl_runtime_apis! {
 	/// API for querying information about the finalized Rialto headers.
@@ -269,8 +256,6 @@ sp_api::decl_runtime_apis! {
 	pub trait RialtoFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Rialto chain.
@@ -290,6 +275,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			rialto_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -301,23 +287,6 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Rialto chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Rialto chain, not the
-	/// Rialto runtime itself.
-	pub trait FromRialtoInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
 
@@ -329,9 +298,9 @@ mod tests {
 	#[test]
 	fn maximal_account_size_does_not_overflow_constant() {
 		assert!(
-			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::default().encode().len(),
+			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize >= AccountId::from([0u8; 32]).encode().len(),
 			"Actual maximal size of encoded AccountId ({}) overflows expected ({})",
-			AccountId::default().encode().len(),
+			AccountId::from([0u8; 32]).encode().len(),
 			MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
 		);
 	}
diff --git a/polkadot/bridges/primitives/chain-rococo/Cargo.toml b/polkadot/bridges/primitives/chain-rococo/Cargo.toml
index 3fc52783045b2356585f8bf7231e5e895400372e..814cd09bf170c5ddf2a067761ae543d728420222 100644
--- a/polkadot/bridges/primitives/chain-rococo/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-rococo/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-rococo"
 description = "Primitives of Rococo runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/primitives/chain-rococo/src/lib.rs b/polkadot/bridges/primitives/chain-rococo/src/lib.rs
index 8cd22eef60958efa7e0bc667d8b612b6bb81f8ac..127e75d5f8b2fd11081243792fb9042bcb5b2af1 100644
--- a/polkadot/bridges/primitives/chain-rococo/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-rococo/src/lib.rs
@@ -18,10 +18,11 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
 use frame_support::weights::{
 	Weight, WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial,
 };
+use sp_runtime::FixedU128;
 use sp_std::prelude::*;
 use sp_version::RuntimeVersion;
 
@@ -30,19 +31,19 @@ pub use bp_polkadot_core::*;
 /// Rococo Chain
 pub type Rococo = PolkadotLike;
 
-/// The target length of a session (how often authorities change) on Westend measured in of number
+/// The target length of a session (how often authorities change) on Rococo measured in of number
 /// of blocks.
 ///
 /// Note that since this is a target sessions may change before/after this time depending on network
 /// conditions.
-pub const SESSION_LENGTH: BlockNumber = 10 * time_units::MINUTES;
+pub const SESSION_LENGTH: BlockNumber = time_units::HOURS;
 
 // NOTE: This needs to be kept up to date with the Rococo runtime found in the Polkadot repo.
 pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: sp_version::create_runtime_str!("rococo"),
-	impl_name: sp_version::create_runtime_str!("parity-rococo-v1.6"),
+	impl_name: sp_version::create_runtime_str!("parity-rococo-v2.0"),
 	authoring_version: 0,
-	spec_version: 9100,
+	spec_version: 9180,
 	impl_version: 0,
 	apis: sp_version::create_apis_vec![[]],
 	transaction_version: 0,
@@ -73,13 +74,13 @@ pub fn derive_account_from_wococo_id(id: bp_runtime::SourceAccount<AccountId>) -
 	AccountIdConverter::convert(encoded_id)
 }
 
-/// Name of the With-Wococo messages pallet instance in the Rococo runtime.
-pub const WITH_WOCOCO_MESSAGES_PALLET_NAME: &str = "BridgeWococoMessages";
+/// Name of the With-Rococo GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_ROCOCO_GRANDPA_PALLET_NAME: &str = "BridgeRococoGrandpa";
+/// Name of the With-Rococo messages pallet instance that is deployed at bridged chains.
+pub const WITH_ROCOCO_MESSAGES_PALLET_NAME: &str = "BridgeRococoMessages";
 
 /// Name of the `RococoFinalityApi::best_finalized` runtime method.
 pub const BEST_FINALIZED_ROCOCO_HEADER_METHOD: &str = "RococoFinalityApi_best_finalized";
-/// Name of the `RococoFinalityApi::is_known_header` runtime method.
-pub const IS_KNOWN_ROCOCO_HEADER_METHOD: &str = "RococoFinalityApi_is_known_header";
 
 /// Name of the `ToRococoOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime
 /// method.
@@ -87,22 +88,9 @@ pub const TO_ROCOCO_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToRococoOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToRococoOutboundLaneApi::message_details` runtime method.
 pub const TO_ROCOCO_MESSAGE_DETAILS_METHOD: &str = "ToRococoOutboundLaneApi_message_details";
-/// Name of the `ToRococoOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_ROCOCO_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToRococoOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToRococoOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_ROCOCO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToRococoOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromRococoInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_ROCOCO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromRococoInboundLaneApi_latest_received_nonce";
-/// Name of the `FromRococoInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_ROCOCO_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromRococoInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromRococoInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_ROCOCO_UNREWARDED_RELAYERS_STATE: &str =
-	"FromRococoInboundLaneApi_unrewarded_relayers_state";
+
+/// Existential deposit on Rococo.
+pub const EXISTENTIAL_DEPOSIT: Balance = 1_000_000_000_000 / 100;
 
 /// Weight of pay-dispatch-fee operation for inbound messages at Rococo chain.
 ///
@@ -121,8 +109,6 @@ sp_api::decl_runtime_apis! {
 	pub trait RococoFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Rococo chain.
@@ -142,6 +128,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			rococo_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -153,22 +140,5 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Rococo chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Rococo chain, not the
-	/// Rococo runtime itself.
-	pub trait FromRococoInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
diff --git a/polkadot/bridges/primitives/chain-westend/Cargo.toml b/polkadot/bridges/primitives/chain-westend/Cargo.toml
index 4fd1652744ed6473690016bcf9b812ae29505c85..ee6e2b9be99a0182b366857092498a4faacf149d 100644
--- a/polkadot/bridges/primitives/chain-westend/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-westend/Cargo.toml
@@ -3,18 +3,17 @@ name = "bp-westend"
 description = "Primitives of Westend runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-parity-scale-codec = { version = "2.2.0", default-features = false, features = ["derive"] }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+parity-scale-codec = { version = "3.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 smallvec = "1.7"
 
 # 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 }
 
@@ -30,7 +29,6 @@ sp-version = { git = "https://github.com/paritytech/substrate", branch = "master
 default = ["std"]
 std = [
 	"bp-header-chain/std",
-	"bp-messages/std",
 	"bp-polkadot-core/std",
 	"bp-runtime/std",
 	"frame-support/std",
diff --git a/polkadot/bridges/primitives/chain-westend/src/lib.rs b/polkadot/bridges/primitives/chain-westend/src/lib.rs
index 7c9b2b65debaafec4e34f692744949f837eb4754..c7ebe4b00fd5d2c90b46ad1b90c9e60ed7e3347f 100644
--- a/polkadot/bridges/primitives/chain-westend/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-westend/src/lib.rs
@@ -18,7 +18,6 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
 use frame_support::weights::{
 	WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial,
 };
@@ -54,10 +53,11 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: sp_version::create_runtime_str!("westend"),
 	impl_name: sp_version::create_runtime_str!("parity-westend"),
 	authoring_version: 2,
-	spec_version: 51,
+	spec_version: 9140,
 	impl_version: 0,
 	apis: sp_version::create_apis_vec![[]],
-	transaction_version: 5,
+	transaction_version: 8,
+	state_version: 0,
 };
 
 /// Westend Runtime `Call` enum.
@@ -86,33 +86,11 @@ pub fn derive_account_from_rococo_id(id: bp_runtime::SourceAccount<AccountId>) -
 	AccountIdConverter::convert(encoded_id)
 }
 
+/// Name of the With-Westend GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_WESTEND_GRANDPA_PALLET_NAME: &str = "BridgeWestendGrandpa";
+
 /// Name of the `WestendFinalityApi::best_finalized` runtime method.
 pub const BEST_FINALIZED_WESTEND_HEADER_METHOD: &str = "WestendFinalityApi_best_finalized";
-/// Name of the `WestendFinalityApi::is_known_header` runtime method.
-pub const IS_KNOWN_WESTEND_HEADER_METHOD: &str = "WestendFinalityApi_is_known_header";
-
-/// Name of the `ToWestendOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime
-/// method.
-pub const TO_WESTEND_ESTIMATE_MESSAGE_FEE_METHOD: &str =
-	"ToWestendOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
-/// Name of the `ToWestendOutboundLaneApi::message_details` runtime method.
-pub const TO_WESTEND_MESSAGE_DETAILS_METHOD: &str = "ToWestendOutboundLaneApi_message_details";
-/// Name of the `ToWestendOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_WESTEND_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToWestendOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToWestendOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_WESTEND_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToWestendOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromWestendInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_WESTEND_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromWestendInboundLaneApi_latest_received_nonce";
-/// Name of the `FromWestendInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_WESTEND_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromWestendInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromWestendInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_WESTEND_UNREWARDED_RELAYERS_STATE: &str =
-	"FromWestendInboundLaneApi_unrewarded_relayers_state";
 
 /// The target length of a session (how often authorities change) on Westend measured in of number
 /// of blocks.
@@ -129,54 +107,5 @@ sp_api::decl_runtime_apis! {
 	pub trait WestendFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
-	}
-
-	/// Outbound message lane API for messages that are sent to Westend chain.
-	///
-	/// This API is implemented by runtimes that are sending messages to Westend chain, not the
-	/// Westend runtime itself.
-	pub trait ToWestendOutboundLaneApi<OutboundMessageFee: Parameter, OutboundPayload: Parameter> {
-		/// Estimate message delivery and dispatch fee that needs to be paid by the sender on
-		/// this chain.
-		///
-		/// Returns `None` if message is too expensive to be sent to Westend from this chain.
-		///
-		/// Please keep in mind that this method returns the lowest message fee required for message
-		/// to be accepted to the lane. It may be good idea to pay a bit over this price to account
-		/// future exchange rate changes and guarantee that relayer would deliver your message
-		/// to the target chain.
-		fn estimate_message_delivery_and_dispatch_fee(
-			lane_id: LaneId,
-			payload: OutboundPayload,
-		) -> Option<OutboundMessageFee>;
-		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
-		/// messages in given inclusive range.
-		///
-		/// 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<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Westend chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Westend chain, not the
-	/// Westend runtime itself.
-	pub trait FromWestendInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
diff --git a/polkadot/bridges/primitives/chain-wococo/Cargo.toml b/polkadot/bridges/primitives/chain-wococo/Cargo.toml
index d781de91dfcb6f3fdc698cb0c488a95fbb5de707..633cdd15c1f56918f78863e67562d340d62d096f 100644
--- a/polkadot/bridges/primitives/chain-wococo/Cargo.toml
+++ b/polkadot/bridges/primitives/chain-wococo/Cargo.toml
@@ -3,7 +3,7 @@ name = "bp-wococo"
 description = "Primitives of Wococo runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/primitives/chain-wococo/src/lib.rs b/polkadot/bridges/primitives/chain-wococo/src/lib.rs
index d071cf8279ea62383fd9a72f315f8a8334baffa7..f39543114c78bfe1656d3a33f975ce47541a2389 100644
--- a/polkadot/bridges/primitives/chain-wococo/src/lib.rs
+++ b/polkadot/bridges/primitives/chain-wococo/src/lib.rs
@@ -18,16 +18,24 @@
 // RuntimeApi generated functions
 #![allow(clippy::too_many_arguments)]
 
-use bp_messages::{LaneId, MessageDetails, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{LaneId, MessageDetails, MessageNonce};
+use sp_runtime::FixedU128;
 use sp_std::prelude::*;
 
 pub use bp_polkadot_core::*;
 // Rococo runtime = Wococo runtime
-pub use bp_rococo::{WeightToFee, PAY_INBOUND_DISPATCH_FEE_WEIGHT, SESSION_LENGTH, VERSION};
+pub use bp_rococo::{WeightToFee, EXISTENTIAL_DEPOSIT, PAY_INBOUND_DISPATCH_FEE_WEIGHT, VERSION};
 
 /// Wococo Chain
 pub type Wococo = PolkadotLike;
 
+/// The target length of a session (how often authorities change) on Wococo measured in of number
+/// of blocks.
+///
+/// Note that since this is a target sessions may change before/after this time depending on network
+/// conditions.
+pub const SESSION_LENGTH: BlockNumber = time_units::MINUTES;
+
 // We use this to get the account on Wococo (target) which is derived from Rococo's (source)
 // account.
 pub fn derive_account_from_rococo_id(id: bp_runtime::SourceAccount<AccountId>) -> AccountId {
@@ -35,13 +43,13 @@ pub fn derive_account_from_rococo_id(id: bp_runtime::SourceAccount<AccountId>) -
 	AccountIdConverter::convert(encoded_id)
 }
 
-/// Name of the With-Rococo messages pallet instance in the Wococo runtime.
-pub const WITH_ROCOCO_MESSAGES_PALLET_NAME: &str = "BridgeRococoMessages";
+/// Name of the With-Wococo GRANDPA pallet instance that is deployed at bridged chains.
+pub const WITH_WOCOCO_GRANDPA_PALLET_NAME: &str = "BridgeWococoGrandpa";
+/// Name of the With-Wococo messages pallet instance that is deployed at bridged chains.
+pub const WITH_WOCOCO_MESSAGES_PALLET_NAME: &str = "BridgeWococoMessages";
 
 /// Name of the `WococoFinalityApi::best_finalized` runtime method.
 pub const BEST_FINALIZED_WOCOCO_HEADER_METHOD: &str = "WococoFinalityApi_best_finalized";
-/// Name of the `WococoFinalityApi::is_known_header` runtime method.
-pub const IS_KNOWN_WOCOCO_HEADER_METHOD: &str = "WococoFinalityApi_is_known_header";
 
 /// Name of the `ToWococoOutboundLaneApi::estimate_message_delivery_and_dispatch_fee` runtime
 /// method.
@@ -49,22 +57,6 @@ pub const TO_WOCOCO_ESTIMATE_MESSAGE_FEE_METHOD: &str =
 	"ToWococoOutboundLaneApi_estimate_message_delivery_and_dispatch_fee";
 /// Name of the `ToWococoOutboundLaneApi::message_details` runtime method.
 pub const TO_WOCOCO_MESSAGE_DETAILS_METHOD: &str = "ToWococoOutboundLaneApi_message_details";
-/// Name of the `ToWococoOutboundLaneApi::latest_generated_nonce` runtime method.
-pub const TO_WOCOCO_LATEST_GENERATED_NONCE_METHOD: &str =
-	"ToWococoOutboundLaneApi_latest_generated_nonce";
-/// Name of the `ToWococoOutboundLaneApi::latest_received_nonce` runtime method.
-pub const TO_WOCOCO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"ToWococoOutboundLaneApi_latest_received_nonce";
-
-/// Name of the `FromWococoInboundLaneApi::latest_received_nonce` runtime method.
-pub const FROM_WOCOCO_LATEST_RECEIVED_NONCE_METHOD: &str =
-	"FromWococoInboundLaneApi_latest_received_nonce";
-/// Name of the `FromWococoInboundLaneApi::latest_onfirmed_nonce` runtime method.
-pub const FROM_WOCOCO_LATEST_CONFIRMED_NONCE_METHOD: &str =
-	"FromWococoInboundLaneApi_latest_confirmed_nonce";
-/// Name of the `FromWococoInboundLaneApi::unrewarded_relayers_state` runtime method.
-pub const FROM_WOCOCO_UNREWARDED_RELAYERS_STATE: &str =
-	"FromWococoInboundLaneApi_unrewarded_relayers_state";
 
 sp_api::decl_runtime_apis! {
 	/// API for querying information about the finalized Wococo headers.
@@ -74,8 +66,6 @@ sp_api::decl_runtime_apis! {
 	pub trait WococoFinalityApi {
 		/// Returns number and hash of the best finalized header known to the bridge module.
 		fn best_finalized() -> (BlockNumber, Hash);
-		/// Returns true if the header is known to the runtime.
-		fn is_known_header(hash: Hash) -> bool;
 	}
 
 	/// Outbound message lane API for messages that are sent to Wococo chain.
@@ -95,6 +85,7 @@ sp_api::decl_runtime_apis! {
 		fn estimate_message_delivery_and_dispatch_fee(
 			lane_id: LaneId,
 			payload: OutboundPayload,
+			wococo_to_this_conversion_rate: Option<FixedU128>,
 		) -> Option<OutboundMessageFee>;
 		/// Returns dispatch weight, encoded payload size and delivery+dispatch fee of all
 		/// messages in given inclusive range.
@@ -106,22 +97,5 @@ sp_api::decl_runtime_apis! {
 			begin: MessageNonce,
 			end: MessageNonce,
 		) -> Vec<MessageDetails<OutboundMessageFee>>;
-		/// Returns nonce of the latest message, received by bridged chain.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Returns nonce of the latest message, generated by given lane.
-		fn latest_generated_nonce(lane: LaneId) -> MessageNonce;
-	}
-
-	/// Inbound message lane API for messages sent by Wococo chain.
-	///
-	/// This API is implemented by runtimes that are receiving messages from Wococo chain, not the
-	/// Wococo runtime itself.
-	pub trait FromWococoInboundLaneApi {
-		/// Returns nonce of the latest message, received by given lane.
-		fn latest_received_nonce(lane: LaneId) -> MessageNonce;
-		/// Nonce of the latest message that has been confirmed to the bridged chain.
-		fn latest_confirmed_nonce(lane: LaneId) -> MessageNonce;
-		/// State of the unrewarded relayers set at given lane.
-		fn unrewarded_relayers_state(lane: LaneId) -> UnrewardedRelayersState;
 	}
 }
diff --git a/polkadot/bridges/primitives/header-chain/Cargo.toml b/polkadot/bridges/primitives/header-chain/Cargo.toml
index 447f67c8df28ae4727c00ef5df0f3df4428aea16..945d79d57cd950be929ef16d98be05f8903beccd 100644
--- a/polkadot/bridges/primitives/header-chain/Cargo.toml
+++ b/polkadot/bridges/primitives/header-chain/Cargo.toml
@@ -3,15 +3,19 @@ name = "bp-header-chain"
 description = "A common interface for describing what a bridge pallet should be able to do."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
 finality-grandpa = { version = "0.15.0", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 serde = { version = "1.0", optional = true }
 
+# Bridge dependencies
+
+bp-runtime = { path = "../runtime", default-features = false }
+
 # Substrate Dependencies
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
@@ -23,10 +27,13 @@ sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", d
 [dev-dependencies]
 assert_matches = "1.5"
 bp-test-utils = { path = "../test-utils" }
+hex = "0.4"
+hex-literal = "0.3"
 
 [features]
 default = ["std"]
 std = [
+	"bp-runtime/std",
 	"codec/std",
 	"finality-grandpa/std",
 	"scale-info/std",
diff --git a/polkadot/bridges/primitives/header-chain/src/lib.rs b/polkadot/bridges/primitives/header-chain/src/lib.rs
index 5feb30aec3eeeb136b9558bf0efe8e5cf4e8cf85..28949f28de5d862c93e9759bc58a11a52fe6629d 100644
--- a/polkadot/bridges/primitives/header-chain/src/lib.rs
+++ b/polkadot/bridges/primitives/header-chain/src/lib.rs
@@ -29,6 +29,7 @@ use sp_runtime::{generic::OpaqueDigestItemId, traits::Header as HeaderT, Runtime
 use sp_std::boxed::Box;
 
 pub mod justification;
+pub mod storage_keys;
 
 /// A type that can be used as a parameter in a dispatchable function.
 ///
diff --git a/polkadot/bridges/primitives/header-chain/src/storage_keys.rs b/polkadot/bridges/primitives/header-chain/src/storage_keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e123703eed50e74e18c47936672314989a517941
--- /dev/null
+++ b/polkadot/bridges/primitives/header-chain/src/storage_keys.rs
@@ -0,0 +1,78 @@
+// 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/>.
+
+//! Storage keys of bridge GRANDPA pallet.
+
+/// Name of the `IsHalted` storage value.
+pub const IS_HALTED_VALUE_NAME: &str = "IsHalted";
+/// Name of the `BestFinalized` storage value.
+pub const BEST_FINALIZED_VALUE_NAME: &str = "BestFinalized";
+
+use sp_core::storage::StorageKey;
+
+/// Storage key of the `IsHalted` flag in the runtime storage.
+pub fn is_halted_key(pallet_prefix: &str) -> StorageKey {
+	StorageKey(
+		bp_runtime::storage_value_final_key(
+			pallet_prefix.as_bytes(),
+			IS_HALTED_VALUE_NAME.as_bytes(),
+		)
+		.to_vec(),
+	)
+}
+
+/// Storage key of the best finalized header hash value in the runtime storage.
+pub fn best_finalized_hash_key(pallet_prefix: &str) -> StorageKey {
+	StorageKey(
+		bp_runtime::storage_value_final_key(
+			pallet_prefix.as_bytes(),
+			BEST_FINALIZED_VALUE_NAME.as_bytes(),
+		)
+		.to_vec(),
+	)
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use hex_literal::hex;
+
+	#[test]
+	fn is_halted_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is breaking
+		// compatibility with previous pallet.
+		let storage_key = is_halted_key("BridgeGrandpa").0;
+		assert_eq!(
+			storage_key,
+			hex!("0b06f475eddb98cf933a12262e0388de9611a984bbd04e2fd39f97bbc006115f").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+
+	#[test]
+	fn best_finalized_hash_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is breaking
+		// compatibility with previous pallet.
+		let storage_key = best_finalized_hash_key("BridgeGrandpa").0;
+		assert_eq!(
+			storage_key,
+			hex!("0b06f475eddb98cf933a12262e0388dea4ebafdd473c549fdb24c5c991c5591c").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+}
diff --git a/polkadot/bridges/primitives/message-dispatch/Cargo.toml b/polkadot/bridges/primitives/message-dispatch/Cargo.toml
index ed2e90accd572d3b74918efbeaf2c917207fe42a..39b2d00111e15129f185f165f91478ab164faedb 100644
--- a/polkadot/bridges/primitives/message-dispatch/Cargo.toml
+++ b/polkadot/bridges/primitives/message-dispatch/Cargo.toml
@@ -3,13 +3,13 @@ name = "bp-message-dispatch"
 description = "Primitives of bridge messages dispatch modules."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 bp-runtime = { path = "../runtime", default-features = false }
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Substrate Dependencies
 
diff --git a/polkadot/bridges/primitives/messages/Cargo.toml b/polkadot/bridges/primitives/messages/Cargo.toml
index 1271ce616c949ad4abc4f443214878821684db7b..2a84f74d225bdc0c70c11dd8fed2f5142d4c9df1 100644
--- a/polkadot/bridges/primitives/messages/Cargo.toml
+++ b/polkadot/bridges/primitives/messages/Cargo.toml
@@ -3,14 +3,14 @@ name = "bp-messages"
 description = "Primitives of messages module."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
+bitvec = { version = "1", default-features = false, features = ["alloc"] }
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "bit-vec"] }
 impl-trait-for-tuples = "0.2"
-scale-info = { version = "2.0.0", default-features = false, features = ["bit-vec", "derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["bit-vec", "derive"] }
 serde = { version = "1.0", optional = true, features = ["derive"] }
 
 # Bridge dependencies
@@ -21,16 +21,23 @@ bp-runtime = { path = "../runtime", default-features = false }
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
+[dev-dependencies]
+hex = "0.4"
+hex-literal = "0.3"
+
 [features]
 default = ["std"]
 std = [
+	"bitvec/std",
 	"bp-runtime/std",
 	"codec/std",
 	"frame-support/std",
 	"frame-system/std",
 	"scale-info/std",
 	"serde",
-	"sp-std/std",
+	"sp-core/std",
+	"sp-std/std"
 ]
diff --git a/polkadot/bridges/primitives/messages/src/lib.rs b/polkadot/bridges/primitives/messages/src/lib.rs
index e657214a7a9c77feb9c2d402158fb6a3223d39d6..05ac38d7e48997702646e007d1f4ab2d9b463dc8 100644
--- a/polkadot/bridges/primitives/messages/src/lib.rs
+++ b/polkadot/bridges/primitives/messages/src/lib.rs
@@ -28,6 +28,7 @@ use scale_info::TypeInfo;
 use sp_std::{collections::vec_deque::VecDeque, prelude::*};
 
 pub mod source_chain;
+pub mod storage_keys;
 pub mod target_chain;
 
 // Weight is reexported to avoid additional frame-support dependencies in related crates.
@@ -222,15 +223,9 @@ impl DeliveredMessages {
 	/// Create new `DeliveredMessages` struct that confirms delivery of single nonce with given
 	/// dispatch result.
 	pub fn new(nonce: MessageNonce, dispatch_result: bool) -> Self {
-		DeliveredMessages {
-			begin: nonce,
-			end: nonce,
-			dispatch_results: if dispatch_result {
-				bitvec![u8, Msb0; 1]
-			} else {
-				bitvec![u8, Msb0; 0]
-			},
-		}
+		let mut dispatch_results = BitVec::with_capacity(1);
+		dispatch_results.push(if dispatch_result { true } else { false });
+		DeliveredMessages { begin: nonce, end: nonce, dispatch_results }
 	}
 
 	/// Return total count of delivered messages.
diff --git a/polkadot/bridges/primitives/messages/src/source_chain.rs b/polkadot/bridges/primitives/messages/src/source_chain.rs
index 1ff05abf131eae97417354300640744c05a000c8..fa7b3bb85ed0537094963f566d102cef4bdd145f 100644
--- a/polkadot/bridges/primitives/messages/src/source_chain.rs
+++ b/polkadot/bridges/primitives/messages/src/source_chain.rs
@@ -28,7 +28,21 @@ use sp_std::{
 };
 
 /// The sender of the message on the source chain.
-pub type Sender<AccountId> = frame_system::RawOrigin<AccountId>;
+pub trait SenderOrigin<AccountId> {
+	/// Return id of the account that is sending this message.
+	///
+	/// In regular messages configuration, when regular message is sent you'll always get `Some(_)`
+	/// from this call. This is the account that is paying send costs. However, there are some
+	/// examples when `None` may be returned from the call:
+	///
+	/// - if the send-message call origin is either `frame_system::RawOrigin::Root` or
+	///   `frame_system::RawOrigin::None` and your configuration forbids such messages;
+	/// - if your configuration allows 'unpaid' messages sent by pallets. Then the pallet may just
+	///   use its own defined origin (not linked to any account) and the message will be accepted.
+	///   This may be useful for pallets that are sending important system-wide information (like
+	///   update of runtime version).
+	fn linked_account(&self) -> Option<AccountId>;
+}
 
 /// Relayers rewards, grouped by relayer account id.
 pub type RelayersRewards<AccountId, Balance> = BTreeMap<AccountId, RelayerRewards<Balance>>;
@@ -82,14 +96,14 @@ pub trait TargetHeaderChain<Payload, AccountId> {
 /// Lane3 until some block, ...), then it may be built using this verifier.
 ///
 /// Any fee requirements should also be enforced here.
-pub trait LaneMessageVerifier<Submitter, Payload, Fee> {
+pub trait LaneMessageVerifier<SenderOrigin, Submitter, Payload, Fee> {
 	/// Error type.
 	type Error: Debug + Into<&'static str>;
 
 	/// Verify message payload and return Ok(()) if message is valid and allowed to be sent over the
 	/// lane.
 	fn verify_message(
-		submitter: &Sender<Submitter>,
+		submitter: &SenderOrigin,
 		delivery_and_dispatch_fee: &Fee,
 		lane: &LaneId,
 		outbound_data: &OutboundLaneData,
@@ -110,14 +124,14 @@ pub trait LaneMessageVerifier<Submitter, Payload, Fee> {
 /// So to be sure that any non-altruist relayer would agree to deliver message, submitter
 /// should set `delivery_and_dispatch_fee` to at least (equivalent of): sum of fees from (2)
 /// to (4) above, plus some interest for the relayer.
-pub trait MessageDeliveryAndDispatchPayment<AccountId, Balance> {
+pub trait MessageDeliveryAndDispatchPayment<SenderOrigin, AccountId, Balance> {
 	/// Error type.
 	type Error: Debug + Into<&'static str>;
 
 	/// Withhold/write-off delivery_and_dispatch_fee from submitter account to
 	/// some relayers-fund account.
 	fn pay_delivery_and_dispatch_fee(
-		submitter: &Sender<AccountId>,
+		submitter: &SenderOrigin,
 		fee: &Balance,
 		relayer_fund_account: &AccountId,
 	) -> Result<(), Self::Error>;
@@ -145,7 +159,7 @@ pub struct SendMessageArtifacts {
 }
 
 /// Messages bridge API to be used from other pallets.
-pub trait MessagesBridge<AccountId, Balance, Payload> {
+pub trait MessagesBridge<SenderOrigin, AccountId, Balance, Payload> {
 	/// Error type.
 	type Error: Debug;
 
@@ -153,7 +167,7 @@ pub trait MessagesBridge<AccountId, Balance, Payload> {
 	///
 	/// Returns unique message nonce or error if send has failed.
 	fn send_message(
-		sender: Sender<AccountId>,
+		sender: SenderOrigin,
 		lane: LaneId,
 		message: Payload,
 		delivery_and_dispatch_fee: Balance,
@@ -164,13 +178,13 @@ pub trait MessagesBridge<AccountId, Balance, Payload> {
 #[derive(RuntimeDebug, PartialEq)]
 pub struct NoopMessagesBridge;
 
-impl<AccountId, Balance, Payload> MessagesBridge<AccountId, Balance, Payload>
-	for NoopMessagesBridge
+impl<SenderOrigin, AccountId, Balance, Payload>
+	MessagesBridge<SenderOrigin, AccountId, Balance, Payload> for NoopMessagesBridge
 {
 	type Error = &'static str;
 
 	fn send_message(
-		_sender: Sender<AccountId>,
+		_sender: SenderOrigin,
 		_lane: LaneId,
 		_message: Payload,
 		_delivery_and_dispatch_fee: Balance,
@@ -245,13 +259,13 @@ impl<Payload, AccountId> TargetHeaderChain<Payload, AccountId> for ForbidOutboun
 	}
 }
 
-impl<Submitter, Payload, Fee> LaneMessageVerifier<Submitter, Payload, Fee>
-	for ForbidOutboundMessages
+impl<SenderOrigin, Submitter, Payload, Fee>
+	LaneMessageVerifier<SenderOrigin, Submitter, Payload, Fee> for ForbidOutboundMessages
 {
 	type Error = &'static str;
 
 	fn verify_message(
-		_submitter: &Sender<Submitter>,
+		_submitter: &SenderOrigin,
 		_delivery_and_dispatch_fee: &Fee,
 		_lane: &LaneId,
 		_outbound_data: &OutboundLaneData,
@@ -261,13 +275,13 @@ impl<Submitter, Payload, Fee> LaneMessageVerifier<Submitter, Payload, Fee>
 	}
 }
 
-impl<AccountId, Balance> MessageDeliveryAndDispatchPayment<AccountId, Balance>
-	for ForbidOutboundMessages
+impl<SenderOrigin, AccountId, Balance>
+	MessageDeliveryAndDispatchPayment<SenderOrigin, AccountId, Balance> for ForbidOutboundMessages
 {
 	type Error = &'static str;
 
 	fn pay_delivery_and_dispatch_fee(
-		_submitter: &Sender<AccountId>,
+		_submitter: &SenderOrigin,
 		_fee: &Balance,
 		_relayer_fund_account: &AccountId,
 	) -> Result<(), Self::Error> {
diff --git a/polkadot/bridges/primitives/messages/src/storage_keys.rs b/polkadot/bridges/primitives/messages/src/storage_keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..19494b8b8527e52eb0feed3ea26907bdc9938130
--- /dev/null
+++ b/polkadot/bridges/primitives/messages/src/storage_keys.rs
@@ -0,0 +1,128 @@
+// 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/>.
+
+//! Storage keys of bridge messages pallet.
+
+/// Name of the `OPERATING_MODE_VALUE_NAME` storage value.
+pub const OPERATING_MODE_VALUE_NAME: &str = "PalletOperatingMode";
+/// Name of the `OutboundMessages` storage map.
+pub const OUTBOUND_MESSAGES_MAP_NAME: &str = "OutboundMessages";
+/// Name of the `OutboundLanes` storage map.
+pub const OUTBOUND_LANES_MAP_NAME: &str = "OutboundLanes";
+/// Name of the `InboundLanes` storage map.
+pub const INBOUND_LANES_MAP_NAME: &str = "InboundLanes";
+
+use crate::{LaneId, MessageKey, MessageNonce};
+
+use codec::Encode;
+use frame_support::Blake2_128Concat;
+use sp_core::storage::StorageKey;
+
+/// Storage key of the `PalletOperatingMode` value in the runtime storage.
+pub fn operating_mode_key(pallet_prefix: &str) -> StorageKey {
+	StorageKey(
+		bp_runtime::storage_value_final_key(
+			pallet_prefix.as_bytes(),
+			OPERATING_MODE_VALUE_NAME.as_bytes(),
+		)
+		.to_vec(),
+	)
+}
+
+/// Storage key of the outbound message in the runtime storage.
+pub fn message_key(pallet_prefix: &str, lane: &LaneId, nonce: MessageNonce) -> StorageKey {
+	bp_runtime::storage_map_final_key::<Blake2_128Concat>(
+		pallet_prefix,
+		OUTBOUND_MESSAGES_MAP_NAME,
+		&MessageKey { lane_id: *lane, nonce }.encode(),
+	)
+}
+
+/// Storage key of the outbound message lane state in the runtime storage.
+pub fn outbound_lane_data_key(pallet_prefix: &str, lane: &LaneId) -> StorageKey {
+	bp_runtime::storage_map_final_key::<Blake2_128Concat>(
+		pallet_prefix,
+		OUTBOUND_LANES_MAP_NAME,
+		lane,
+	)
+}
+
+/// Storage key of the inbound message lane state in the runtime storage.
+pub fn inbound_lane_data_key(pallet_prefix: &str, lane: &LaneId) -> StorageKey {
+	bp_runtime::storage_map_final_key::<Blake2_128Concat>(
+		pallet_prefix,
+		INBOUND_LANES_MAP_NAME,
+		lane,
+	)
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use hex_literal::hex;
+
+	#[test]
+	fn operating_mode_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is possibly
+		// breaking all existing message relays.
+		let storage_key = operating_mode_key("BridgeMessages").0;
+		assert_eq!(
+			storage_key,
+			hex!("dd16c784ebd3390a9bc0357c7511ed010f4cf0917788d791142ff6c1f216e7b3").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+
+	#[test]
+	fn storage_message_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is breaking
+		// all previously crafted messages proofs.
+		let storage_key = message_key("BridgeMessages", &*b"test", 42).0;
+		assert_eq!(
+			storage_key,
+			hex!("dd16c784ebd3390a9bc0357c7511ed018a395e6242c6813b196ca31ed0547ea79446af0e09063bd4a7874aef8a997cec746573742a00000000000000").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+
+	#[test]
+	fn outbound_lane_data_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is breaking
+		// all previously crafted outbound lane state proofs.
+		let storage_key = outbound_lane_data_key("BridgeMessages", &*b"test").0;
+		assert_eq!(
+			storage_key,
+			hex!("dd16c784ebd3390a9bc0357c7511ed0196c246acb9b55077390e3ca723a0ca1f44a8995dd50b6657a037a7839304535b74657374").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+
+	#[test]
+	fn inbound_lane_data_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that is breaking
+		// all previously crafted inbound lane state proofs.
+		let storage_key = inbound_lane_data_key("BridgeMessages", &*b"test").0;
+		assert_eq!(
+			storage_key,
+			hex!("dd16c784ebd3390a9bc0357c7511ed01e5f83cf83f2127eb47afdc35d6e43fab44a8995dd50b6657a037a7839304535b74657374").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+}
diff --git a/polkadot/bridges/primitives/polkadot-core/Cargo.toml b/polkadot/bridges/primitives/polkadot-core/Cargo.toml
index 31d2d20b9b9defe12d2b94ae13de34e7140cd30b..1542a784ef561553122a6b438198d73a6242c178 100644
--- a/polkadot/bridges/primitives/polkadot-core/Cargo.toml
+++ b/polkadot/bridges/primitives/polkadot-core/Cargo.toml
@@ -3,12 +3,12 @@ name = "bp-polkadot-core"
 description = "Primitives of Polkadot-like runtime."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 parity-scale-codec = { version = "3.0.0", default-features = false, features = ["derive"] }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Bridge Dependencies
 
diff --git a/polkadot/bridges/primitives/polkadot-core/src/lib.rs b/polkadot/bridges/primitives/polkadot-core/src/lib.rs
index 2de3aaadab329e532d59bdacd293575ce146b777..4c0a450eb7191b2aea88b65a93da0a7a3287ac8a 100644
--- a/polkadot/bridges/primitives/polkadot-core/src/lib.rs
+++ b/polkadot/bridges/primitives/polkadot-core/src/lib.rs
@@ -17,7 +17,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use bp_messages::MessageNonce;
-use bp_runtime::Chain;
+use bp_runtime::{Chain, EncodedOrDecodedCall};
 use frame_support::{
 	dispatch::Dispatchable,
 	parameter_types,
@@ -116,31 +116,17 @@ parameter_types! {
 		.build_or_panic();
 }
 
-/// Get the maximum weight (compute time) that a Normal extrinsic on the Polkadot-like chain can
-/// use.
-pub fn max_extrinsic_weight() -> Weight {
-	BlockWeights::get()
-		.get(DispatchClass::Normal)
-		.max_extrinsic
-		.unwrap_or(Weight::MAX)
-}
-
-/// Get the maximum length in bytes that a Normal extrinsic on the Polkadot-like chain requires.
-pub fn max_extrinsic_size() -> u32 {
-	*BlockLength::get().max.get(DispatchClass::Normal)
-}
-
 // TODO [#78] may need to be updated after https://github.com/paritytech/parity-bridges-common/issues/78
 /// Maximal number of messages in single delivery transaction.
 pub const MAX_MESSAGES_IN_DELIVERY_TRANSACTION: MessageNonce = 128;
 
 /// Maximal number of unrewarded relayer entries at inbound lane.
-pub const MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE: MessageNonce = 128;
+pub const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 128;
 
 // TODO [#438] should be selected keeping in mind:
 // finality delay on both chains + reward payout cost + messages throughput.
 /// Maximal number of unconfirmed messages at inbound lane.
-pub const MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE: MessageNonce = 8192;
+pub const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 8192;
 
 // One important thing about weight-related constants here is that actually we may have
 // different weights on different Polkadot-like chains. But now all deployments are
@@ -242,8 +228,12 @@ pub type SignedBlock = generic::SignedBlock<Block>;
 pub type Balance = u128;
 
 /// Unchecked Extrinsic type.
-pub type UncheckedExtrinsic<Call> =
-	generic::UncheckedExtrinsic<AccountAddress, Call, Signature, SignedExtensions<Call>>;
+pub type UncheckedExtrinsic<Call> = generic::UncheckedExtrinsic<
+	AccountAddress,
+	EncodedOrDecodedCall<Call>,
+	Signature,
+	SignedExtensions<Call>,
+>;
 
 /// Account address, used by the Polkadot-like chain.
 pub type Address = MultiAddress<AccountId, ()>;
@@ -261,7 +251,11 @@ pub type AdditionalSigned = ((), u32, u32, Hash, Hash, (), (), ());
 #[derive(PartialEq, Eq, Clone, RuntimeDebug, TypeInfo)]
 pub struct SignedExtensions<Call> {
 	encode_payload: SignedExtra,
-	additional_signed: AdditionalSigned,
+	// It may be set to `None` if extensions are decoded. We are never reconstructing transactions
+	// (and it makes no sense to do that) => decoded version of `SignedExtensions` is only used to
+	// read fields of `encode_payload`. And when resigning transaction, we're reconstructing
+	// `SignedExtensions` from the scratch.
+	additional_signed: Option<AdditionalSigned>,
 	_data: sp_std::marker::PhantomData<Call>,
 }
 
@@ -273,15 +267,20 @@ impl<Call> parity_scale_codec::Encode for SignedExtensions<Call> {
 
 impl<Call> parity_scale_codec::Decode for SignedExtensions<Call> {
 	fn decode<I: parity_scale_codec::Input>(
-		_input: &mut I,
+		input: &mut I,
 	) -> Result<Self, parity_scale_codec::Error> {
-		unimplemented!("SignedExtensions are never meant to be decoded, they are only used to create transaction");
+		SignedExtra::decode(input).map(|encode_payload| SignedExtensions {
+			encode_payload,
+			additional_signed: None,
+			_data: Default::default(),
+		})
 	}
 }
 
 impl<Call> SignedExtensions<Call> {
 	pub fn new(
-		version: sp_version::RuntimeVersion,
+		spec_version: u32,
+		transaction_version: u32,
 		era: bp_runtime::TransactionEraOf<PolkadotLike>,
 		genesis_hash: Hash,
 		nonce: Nonce,
@@ -298,16 +297,16 @@ impl<Call> SignedExtensions<Call> {
 				(),              // Check weight
 				tip.into(),      // transaction payment / tip (compact encoding)
 			),
-			additional_signed: (
+			additional_signed: Some((
 				(),
-				version.spec_version,
-				version.transaction_version,
+				spec_version,
+				transaction_version,
 				genesis_hash,
 				era.signed_payload(genesis_hash),
 				(),
 				(),
 				(),
-			),
+			)),
 			_data: Default::default(),
 		}
 	}
@@ -345,17 +344,22 @@ where
 	type Pre = ();
 
 	fn additional_signed(&self) -> Result<Self::AdditionalSigned, TransactionValidityError> {
-		Ok(self.additional_signed)
+		// we shall not ever see this error in relay, because we are never signing decoded
+		// transactions. Instead we're constructing and signing new transactions. So the error code
+		// is kinda random here
+		self.additional_signed.ok_or(TransactionValidityError::Unknown(
+			frame_support::unsigned::UnknownTransaction::Custom(0xFF),
+		))
 	}
 
 	fn pre_dispatch(
 		self,
-		who: &Self::AccountId,
-		call: &Self::Call,
-		info: &DispatchInfoOf<Self::Call>,
-		len: usize,
+		_who: &Self::AccountId,
+		_call: &Self::Call,
+		_info: &DispatchInfoOf<Self::Call>,
+		_len: usize,
 	) -> Result<Self::Pre, TransactionValidityError> {
-		Ok(self.validate(who, call, info, len).map(|_| ())?)
+		Ok(())
 	}
 }
 
@@ -373,6 +377,17 @@ impl Chain for PolkadotLike {
 	type Balance = Balance;
 	type Index = Index;
 	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)
+	}
 }
 
 /// Convert a 256-bit hash into an AccountId.
@@ -409,13 +424,11 @@ pub fn account_info_storage_key(id: &AccountId) -> Vec<u8> {
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use parity_scale_codec::Decode;
-	use sp_runtime::{codec::Encode, traits::TrailingZeroInput};
+	use sp_runtime::codec::Encode;
 
 	#[test]
 	fn maximal_encoded_account_id_size_is_correct() {
-		let actual_size =
-			AccountId::decode(&mut TrailingZeroInput::new(&[])).unwrap().encode().len();
+		let actual_size = AccountId::from([0u8; 32]).encode().len();
 		assert!(
 			actual_size <= MAXIMAL_ENCODED_ACCOUNT_ID_SIZE as usize,
 			"Actual size of encoded account id for Polkadot-like chains ({}) is larger than expected {}",
diff --git a/polkadot/bridges/primitives/runtime/Cargo.toml b/polkadot/bridges/primitives/runtime/Cargo.toml
index aa80dab056def778d8c774ea97ffa91f355d206c..085cfb9dbc6d4b91f75601ac017e64ed1ebfe202 100644
--- a/polkadot/bridges/primitives/runtime/Cargo.toml
+++ b/polkadot/bridges/primitives/runtime/Cargo.toml
@@ -3,14 +3,14 @@ name = "bp-runtime"
 description = "Primitives that may be used at (bridges) runtime level."
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
 hash-db = { version = "0.15.2", default-features = false }
 num-traits = { version = "0.2", default-features = false }
-scale-info = { version = "2.0.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Substrate Dependencies
 
diff --git a/polkadot/bridges/primitives/runtime/src/chain.rs b/polkadot/bridges/primitives/runtime/src/chain.rs
index 573782da83b1b101cf757563a678960fd0663ccf..5a7fafe9f67c0b0917cf52f0560ee48001aa0d8e 100644
--- a/polkadot/bridges/primitives/runtime/src/chain.rs
+++ b/polkadot/bridges/primitives/runtime/src/chain.rs
@@ -14,8 +14,9 @@
 // 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/>.
 
-use frame_support::Parameter;
-use num_traits::{AsPrimitive, Bounded, CheckedSub, SaturatingAdd, Zero};
+use codec::{Decode, Encode};
+use frame_support::{weights::Weight, Parameter};
+use num_traits::{AsPrimitive, Bounded, CheckedSub, Saturating, SaturatingAdd, Zero};
 use sp_runtime::{
 	traits::{
 		AtLeast32Bit, AtLeast32BitUnsigned, Hash as HashT, Header as HeaderT, MaybeDisplay,
@@ -23,7 +24,69 @@ use sp_runtime::{
 	},
 	FixedPointOperand,
 };
-use sp_std::{convert::TryFrom, fmt::Debug, hash::Hash, str::FromStr};
+use sp_std::{convert::TryFrom, fmt::Debug, hash::Hash, str::FromStr, vec, vec::Vec};
+
+/// Chain call, that is either SCALE-encoded, or decoded.
+#[derive(Debug, Clone, PartialEq)]
+pub enum EncodedOrDecodedCall<ChainCall> {
+	/// The call that is SCALE-encoded.
+	///
+	/// This variant is used when we the chain runtime is not bundled with the relay, but
+	/// we still need the represent call in some RPC calls or transactions.
+	Encoded(Vec<u8>),
+	/// The decoded call.
+	Decoded(ChainCall),
+}
+
+impl<ChainCall: Clone + Decode> EncodedOrDecodedCall<ChainCall> {
+	/// Returns decoded call.
+	pub fn to_decoded(&self) -> Result<ChainCall, codec::Error> {
+		match self {
+			Self::Encoded(ref encoded_call) =>
+				ChainCall::decode(&mut &encoded_call[..]).map_err(Into::into),
+			Self::Decoded(ref decoded_call) => Ok(decoded_call.clone()),
+		}
+	}
+
+	/// Converts self to decoded call.
+	pub fn into_decoded(self) -> Result<ChainCall, codec::Error> {
+		match self {
+			Self::Encoded(encoded_call) =>
+				ChainCall::decode(&mut &encoded_call[..]).map_err(Into::into),
+			Self::Decoded(decoded_call) => Ok(decoded_call),
+		}
+	}
+}
+
+impl<ChainCall> From<ChainCall> for EncodedOrDecodedCall<ChainCall> {
+	fn from(call: ChainCall) -> EncodedOrDecodedCall<ChainCall> {
+		EncodedOrDecodedCall::Decoded(call)
+	}
+}
+
+impl<ChainCall: Decode> Decode for EncodedOrDecodedCall<ChainCall> {
+	fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
+		// having encoded version is better than decoded, because decoding isn't required
+		// everywhere and for mocked calls it may lead to **unneeded** errors
+		match input.remaining_len()? {
+			Some(remaining_len) => {
+				let mut encoded_call = vec![0u8; remaining_len];
+				input.read(&mut encoded_call)?;
+				Ok(EncodedOrDecodedCall::Encoded(encoded_call))
+			},
+			None => Ok(EncodedOrDecodedCall::Decoded(ChainCall::decode(input)?)),
+		}
+	}
+}
+
+impl<ChainCall: Encode> Encode for EncodedOrDecodedCall<ChainCall> {
+	fn encode(&self) -> Vec<u8> {
+		match *self {
+			Self::Encoded(ref encoded_call) => encoded_call.clone(),
+			Self::Decoded(ref decoded_call) => decoded_call.encode(),
+		}
+	}
+}
 
 /// Minimal Substrate-based chain representation that may be used from no_std environment.
 pub trait Chain: Send + Sync + 'static {
@@ -46,6 +109,7 @@ pub trait Chain: Send + Sync + 'static {
 		+ MaybeMallocSizeOf
 		+ AsPrimitive<usize>
 		+ Default
+		+ Saturating
 		// original `sp_runtime::traits::Header::BlockNumber` doesn't have this trait, but
 		// `sp_runtime::generic::Era` requires block number -> `u64` conversion.
 		+ Into<u64>;
@@ -83,7 +147,6 @@ pub trait Chain: Send + Sync + 'static {
 
 	/// The user account identifier type for the runtime.
 	type AccountId: Parameter + Member + MaybeSerializeDeserialize + Debug + MaybeDisplay + Ord;
-
 	/// Balance of an account in native tokens.
 	///
 	/// The chain may support multiple tokens, but this particular type is for token that is used
@@ -114,6 +177,11 @@ pub trait Chain: Send + Sync + 'static {
 		+ Copy;
 	/// Signature type, used on this chain.
 	type Signature: Parameter + Verify;
+
+	/// Get the maximum size (in bytes) of a Normal extrinsic at this chain.
+	fn max_extrinsic_size() -> u32;
+	/// Get the maximum weight (compute time) that a Normal extrinsic at this chain can use.
+	fn max_extrinsic_weight() -> Weight;
 }
 
 /// Block number used by the chain.
diff --git a/polkadot/bridges/primitives/runtime/src/lib.rs b/polkadot/bridges/primitives/runtime/src/lib.rs
index 051dc1f43c002e93d4be74a293ed5efa6bcbc886..1d8a40339ab0c40db6fb77756bf59018a64caa1b 100644
--- a/polkadot/bridges/primitives/runtime/src/lib.rs
+++ b/polkadot/bridges/primitives/runtime/src/lib.rs
@@ -22,11 +22,11 @@ use codec::Encode;
 use frame_support::{RuntimeDebug, StorageHasher};
 use sp_core::{hash::H256, storage::StorageKey};
 use sp_io::hashing::blake2_256;
-use sp_std::{convert::TryFrom, vec::Vec};
+use sp_std::{convert::TryFrom, vec, vec::Vec};
 
 pub use chain::{
-	AccountIdOf, AccountPublicOf, BalanceOf, BlockNumberOf, Chain, HashOf, HasherOf, HeaderOf,
-	IndexOf, SignatureOf, TransactionEraOf,
+	AccountIdOf, AccountPublicOf, BalanceOf, BlockNumberOf, Chain, EncodedOrDecodedCall, HashOf,
+	HasherOf, HeaderOf, IndexOf, SignatureOf, TransactionEraOf,
 };
 pub use frame_support::storage::storage_prefix as storage_value_final_key;
 pub use storage_proof::{Error as StorageProofError, StorageProofChecker};
@@ -201,47 +201,22 @@ impl<BlockNumber: Copy + Into<u64>, BlockHash: Copy> TransactionEra<BlockNumber,
 }
 
 /// This is a copy of the
-/// `frame_support::storage::generator::StorageMap::storage_map_final_key` for `Blake2_128Concat`
-/// maps.
+/// `frame_support::storage::generator::StorageMap::storage_map_final_key` for maps based
+/// on selected hasher.
 ///
 /// We're using it because to call `storage_map_final_key` directly, we need access to the runtime
 /// and pallet instance, which (sometimes) is impossible.
-pub fn storage_map_final_key_blake2_128concat(
+pub fn storage_map_final_key<H: StorageHasher>(
 	pallet_prefix: &str,
 	map_name: &str,
 	key: &[u8],
 ) -> StorageKey {
-	storage_map_final_key_identity(
-		pallet_prefix,
-		map_name,
-		&frame_support::Blake2_128Concat::hash(key),
-	)
-}
-
-///
-pub fn storage_map_final_key_twox64_concat(
-	pallet_prefix: &str,
-	map_name: &str,
-	key: &[u8],
-) -> StorageKey {
-	storage_map_final_key_identity(pallet_prefix, map_name, &frame_support::Twox64Concat::hash(key))
-}
-
-/// This is a copy of the
-/// `frame_support::storage::generator::StorageMap::storage_map_final_key` for `Identity` maps.
-///
-/// We're using it because to call `storage_map_final_key` directly, we need access to the runtime
-/// and pallet instance, which (sometimes) is impossible.
-pub fn storage_map_final_key_identity(
-	pallet_prefix: &str,
-	map_name: &str,
-	key_hashed: &[u8],
-) -> StorageKey {
+	let key_hashed = H::hash(key);
 	let pallet_prefix_hashed = frame_support::Twox128::hash(pallet_prefix.as_bytes());
 	let storage_prefix_hashed = frame_support::Twox128::hash(map_name.as_bytes());
 
 	let mut final_key = Vec::with_capacity(
-		pallet_prefix_hashed.len() + storage_prefix_hashed.len() + key_hashed.len(),
+		pallet_prefix_hashed.len() + storage_prefix_hashed.len() + key_hashed.as_ref().len(),
 	);
 
 	final_key.extend_from_slice(&pallet_prefix_hashed[..]);
@@ -254,7 +229,7 @@ pub fn storage_map_final_key_identity(
 /// This is how a storage key of storage parameter (`parameter_types! { storage Param: bool = false;
 /// }`) is computed.
 ///
-/// Copied from `frame_support::parameter_types` macro
+/// Copied from `frame_support::parameter_types` macro.
 pub fn storage_parameter_key(parameter_name: &str) -> StorageKey {
 	let mut buffer = Vec::with_capacity(1 + parameter_name.len() + 1);
 	buffer.push(b':');
@@ -263,6 +238,20 @@ pub fn storage_parameter_key(parameter_name: &str) -> StorageKey {
 	StorageKey(sp_io::hashing::twox_128(&buffer).to_vec())
 }
 
+/// This is how a storage key of storage value is computed.
+///
+/// Copied from `frame_support::storage::storage_prefix`.
+pub fn storage_value_key(pallet_prefix: &str, value_name: &str) -> StorageKey {
+	let pallet_hash = sp_io::hashing::twox_128(pallet_prefix.as_bytes());
+	let storage_hash = sp_io::hashing::twox_128(value_name.as_bytes());
+
+	let mut final_key = vec![0u8; 32];
+	final_key[..16].copy_from_slice(&pallet_hash);
+	final_key[16..].copy_from_slice(&storage_hash);
+
+	StorageKey(final_key)
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -274,4 +263,17 @@ mod tests {
 			StorageKey(hex_literal::hex!("58942375551bb0af1682f72786b59d04").to_vec()),
 		);
 	}
+
+	#[test]
+	fn storage_value_key_works() {
+		assert_eq!(
+			storage_value_key("PalletTransactionPayment", "NextFeeMultiplier"),
+			StorageKey(
+				hex_literal::hex!(
+					"f0e954dfcca51a255ab12c60c789256a3f2edf3bdf381debe331ab7446addfdc"
+				)
+				.to_vec()
+			),
+		);
+	}
 }
diff --git a/polkadot/bridges/primitives/test-utils/Cargo.toml b/polkadot/bridges/primitives/test-utils/Cargo.toml
index 97d260c7a7a590ae64116e2aa24456d318d159e7..6da5c7c0f4b5fdaa1383f46feae6567fcfd26316 100644
--- a/polkadot/bridges/primitives/test-utils/Cargo.toml
+++ b/polkadot/bridges/primitives/test-utils/Cargo.toml
@@ -2,7 +2,7 @@
 name = "bp-test-utils"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/primitives/token-swap/Cargo.toml b/polkadot/bridges/primitives/token-swap/Cargo.toml
index 4b16c3567ea6eec862733abf4d44a0a40919795a..9097856f853ac64a764cf23b19a40e8fb2e83833 100644
--- a/polkadot/bridges/primitives/token-swap/Cargo.toml
+++ b/polkadot/bridges/primitives/token-swap/Cargo.toml
@@ -3,25 +3,36 @@ name = "bp-token-swap"
 description = "Primitives of the pallet-bridge-token-swap pallet"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
+
+# Bridge Dependencies
+
+bp-runtime = { path = "../runtime", default-features = false }
 
 # Substrate Dependencies
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
+sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
 
+[dev-dependencies]
+hex = "0.4"
+hex-literal = "0.3"
+
 [features]
 default = ["std"]
 std = [
+	"bp-runtime/std",
 	"codec/std",
 	"frame-support/std",
 	"scale-info/std",
 	"sp-core/std",
+	"sp-io/std",
 	"sp-std/std",
 ]
diff --git a/polkadot/bridges/primitives/token-swap/src/lib.rs b/polkadot/bridges/primitives/token-swap/src/lib.rs
index d46389e86891d68d9f14b68eda5574bf96c7b027..79363e5477a449018b9eeb2a13d86295d77b98d2 100644
--- a/polkadot/bridges/primitives/token-swap/src/lib.rs
+++ b/polkadot/bridges/primitives/token-swap/src/lib.rs
@@ -16,10 +16,13 @@
 
 #![cfg_attr(not(feature = "std"), no_std)]
 
+pub mod storage_keys;
+
 use codec::{Decode, Encode};
 use frame_support::{weights::Weight, RuntimeDebug};
 use scale_info::TypeInfo;
-use sp_core::U256;
+use sp_core::{H256, U256};
+use sp_io::hashing::blake2_256;
 use sp_std::vec::Vec;
 
 /// Pending token swap state.
@@ -85,6 +88,18 @@ pub struct TokenSwap<ThisBlockNumber, ThisBalance, ThisAccountId, BridgedBalance
 	pub target_account_at_bridged_chain: BridgedAccountId,
 }
 
+impl<ThisBlockNumber, ThisBalance, ThisAccountId, BridgedBalance, BridgedAccountId>
+	TokenSwap<ThisBlockNumber, ThisBalance, ThisAccountId, BridgedBalance, BridgedAccountId>
+where
+	TokenSwap<ThisBlockNumber, ThisBalance, ThisAccountId, BridgedBalance, BridgedAccountId>:
+		Encode,
+{
+	/// Returns hash, used to identify this token swap.
+	pub fn hash(&self) -> H256 {
+		self.using_encoded(blake2_256).into()
+	}
+}
+
 /// SCALE-encoded `Currency::transfer` call on the bridged chain.
 pub type RawBridgedTransferCall = Vec<u8>;
 
diff --git a/polkadot/bridges/primitives/token-swap/src/storage_keys.rs b/polkadot/bridges/primitives/token-swap/src/storage_keys.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d0aafc0d5c2730e60cc525cc57f84b89048289eb
--- /dev/null
+++ b/polkadot/bridges/primitives/token-swap/src/storage_keys.rs
@@ -0,0 +1,51 @@
+// 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/>.
+
+//! Storage keys of bridge token swap pallet.
+
+use frame_support::Identity;
+use sp_core::{storage::StorageKey, H256};
+
+/// Name of the `PendingSwaps` storage map.
+pub const PENDING_SWAPS_MAP_NAME: &str = "PendingSwaps";
+
+/// Storage key of `PendingSwaps` value with given token swap hash.
+pub fn pending_swaps_key(pallet_prefix: &str, token_swap_hash: H256) -> StorageKey {
+	bp_runtime::storage_map_final_key::<Identity>(
+		pallet_prefix,
+		PENDING_SWAPS_MAP_NAME,
+		token_swap_hash.as_ref(),
+	)
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use hex_literal::hex;
+
+	#[test]
+	fn pending_swaps_key_computed_properly() {
+		// If this test fails, then something has been changed in module storage that may break
+		// all previous swaps.
+		let storage_key = pending_swaps_key("BridgeTokenSwap", [42u8; 32].into()).0;
+		assert_eq!(
+			storage_key,
+			hex!("76276da64e7a4f454760eedeb4bad11adca2227fef56ad07cc424f1f5d128b9a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a").to_vec(),
+			"Unexpected storage key: {}",
+			hex::encode(&storage_key),
+		);
+	}
+}
diff --git a/polkadot/bridges/relays/bin-substrate/Cargo.toml b/polkadot/bridges/relays/bin-substrate/Cargo.toml
index a28c61262f403d1eb6b94b7dcafaba14c72d1c6e..fb8ff467d047dc48d3df605334f5e2bbe11e4203 100644
--- a/polkadot/bridges/relays/bin-substrate/Cargo.toml
+++ b/polkadot/bridges/relays/bin-substrate/Cargo.toml
@@ -1,14 +1,15 @@
 [package]
 name = "substrate-relay"
-version = "0.1.0"
+version = "1.0.1"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 anyhow = "1.0"
 async-std = "1.9.0"
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+async-trait = "0.1.42"
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 futures = "0.3.12"
 hex = "0.4"
 log = "0.4.14"
@@ -30,15 +31,16 @@ bp-polkadot = { path = "../../primitives/chain-polkadot" }
 bp-rialto = { path = "../../primitives/chain-rialto" }
 bp-rialto-parachain = { path = "../../primitives/chain-rialto-parachain" }
 bp-rococo = { path = "../../primitives/chain-rococo" }
-bp-token-swap = { path = "../../primitives/token-swap" }
-bp-wococo = { path = "../../primitives/chain-wococo" }
 bp-runtime = { path = "../../primitives/runtime" }
+bp-token-swap = { path = "../../primitives/token-swap" }
 bp-westend = { path = "../../primitives/chain-westend" }
+bp-wococo = { path = "../../primitives/chain-wococo" }
 bridge-runtime-common = { path = "../../bin/runtime-common" }
 finality-relay = { path = "../finality" }
 messages-relay = { path = "../messages" }
 millau-runtime = { path = "../../bin/millau/runtime" }
 pallet-bridge-dispatch = { path = "../../modules/dispatch" }
+pallet-bridge-grandpa = { path = "../../modules/grandpa" }
 pallet-bridge-messages = { path = "../../modules/messages" }
 pallet-bridge-token-swap = { path = "../../modules/token-swap" }
 relay-kusama-client = { path = "../client-kusama" }
@@ -72,8 +74,9 @@ polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot", bran
 polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 
 [dev-dependencies]
+bp-test-utils = { path = "../../primitives/test-utils" }
 hex-literal = "0.3"
 pallet-bridge-grandpa = { path = "../../modules/grandpa" }
 sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" }
 tempfile = "3.2"
-finality-grandpa = { version = "0.14.0" }
+finality-grandpa = { version = "0.15.0" }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/kusama.rs b/polkadot/bridges/relays/bin-substrate/src/chains/kusama.rs
index b12d23f2a56dc5139c81501d624bceccb593988d..9cdc6cd125e02d9db4b026e57350b629e5d43f27 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/kusama.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/kusama.rs
@@ -14,17 +14,20 @@
 // 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/>.
 
+use anyhow::anyhow;
+use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
 use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
 use relay_kusama_client::Kusama;
-use sp_core::storage::StorageKey;
-use sp_runtime::{FixedPointNumber, FixedU128};
 use sp_version::RuntimeVersion;
 
 use crate::cli::{
 	bridge,
-	encode_call::{Call, CliEncodeCall},
-	encode_message, CliChain,
+	encode_call::{self, Call, CliEncodeCall},
+	encode_message,
+	send_message::{self, DispatchFeePayment},
+	CliChain,
 };
 
 /// Weight of the `system::remark` call at Kusama.
@@ -33,21 +36,16 @@ use crate::cli::{
 /// calls in the future. But since it is used only in tests (and on test chains), this is ok.
 pub(crate) const SYSTEM_REMARK_CALL_WEIGHT: Weight = 2 * 1_345_000;
 
-/// Id of Kusama token that is used to fetch token price.
-pub(crate) const TOKEN_ID: &str = "kusama";
-
 impl CliEncodeCall for Kusama {
-	fn max_extrinsic_size() -> u32 {
-		bp_kusama::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
+			Call::Raw { data } => EncodedOrDecodedCall::Encoded(data.0.clone()),
 			Call::Remark { remark_payload, .. } => relay_kusama_client::runtime::Call::System(
 				relay_kusama_client::runtime::SystemCall::remark(
 					remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
 				),
-			),
+			)
+			.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::KUSAMA_TO_POLKADOT_INDEX => {
@@ -57,6 +55,7 @@ impl CliEncodeCall for Kusama {
 								lane.0, payload, fee.0,
 							),
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -67,13 +66,11 @@ impl CliEncodeCall for Kusama {
 		})
 	}
 
-	fn get_dispatch_info(
-		call: &relay_kusama_client::runtime::Call,
-	) -> anyhow::Result<DispatchInfo> {
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
 		match *call {
-			relay_kusama_client::runtime::Call::System(
+			EncodedOrDecodedCall::Decoded(relay_kusama_client::runtime::Call::System(
 				relay_kusama_client::runtime::SystemCall::remark(_),
-			) => Ok(DispatchInfo {
+			)) => Ok(DispatchInfo {
 				weight: crate::chains::kusama::SYSTEM_REMARK_CALL_WEIGHT,
 				class: DispatchClass::Normal,
 				pays_fee: Pays::Yes,
@@ -87,30 +84,52 @@ impl CliChain for Kusama {
 	const RUNTIME_VERSION: RuntimeVersion = bp_kusama::VERSION;
 
 	type KeyPair = sp_core::sr25519::Pair;
-	type MessagePayload = ();
+	type MessagePayload = MessagePayload<
+		bp_kusama::AccountId,
+		bp_polkadot::AccountPublic,
+		bp_polkadot::Signature,
+		Vec<u8>,
+	>;
 
 	fn ss58_format() -> u16 {
-		42
-	}
-
-	fn max_extrinsic_weight() -> Weight {
-		bp_kusama::max_extrinsic_weight()
+		sp_core::crypto::Ss58AddressFormat::from(
+			sp_core::crypto::Ss58AddressFormatRegistry::KusamaAccount,
+		)
+		.into()
 	}
 
 	fn encode_message(
-		_message: encode_message::MessagePayload,
+		message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
-		anyhow::bail!("Sending messages from Kusama is not yet supported.")
+		match message {
+			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
+				.map_err(|e| anyhow!("Failed to decode Kusama's MessagePayload: {:?}", e)),
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
+				type Source = Kusama;
+				type Target = relay_polkadot_client::Polkadot;
+
+				sender.enforce_chain::<Source>();
+				let spec_version = Target::RUNTIME_VERSION.spec_version;
+				let origin = CallOrigin::SourceAccount(sender.raw_id());
+				encode_call::preprocess_call::<Source, Target>(
+					&mut call,
+					bridge::KUSAMA_TO_POLKADOT_INDEX,
+				);
+				let call = Target::encode_call(&call)?;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					Err(anyhow::format_err!(
+						"Please specify dispatch weight of the encoded Polkadot call"
+					))
+				})?;
+
+				Ok(send_message::message_payload(
+					spec_version,
+					dispatch_weight,
+					origin,
+					&call,
+					DispatchFeePayment::AtSourceChain,
+				))
+			},
+		}
 	}
 }
-
-/// Storage key and initial value of Polkadot -> Kusama conversion rate.
-pub(crate) fn polkadot_to_kusama_conversion_rate_params() -> (StorageKey, FixedU128) {
-	(
-		bp_runtime::storage_parameter_key(
-			bp_kusama::POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME,
-		),
-		// starting relay before this parameter will be set to some value may cause troubles
-		FixedU128::from_inner(FixedU128::DIV),
-	)
-}
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/kusama_headers_to_polkadot.rs b/polkadot/bridges/relays/bin-substrate/src/chains/kusama_headers_to_polkadot.rs
index ce631ef41e0aca463e8631f5d237a403a3536cba..0c0ba2272c7e9d08b051d60a214b5c868948091b 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/kusama_headers_to_polkadot.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/kusama_headers_to_polkadot.rs
@@ -16,95 +16,52 @@
 
 //! Kusama-to-Polkadot headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_kusama_client::{Kusama, SyncHeader as KusamaSyncHeader};
-use relay_polkadot_client::{Polkadot, SigningParams as PolkadotSigningParams};
-use relay_substrate_client::{Client, TransactionSignScheme, UnsignedTransaction};
-use relay_utils::metrics::MetricsParams;
-use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
-};
+use async_trait::async_trait;
+use relay_polkadot_client::Polkadot;
+use substrate_relay_helper::{finality_pipeline::SubstrateFinalitySyncPipeline, TransactionParams};
 
 /// Maximal saturating difference between `balance(now)` and `balance(now-24h)` to treat
 /// relay as gone wild.
 ///
 /// Actual value, returned by `maximal_balance_decrease_per_day_is_sane` test is approximately 21
-/// DOT, but let's round up to 30 DOT here.
-pub(crate) const MAXIMAL_BALANCE_DECREASE_PER_DAY: bp_polkadot::Balance = 30_000_000_000;
-
-/// Kusama-to-Polkadot finality sync pipeline.
-pub(crate) type FinalityPipelineKusamaFinalityToPolkadot =
-	SubstrateFinalityToSubstrate<Kusama, Polkadot, PolkadotSigningParams>;
-
+/// DOT, and initial value of this constant was rounded up to 30 DOT. But for actual Kusama <>
+/// Polkadot deployment we'll be using the same account for delivering finality (free for mandatory
+/// headers) and messages. It means that we can't predict maximal loss. But to protect funds against
+/// relay/deployment issues, let's limit it so something that is much larger than this estimation -
+/// e.g. to 100 DOT.
+// TODO: https://github.com/paritytech/parity-bridges-common/issues/1307
+pub(crate) const MAXIMAL_BALANCE_DECREASE_PER_DAY: bp_polkadot::Balance = 100 * 10_000_000_000;
+
+/// Description of Kusama -> Polkadot finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct KusamaFinalityToPolkadot {
-	finality_pipeline: FinalityPipelineKusamaFinalityToPolkadot,
-}
-
-impl KusamaFinalityToPolkadot {
-	pub fn new(target_client: Client<Polkadot>, target_sign: PolkadotSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelineKusamaFinalityToPolkadot::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
-
+pub struct KusamaFinalityToPolkadot;
+substrate_relay_helper::generate_mocked_submit_finality_proof_call_builder!(
+	KusamaFinalityToPolkadot,
+	KusamaFinalityToPolkadotCallBuilder,
+	relay_polkadot_client::runtime::Call::BridgeKusamaGrandpa,
+	relay_polkadot_client::runtime::BridgeKusamaGrandpaCall::submit_finality_proof
+);
+
+#[async_trait]
 impl SubstrateFinalitySyncPipeline for KusamaFinalityToPolkadot {
-	type FinalitySyncPipeline = FinalityPipelineKusamaFinalityToPolkadot;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_kusama::BEST_FINALIZED_KUSAMA_HEADER_METHOD;
-
+	type SourceChain = relay_kusama_client::Kusama;
 	type TargetChain = Polkadot;
 
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
-	}
-
-	fn start_relay_guards(&self) {
-		relay_substrate_client::guard::abort_on_spec_version_change(
-			self.finality_pipeline.target_client.clone(),
-			bp_polkadot::VERSION.spec_version,
-		);
-		relay_substrate_client::guard::abort_when_account_balance_decreased(
-			self.finality_pipeline.target_client.clone(),
-			self.transactions_author(),
+	type SubmitFinalityProofCallBuilder = KusamaFinalityToPolkadotCallBuilder;
+	type TransactionSignScheme = Polkadot;
+
+	async fn start_relay_guards(
+		target_client: &relay_substrate_client::Client<Polkadot>,
+		transaction_params: &TransactionParams<sp_core::sr25519::Pair>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		substrate_relay_helper::finality_guards::start::<Polkadot, Polkadot>(
+			target_client,
+			transaction_params,
+			enable_version_guard,
 			MAXIMAL_BALANCE_DECREASE_PER_DAY,
-		);
-	}
-
-	fn transactions_author(&self) -> bp_polkadot::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Polkadot>,
-		transaction_nonce: bp_runtime::IndexOf<Polkadot>,
-		header: KusamaSyncHeader,
-		proof: GrandpaJustification<bp_kusama::Header>,
-	) -> Bytes {
-		let call = relay_polkadot_client::runtime::Call::BridgeKusamaGrandpa(
-			relay_polkadot_client::runtime::BridgeKusamaGrandpaCall::submit_finality_proof(
-				Box::new(header.into_inner()),
-				proof,
-			),
-		);
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Polkadot::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
+		)
+		.await
 	}
 }
 
@@ -132,7 +89,7 @@ pub(crate) mod tests {
 		// differ from the `DbWeight` of Rialto runtime. But now (and most probably forever) it is
 		// the same.
 		type GrandpaPalletWeights =
-			pallet_bridge_grandpa::weights::RialtoWeight<rialto_runtime::Runtime>;
+			pallet_bridge_grandpa::weights::MillauWeight<rialto_runtime::Runtime>;
 
 		// The following formula shall not be treated as super-accurate - guard is to protect from
 		// mad relays, not to protect from over-average loses.
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/kusama_messages_to_polkadot.rs b/polkadot/bridges/relays/bin-substrate/src/chains/kusama_messages_to_polkadot.rs
index 32133adc3e54bdb2d13ab1c6b341bb7a3954daaf..9a71fbe3c6213255941b52fa9e8ac3a324bd067a 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/kusama_messages_to_polkadot.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/kusama_messages_to_polkadot.rs
@@ -16,316 +16,64 @@
 
 //! Kusama-to-Polkadot messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
 use frame_support::weights::Weight;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_kusama_client::{
-	HeaderId as KusamaHeaderId, Kusama, SigningParams as KusamaSigningParams,
-};
-use relay_polkadot_client::{
-	HeaderId as PolkadotHeaderId, Polkadot, SigningParams as PolkadotSigningParams,
-};
-use relay_substrate_client::{Chain, Client, TransactionSignScheme, UnsignedTransaction};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
 
-/// Kusama-to-Polkadot message lane.
-pub type MessageLaneKusamaMessagesToPolkadot =
-	SubstrateMessageLaneToSubstrate<Kusama, KusamaSigningParams, Polkadot, PolkadotSigningParams>;
-
-#[derive(Clone)]
-pub struct KusamaMessagesToPolkadot {
-	message_lane: MessageLaneKusamaMessagesToPolkadot,
-}
+use messages_relay::relay_strategy::MixStrategy;
+use relay_kusama_client::Kusama;
+use relay_polkadot_client::Polkadot;
+use substrate_relay_helper::messages_lane::SubstrateMessageLane;
+
+/// Description of Kusama -> Polkadot messages bridge.
+#[derive(Clone, Debug)]
+pub struct KusamaMessagesToPolkadot;
+substrate_relay_helper::generate_mocked_receive_message_proof_call_builder!(
+	KusamaMessagesToPolkadot,
+	KusamaMessagesToPolkadotReceiveMessagesProofCallBuilder,
+	relay_polkadot_client::runtime::Call::BridgeKusamaMessages,
+	relay_polkadot_client::runtime::BridgeKusamaMessagesCall::receive_messages_proof
+);
+substrate_relay_helper::generate_mocked_receive_message_delivery_proof_call_builder!(
+	KusamaMessagesToPolkadot,
+	KusamaMessagesToPolkadotReceiveMessagesDeliveryProofCallBuilder,
+	relay_kusama_client::runtime::Call::BridgePolkadotMessages,
+	relay_kusama_client::runtime::BridgePolkadotMessagesCall::receive_messages_delivery_proof
+);
+substrate_relay_helper::generate_mocked_update_conversion_rate_call_builder!(
+	Kusama,
+	KusamaMessagesToPolkadotUpdateConversionRateCallBuilder,
+	relay_kusama_client::runtime::Call::BridgePolkadotMessages,
+	relay_kusama_client::runtime::BridgePolkadotMessagesCall::update_pallet_parameter,
+	relay_kusama_client::runtime::BridgePolkadotMessagesParameter::PolkadotToKusamaConversionRate
+);
 
 impl SubstrateMessageLane for KusamaMessagesToPolkadot {
-	type MessageLane = MessageLaneKusamaMessagesToPolkadot;
-
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_polkadot::TO_POLKADOT_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_polkadot::TO_POLKADOT_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_polkadot::TO_POLKADOT_LATEST_RECEIVED_NONCE_METHOD;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_polkadot::KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME);
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_kusama::POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME);
 
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_kusama::FROM_KUSAMA_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_kusama::FROM_KUSAMA_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_kusama::FROM_KUSAMA_UNREWARDED_RELAYERS_STATE;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_polkadot::KUSAMA_FEE_MULTIPLIER_PARAMETER_NAME);
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_kusama::POLKADOT_FEE_MULTIPLIER_PARAMETER_NAME);
 
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_kusama::BEST_FINALIZED_KUSAMA_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_polkadot::BEST_FINALIZED_POLKADOT_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str =
-		bp_kusama::WITH_POLKADOT_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str =
-		bp_polkadot::WITH_KUSAMA_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_polkadot::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> =
+		Some(bp_kusama::TRANSACTION_PAYMENT_PALLET_NAME);
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> =
+		Some(bp_polkadot::TRANSACTION_PAYMENT_PALLET_NAME);
 
 	type SourceChain = Kusama;
 	type TargetChain = Polkadot;
 
-	fn source_transactions_author(&self) -> bp_kusama::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: KusamaHeaderId,
-		transaction_nonce: bp_runtime::IndexOf<Kusama>,
-		_generated_at_block: PolkadotHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call = relay_kusama_client::runtime::Call::BridgePolkadotMessages(
-			relay_kusama_client::runtime::BridgePolkadotMessagesCall::receive_messages_delivery_proof(
-				proof,
-				relayers_state,
-			),
-		);
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Kusama::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Polkadot -> Kusama confirmation transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_kusama::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_kusama::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-
-	fn target_transactions_author(&self) -> bp_polkadot::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: PolkadotHeaderId,
-		transaction_nonce: bp_runtime::IndexOf<Polkadot>,
-		_generated_at_header: KusamaHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
+	type SourceTransactionSignScheme = Kusama;
+	type TargetTransactionSignScheme = Polkadot;
 
-		let call = relay_polkadot_client::runtime::Call::BridgeKusamaMessages(
-			relay_polkadot_client::runtime::BridgeKusamaMessagesCall::receive_messages_proof(
-				self.message_lane.relayer_id_at_source.clone(),
-				proof,
-				messages_count as _,
-				dispatch_weight,
-			),
-		);
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Polkadot::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Kusama -> Polkadot delivery transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_polkadot::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_polkadot::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Kusama node as messages source.
-type KusamaSourceClient = SubstrateMessagesSource<KusamaMessagesToPolkadot>;
-
-/// Polkadot node as messages target.
-type PolkadotTargetClient = SubstrateMessagesTarget<KusamaMessagesToPolkadot>;
-
-/// Run Kusama-to-Polkadot messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Kusama,
-		KusamaSigningParams,
-		Polkadot,
-		PolkadotSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Kusama::AVERAGE_BLOCK_INTERVAL,
-		Polkadot::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_kusama = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = KusamaMessagesToPolkadot {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_kusama,
-		},
-	};
+	type ReceiveMessagesProofCallBuilder = KusamaMessagesToPolkadotReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		KusamaMessagesToPolkadotReceiveMessagesDeliveryProofCallBuilder;
 
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_polkadot::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 (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>,
-		>(
-			bp_polkadot::max_extrinsic_weight(),
-			bp_polkadot::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-	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);
-
-	log::info!(
-		target: "bridge",
-		"Starting Kusama -> Polkadot messages relay.\n\t\
-			Kusama relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Kusama::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Polkadot::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_polkadot::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_polkadot::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		KusamaSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		PolkadotTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
-
-/// Create standalone metrics for the Kusama -> Polkadot messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Kusama>,
-	target_client: Client<Polkadot>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Kusama, Polkadot>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		Some(crate::chains::kusama::TOKEN_ID),
-		Some(crate::chains::polkadot::TOKEN_ID),
-		Some(crate::chains::polkadot::kusama_to_polkadot_conversion_rate_params()),
-		Some(crate::chains::kusama::polkadot_to_kusama_conversion_rate_params()),
-	)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder =
+		KusamaMessagesToPolkadotUpdateConversionRateCallBuilder;
 
-/// Update Polkadot -> Kusama conversion rate, stored in Kusama runtime storage.
-pub(crate) async fn update_polkadot_to_kusama_conversion_rate(
-	client: Client<Kusama>,
-	signer: <Kusama as TransactionSignScheme>::AccountKeyPair,
-	updated_rate: f64,
-) -> anyhow::Result<()> {
-	let genesis_hash = *client.genesis_hash();
-	let signer_id = (*signer.public().as_array_ref()).into();
-	client
-		.submit_signed_extrinsic(signer_id, move |_, transaction_nonce| {
-			Bytes(
-				Kusama::sign_transaction(
-					genesis_hash,
-					&signer,
-					relay_substrate_client::TransactionEra::immortal(),
-					UnsignedTransaction::new(
-						relay_kusama_client::runtime::Call::BridgePolkadotMessages(
-							relay_kusama_client::runtime::BridgePolkadotMessagesCall::update_pallet_parameter(
-								relay_kusama_client::runtime::BridgePolkadotMessagesParameter::PolkadotToKusamaConversionRate(
-									sp_runtime::FixedU128::from_float(updated_rate),
-								)
-							)
-						),
-						transaction_nonce,
-					),
-				)
-					.encode(),
-			)
-		})
-		.await
-		.map(drop)
-		.map_err(|err| anyhow::format_err!("{:?}", err))
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/millau.rs b/polkadot/bridges/relays/bin-substrate/src/chains/millau.rs
index 755d7cc4430a442e39d149e23ad60cc05b279d82..1fc1e8308ef451a58c42a33e40d55fb96a2bf692 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/millau.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/millau.rs
@@ -25,37 +25,27 @@ use crate::cli::{
 };
 use anyhow::anyhow;
 use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
-use frame_support::weights::{DispatchInfo, GetDispatchInfo, Weight};
+use frame_support::weights::{DispatchInfo, GetDispatchInfo};
 use relay_millau_client::Millau;
-use sp_core::storage::StorageKey;
-use sp_runtime::FixedU128;
 use sp_version::RuntimeVersion;
 
-// Millau/Rialto tokens have no any real value, so the conversion rate we use is always 1:1. But we
-// want to test our code that is intended to work with real-value chains. So to keep it close to
-// 1:1, we'll be treating Rialto as BTC and Millau as wBTC (only in relayer).
-
-/// The identifier of token, which value is associated with Millau token value by relayer.
-pub(crate) const ASSOCIATED_TOKEN_ID: &str = crate::chains::kusama::TOKEN_ID;
-
 impl CliEncodeCall for Millau {
-	fn max_extrinsic_size() -> u32 {
-		bp_millau::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
-			Call::Raw { data } => Decode::decode(&mut &*data.0)?,
+			Call::Raw { data } => Self::Call::decode(&mut &*data.0)?.into(),
 			Call::Remark { remark_payload, .. } =>
 				millau_runtime::Call::System(millau_runtime::SystemCall::remark {
 					remark: remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
-				}),
+				})
+				.into(),
 			Call::Transfer { recipient, amount } =>
 				millau_runtime::Call::Balances(millau_runtime::BalancesCall::transfer {
 					dest: recipient.raw_id(),
 					value: amount.cast(),
-				}),
+				})
+				.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::MILLAU_TO_RIALTO_INDEX => {
@@ -67,6 +57,7 @@ impl CliEncodeCall for Millau {
 								delivery_and_dispatch_fee: fee.cast(),
 							},
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -76,8 +67,8 @@ impl CliEncodeCall for Millau {
 		})
 	}
 
-	fn get_dispatch_info(call: &millau_runtime::Call) -> anyhow::Result<DispatchInfo> {
-		Ok(call.get_dispatch_info())
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
+		Ok(call.to_decoded()?.get_dispatch_info())
 	}
 }
 
@@ -96,10 +87,6 @@ impl CliChain for Millau {
 		millau_runtime::SS58Prefix::get() as u16
 	}
 
-	fn max_extrinsic_weight() -> Weight {
-		bp_millau::max_extrinsic_weight()
-	}
-
 	// TODO [#854|#843] support multiple bridges?
 	fn encode_message(
 		message: encode_message::MessagePayload,
@@ -107,7 +94,7 @@ impl CliChain for Millau {
 		match message {
 			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
 				.map_err(|e| anyhow!("Failed to decode Millau's MessagePayload: {:?}", e)),
-			encode_message::MessagePayload::Call { mut call, mut sender } => {
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
 				type Source = Millau;
 				type Target = relay_rialto_client::Rialto;
 
@@ -119,11 +106,13 @@ impl CliChain for Millau {
 					bridge::MILLAU_TO_RIALTO_INDEX,
 				);
 				let call = Target::encode_call(&call)?;
-				let weight = call.get_dispatch_info().weight;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					call.to_decoded().map(|call| call.get_dispatch_info().weight)
+				})?;
 
 				Ok(send_message::message_payload(
 					spec_version,
-					weight,
+					dispatch_weight,
 					origin,
 					&call,
 					DispatchFeePayment::AtSourceChain,
@@ -132,11 +121,3 @@ impl CliChain for Millau {
 		}
 	}
 }
-
-/// Storage key and initial value of Rialto -> Millau conversion rate.
-pub(crate) fn rialto_to_millau_conversion_rate_params() -> (StorageKey, FixedU128) {
-	(
-		StorageKey(millau_runtime::rialto_messages::RialtoToMillauConversionRate::key().to_vec()),
-		millau_runtime::rialto_messages::INITIAL_RIALTO_TO_MILLAU_CONVERSION_RATE,
-	)
-}
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/millau_headers_to_rialto.rs b/polkadot/bridges/relays/bin-substrate/src/chains/millau_headers_to_rialto.rs
index 14a0430f6a9182f8487e9cf1e1177cb18d3db6e2..584f0a9bb1d8b43680010dc6c9daf911f278c14b 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/millau_headers_to_rialto.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/millau_headers_to_rialto.rs
@@ -16,65 +16,22 @@
 
 //! Millau-to-Rialto headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_millau_client::{Millau, SyncHeader as MillauSyncHeader};
-use relay_rialto_client::{Rialto, SigningParams as RialtoSigningParams};
-use relay_substrate_client::{Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
 use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
+	DirectSubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
 };
 
-/// Millau-to-Rialto finality sync pipeline.
-pub(crate) type FinalityPipelineMillauToRialto =
-	SubstrateFinalityToSubstrate<Millau, Rialto, RialtoSigningParams>;
-
+/// Description of Millau -> Rialto finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct MillauFinalityToRialto {
-	finality_pipeline: FinalityPipelineMillauToRialto,
-}
-
-impl MillauFinalityToRialto {
-	pub fn new(target_client: Client<Rialto>, target_sign: RialtoSigningParams) -> Self {
-		Self { finality_pipeline: FinalityPipelineMillauToRialto::new(target_client, target_sign) }
-	}
-}
+pub struct MillauFinalityToRialto;
 
 impl SubstrateFinalitySyncPipeline for MillauFinalityToRialto {
-	type FinalitySyncPipeline = FinalityPipelineMillauToRialto;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_millau::BEST_FINALIZED_MILLAU_HEADER_METHOD;
-
-	type TargetChain = Rialto;
-
-	fn transactions_author(&self) -> bp_rialto::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Rialto>,
-		transaction_nonce: IndexOf<Rialto>,
-		header: MillauSyncHeader,
-		proof: GrandpaJustification<bp_millau::Header>,
-	) -> Bytes {
-		let call = rialto_runtime::BridgeGrandpaMillauCall::submit_finality_proof {
-			finality_target: Box::new(header.into_inner()),
-			justification: proof,
-		}
-		.into();
-
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Rialto::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
-	}
+	type SourceChain = relay_millau_client::Millau;
+	type TargetChain = relay_rialto_client::Rialto;
+
+	type SubmitFinalityProofCallBuilder = DirectSubmitFinalityProofCallBuilder<
+		Self,
+		rialto_runtime::Runtime,
+		rialto_runtime::MillauGrandpaInstance,
+	>;
+	type TransactionSignScheme = relay_rialto_client::Rialto;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs b/polkadot/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs
index c4179eea330f1cf6d61d9f2d032dfbca8184890e..f20669e6c7a5845224cce3b0ae6fb6fc8d2d59de 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/millau_messages_to_rialto.rs
@@ -16,310 +16,55 @@
 
 //! Millau-to-Rialto messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
-use frame_support::dispatch::GetDispatchInfo;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
-use frame_support::weights::Weight;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_millau_client::{
-	HeaderId as MillauHeaderId, Millau, SigningParams as MillauSigningParams,
-};
-use relay_rialto_client::{
-	HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams,
+use messages_relay::relay_strategy::MixStrategy;
+use relay_millau_client::Millau;
+use relay_rialto_client::Rialto;
+use substrate_relay_helper::messages_lane::{
+	DirectReceiveMessagesDeliveryProofCallBuilder, DirectReceiveMessagesProofCallBuilder,
+	SubstrateMessageLane,
 };
-use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
-
-/// Millau-to-Rialto message lane.
-pub type MessageLaneMillauMessagesToRialto =
-	SubstrateMessageLaneToSubstrate<Millau, MillauSigningParams, Rialto, RialtoSigningParams>;
 
-#[derive(Clone)]
-pub struct MillauMessagesToRialto {
-	message_lane: MessageLaneMillauMessagesToRialto,
-}
+/// Description of Millau -> Rialto messages bridge.
+#[derive(Clone, Debug)]
+pub struct MillauMessagesToRialto;
+substrate_relay_helper::generate_direct_update_conversion_rate_call_builder!(
+	Millau,
+	MillauMessagesToRialtoUpdateConversionRateCallBuilder,
+	millau_runtime::Runtime,
+	millau_runtime::WithRialtoMessagesInstance,
+	millau_runtime::rialto_messages::MillauToRialtoMessagesParameter::RialtoToMillauConversionRate
+);
 
 impl SubstrateMessageLane for MillauMessagesToRialto {
-	type MessageLane = MessageLaneMillauMessagesToRialto;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_rialto::MILLAU_TO_RIALTO_CONVERSION_RATE_PARAMETER_NAME);
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_millau::RIALTO_TO_MILLAU_CONVERSION_RATE_PARAMETER_NAME);
 
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_rialto::TO_RIALTO_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_rialto::TO_RIALTO_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_rialto::TO_RIALTO_LATEST_RECEIVED_NONCE_METHOD;
-
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_millau::FROM_MILLAU_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_millau::FROM_MILLAU_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_millau::FROM_MILLAU_UNREWARDED_RELAYERS_STATE;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_millau::BEST_FINALIZED_MILLAU_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_rialto::BEST_FINALIZED_RIALTO_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str = bp_millau::WITH_RIALTO_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str = bp_rialto::WITH_MILLAU_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_rialto::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
 
 	type SourceChain = Millau;
 	type TargetChain = Rialto;
 
-	fn source_transactions_author(&self) -> bp_millau::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: MillauHeaderId,
-		transaction_nonce: IndexOf<Millau>,
-		_generated_at_block: RialtoHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call: millau_runtime::Call =
-			millau_runtime::MessagesCall::receive_messages_delivery_proof { proof, relayers_state }
-				.into();
-		let call_weight = call.get_dispatch_info().weight;
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Millau::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Rialto -> Millau confirmation transaction. Weight: {}/{}, size: {}/{}",
-			call_weight,
-			bp_millau::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_millau::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
+	type SourceTransactionSignScheme = Millau;
+	type TargetTransactionSignScheme = Rialto;
 
-	fn target_transactions_author(&self) -> bp_rialto::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
+	type ReceiveMessagesProofCallBuilder = DirectReceiveMessagesProofCallBuilder<
+		Self,
+		rialto_runtime::Runtime,
+		rialto_runtime::WithMillauMessagesInstance,
+	>;
+	type ReceiveMessagesDeliveryProofCallBuilder = DirectReceiveMessagesDeliveryProofCallBuilder<
+		Self,
+		millau_runtime::Runtime,
+		millau_runtime::WithRialtoMessagesInstance,
+	>;
 
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: RialtoHeaderId,
-		transaction_nonce: IndexOf<Rialto>,
-		_generated_at_header: MillauHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
-		let call: rialto_runtime::Call = rialto_runtime::MessagesCall::receive_messages_proof {
-			relayer_id_at_bridged_chain: self.message_lane.relayer_id_at_source.clone(),
-			proof,
-			messages_count: messages_count as _,
-			dispatch_weight,
-		}
-		.into();
-		let call_weight = call.get_dispatch_info().weight;
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Rialto::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Millau -> Rialto delivery transaction. Weight: {}/{}, size: {}/{}",
-			call_weight,
-			bp_rialto::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_rialto::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Millau node as messages source.
-type MillauSourceClient = SubstrateMessagesSource<MillauMessagesToRialto>;
-
-/// Rialto node as messages target.
-type RialtoTargetClient = SubstrateMessagesTarget<MillauMessagesToRialto>;
-
-/// Run Millau-to-Rialto messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Millau,
-		MillauSigningParams,
-		Rialto,
-		RialtoSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Millau::AVERAGE_BLOCK_INTERVAL,
-		Rialto::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_millau = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = MillauMessagesToRialto {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_millau,
-		},
-	};
-
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_rialto::max_extrinsic_size() / 3;
-	// TODO: use Millau weights after https://github.com/paritytech/parity-bridges-common/issues/390
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<millau_runtime::Runtime>,
-		>(
-			bp_rialto::max_extrinsic_weight(),
-			bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-
-	log::info!(
-		target: "bridge",
-		"Starting Millau -> Rialto messages relay.\n\t\
-			Millau relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Millau::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Rialto::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_rialto::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_rialto::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		MillauSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		RialtoTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
-
-/// Create standalone metrics for the Millau -> Rialto messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Millau>,
-	target_client: Client<Rialto>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Millau, Rialto>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		Some(crate::chains::millau::ASSOCIATED_TOKEN_ID),
-		Some(crate::chains::rialto::ASSOCIATED_TOKEN_ID),
-		Some(crate::chains::rialto::millau_to_rialto_conversion_rate_params()),
-		Some(crate::chains::millau::rialto_to_millau_conversion_rate_params()),
-	)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder =
+		MillauMessagesToRialtoUpdateConversionRateCallBuilder;
 
-/// Update Rialto -> Millau conversion rate, stored in Millau runtime storage.
-pub(crate) async fn update_rialto_to_millau_conversion_rate(
-	client: Client<Millau>,
-	signer: <Millau as TransactionSignScheme>::AccountKeyPair,
-	updated_rate: f64,
-) -> anyhow::Result<()> {
-	let genesis_hash = *client.genesis_hash();
-	let signer_id = (*signer.public().as_array_ref()).into();
-	client
-		.submit_signed_extrinsic(signer_id, move |_, transaction_nonce| {
-			Bytes(
-				Millau::sign_transaction(
-					genesis_hash,
-					&signer,
-					relay_substrate_client::TransactionEra::immortal(),
-					UnsignedTransaction::new(
-						millau_runtime::MessagesCall::update_pallet_parameter {
-							parameter: millau_runtime::rialto_messages::MillauToRialtoMessagesParameter::RialtoToMillauConversionRate(
-								sp_runtime::FixedU128::from_float(updated_rate),
-							),
-						}
-						.into(),
-						transaction_nonce,
-					),
-				)
-				.encode(),
-			)
-		})
-		.await
-		.map(drop)
-		.map_err(|err| anyhow::format_err!("{:?}", err))
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/mod.rs b/polkadot/bridges/relays/bin-substrate/src/chains/mod.rs
index e9cb2d9b737f1a55caa6ce9beea3870e8ffa95b0..16901143e19fbc1936eff5750e74cde9081c409a 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/mod.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/mod.rs
@@ -39,27 +39,16 @@ mod rococo;
 mod westend;
 mod wococo;
 
-use relay_utils::metrics::{MetricsParams, StandaloneMetric};
-
-pub(crate) fn add_polkadot_kusama_price_metrics<T: finality_relay::FinalitySyncPipeline>(
-	params: MetricsParams,
-) -> anyhow::Result<MetricsParams> {
-	substrate_relay_helper::helpers::token_price_metric(polkadot::TOKEN_ID)?
-		.register_and_spawn(&params.registry)?;
-	substrate_relay_helper::helpers::token_price_metric(kusama::TOKEN_ID)?
-		.register_and_spawn(&params.registry)?;
-	Ok(params)
-}
-
 #[cfg(test)]
 mod tests {
 	use crate::cli::{encode_call, send_message};
 	use bp_messages::source_chain::TargetHeaderChain;
+	use bp_runtime::Chain as _;
 	use codec::Encode;
 	use frame_support::dispatch::GetDispatchInfo;
 	use relay_millau_client::Millau;
 	use relay_rialto_client::Rialto;
-	use relay_substrate_client::{TransactionSignScheme, UnsignedTransaction};
+	use relay_substrate_client::{SignParam, TransactionSignScheme, UnsignedTransaction};
 	use sp_core::Pair;
 	use sp_runtime::traits::{IdentifyAccount, Verify};
 
@@ -114,8 +103,8 @@ mod tests {
 		use rialto_runtime::millau_messages::Millau;
 
 		let maximal_remark_size = encode_call::compute_maximal_message_arguments_size(
-			bp_rialto::max_extrinsic_size(),
-			bp_millau::max_extrinsic_size(),
+			bp_rialto::Rialto::max_extrinsic_size(),
+			bp_millau::Millau::max_extrinsic_size(),
 		);
 
 		let call: millau_runtime::Call =
@@ -147,8 +136,8 @@ mod tests {
 	fn maximal_size_remark_to_rialto_is_generated_correctly() {
 		assert!(
 			bridge_runtime_common::messages::target::maximal_incoming_message_size(
-				bp_rialto::max_extrinsic_size()
-			) > bp_millau::max_extrinsic_size(),
+				bp_rialto::Rialto::max_extrinsic_size()
+			) > bp_millau::Millau::max_extrinsic_size(),
 			"We can't actually send maximal messages to Rialto from Millau, because Millau extrinsics can't be that large",
 		)
 	}
@@ -158,7 +147,7 @@ mod tests {
 		use rialto_runtime::millau_messages::Millau;
 
 		let maximal_dispatch_weight = send_message::compute_maximal_message_dispatch_weight(
-			bp_millau::max_extrinsic_weight(),
+			bp_millau::Millau::max_extrinsic_weight(),
 		);
 		let call: millau_runtime::Call =
 			rialto_runtime::SystemCall::remark { remark: vec![] }.into();
@@ -187,7 +176,7 @@ mod tests {
 		use millau_runtime::rialto_messages::Rialto;
 
 		let maximal_dispatch_weight = send_message::compute_maximal_message_dispatch_weight(
-			bp_rialto::max_extrinsic_weight(),
+			bp_rialto::Rialto::max_extrinsic_weight(),
 		);
 		let call: rialto_runtime::Call =
 			millau_runtime::SystemCall::remark { remark: vec![] }.into();
@@ -215,12 +204,15 @@ mod tests {
 	fn rialto_tx_extra_bytes_constant_is_correct() {
 		let rialto_call =
 			rialto_runtime::Call::System(rialto_runtime::SystemCall::remark { remark: vec![] });
-		let rialto_tx = Rialto::sign_transaction(
-			Default::default(),
-			&sp_keyring::AccountKeyring::Alice.pair(),
-			relay_substrate_client::TransactionEra::immortal(),
-			UnsignedTransaction::new(rialto_call.clone(), 0),
-		);
+		let rialto_tx = Rialto::sign_transaction(SignParam {
+			spec_version: 1,
+			transaction_version: 1,
+			genesis_hash: Default::default(),
+			signer: sp_keyring::AccountKeyring::Alice.pair(),
+			era: relay_substrate_client::TransactionEra::immortal(),
+			unsigned: UnsignedTransaction::new(rialto_call.clone().into(), 0),
+		})
+		.unwrap();
 		let extra_bytes_in_transaction = rialto_tx.encode().len() - rialto_call.encode().len();
 		assert!(
 			bp_rialto::TX_EXTRA_BYTES as usize >= extra_bytes_in_transaction,
@@ -234,12 +226,15 @@ mod tests {
 	fn millau_tx_extra_bytes_constant_is_correct() {
 		let millau_call =
 			millau_runtime::Call::System(millau_runtime::SystemCall::remark { remark: vec![] });
-		let millau_tx = Millau::sign_transaction(
-			Default::default(),
-			&sp_keyring::AccountKeyring::Alice.pair(),
-			relay_substrate_client::TransactionEra::immortal(),
-			UnsignedTransaction::new(millau_call.clone(), 0),
-		);
+		let millau_tx = Millau::sign_transaction(SignParam {
+			spec_version: 0,
+			transaction_version: 0,
+			genesis_hash: Default::default(),
+			signer: sp_keyring::AccountKeyring::Alice.pair(),
+			era: relay_substrate_client::TransactionEra::immortal(),
+			unsigned: UnsignedTransaction::new(millau_call.clone().into(), 0),
+		})
+		.unwrap();
 		let extra_bytes_in_transaction = millau_tx.encode().len() - millau_call.encode().len();
 		assert!(
 			bp_millau::TX_EXTRA_BYTES as usize >= extra_bytes_in_transaction,
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot.rs b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot.rs
index 7b6256d1749f8854a5e80efa72c8d0463436dd7d..7ae1cbc4777779e3445cc3290a0b678b9c6d2bed 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot.rs
@@ -14,17 +14,20 @@
 // 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/>.
 
+use anyhow::anyhow;
+use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
 use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
 use relay_polkadot_client::Polkadot;
-use sp_core::storage::StorageKey;
-use sp_runtime::{FixedPointNumber, FixedU128};
 use sp_version::RuntimeVersion;
 
 use crate::cli::{
 	bridge,
-	encode_call::{Call, CliEncodeCall},
-	encode_message, CliChain,
+	encode_call::{self, Call, CliEncodeCall},
+	encode_message,
+	send_message::{self, DispatchFeePayment},
+	CliChain,
 };
 
 /// Weight of the `system::remark` call at Polkadot.
@@ -33,21 +36,16 @@ use crate::cli::{
 /// calls in the future. But since it is used only in tests (and on test chains), this is ok.
 pub(crate) const SYSTEM_REMARK_CALL_WEIGHT: Weight = 2 * 1_345_000;
 
-/// Id of Polkadot token that is used to fetch token price.
-pub(crate) const TOKEN_ID: &str = "polkadot";
-
 impl CliEncodeCall for Polkadot {
-	fn max_extrinsic_size() -> u32 {
-		bp_polkadot::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
+			Call::Raw { data } => EncodedOrDecodedCall::Encoded(data.0.clone()),
 			Call::Remark { remark_payload, .. } => relay_polkadot_client::runtime::Call::System(
 				relay_polkadot_client::runtime::SystemCall::remark(
 					remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
 				),
-			),
+			)
+			.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::POLKADOT_TO_KUSAMA_INDEX => {
@@ -57,6 +55,7 @@ impl CliEncodeCall for Polkadot {
 								lane.0, payload, fee.0,
 							),
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -67,13 +66,11 @@ impl CliEncodeCall for Polkadot {
 		})
 	}
 
-	fn get_dispatch_info(
-		call: &relay_polkadot_client::runtime::Call,
-	) -> anyhow::Result<DispatchInfo> {
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
 		match *call {
-			relay_polkadot_client::runtime::Call::System(
+			EncodedOrDecodedCall::Decoded(relay_polkadot_client::runtime::Call::System(
 				relay_polkadot_client::runtime::SystemCall::remark(_),
-			) => Ok(DispatchInfo {
+			)) => Ok(DispatchInfo {
 				weight: crate::chains::polkadot::SYSTEM_REMARK_CALL_WEIGHT,
 				class: DispatchClass::Normal,
 				pays_fee: Pays::Yes,
@@ -87,30 +84,52 @@ impl CliChain for Polkadot {
 	const RUNTIME_VERSION: RuntimeVersion = bp_polkadot::VERSION;
 
 	type KeyPair = sp_core::sr25519::Pair;
-	type MessagePayload = ();
+	type MessagePayload = MessagePayload<
+		bp_polkadot::AccountId,
+		bp_kusama::AccountPublic,
+		bp_kusama::Signature,
+		Vec<u8>,
+	>;
 
 	fn ss58_format() -> u16 {
-		42
-	}
-
-	fn max_extrinsic_weight() -> Weight {
-		bp_polkadot::max_extrinsic_weight()
+		sp_core::crypto::Ss58AddressFormat::from(
+			sp_core::crypto::Ss58AddressFormatRegistry::PolkadotAccount,
+		)
+		.into()
 	}
 
 	fn encode_message(
-		_message: encode_message::MessagePayload,
+		message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
-		anyhow::bail!("Sending messages from Polkadot is not yet supported.")
+		match message {
+			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
+				.map_err(|e| anyhow!("Failed to decode Polkadot's MessagePayload: {:?}", e)),
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
+				type Source = Polkadot;
+				type Target = relay_kusama_client::Kusama;
+
+				sender.enforce_chain::<Source>();
+				let spec_version = Target::RUNTIME_VERSION.spec_version;
+				let origin = CallOrigin::SourceAccount(sender.raw_id());
+				encode_call::preprocess_call::<Source, Target>(
+					&mut call,
+					bridge::POLKADOT_TO_KUSAMA_INDEX,
+				);
+				let call = Target::encode_call(&call)?;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					Err(anyhow::format_err!(
+						"Please specify dispatch weight of the encoded Kusama call"
+					))
+				})?;
+
+				Ok(send_message::message_payload(
+					spec_version,
+					dispatch_weight,
+					origin,
+					&call,
+					DispatchFeePayment::AtSourceChain,
+				))
+			},
+		}
 	}
 }
-
-/// Storage key and initial value of Kusama -> Polkadot conversion rate.
-pub(crate) fn kusama_to_polkadot_conversion_rate_params() -> (StorageKey, FixedU128) {
-	(
-		bp_runtime::storage_parameter_key(
-			bp_polkadot::KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME,
-		),
-		// starting relay before this parameter will be set to some value may cause troubles
-		FixedU128::from_inner(FixedU128::DIV),
-	)
-}
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_headers_to_kusama.rs b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_headers_to_kusama.rs
index b1948b234cc31c54cf251ddb524590e195e9d051..6d118b07caa5b99e5efce03c7285da43366ec42c 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_headers_to_kusama.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_headers_to_kusama.rs
@@ -16,95 +16,52 @@
 
 //! Polkadot-to-Kusama headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_kusama_client::{Kusama, SigningParams as KusamaSigningParams};
-use relay_polkadot_client::{Polkadot, SyncHeader as PolkadotSyncHeader};
-use relay_substrate_client::{Client, TransactionSignScheme, UnsignedTransaction};
-use relay_utils::metrics::MetricsParams;
-use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
-};
+use async_trait::async_trait;
+use relay_kusama_client::Kusama;
+use substrate_relay_helper::{finality_pipeline::SubstrateFinalitySyncPipeline, TransactionParams};
 
 /// Maximal saturating difference between `balance(now)` and `balance(now-24h)` to treat
 /// relay as gone wild.
 ///
 /// Actual value, returned by `maximal_balance_decrease_per_day_is_sane` test is approximately 0.001
-/// KSM, but let's round up to 0.1 KSM here.
-pub(crate) const MAXIMAL_BALANCE_DECREASE_PER_DAY: bp_polkadot::Balance = 100_000_000_000;
-
-/// Polkadot-to-Kusama finality sync pipeline.
-pub(crate) type FinalityPipelinePolkadotFinalityToKusama =
-	SubstrateFinalityToSubstrate<Polkadot, Kusama, KusamaSigningParams>;
-
+/// KSM, and initial value of this constant was rounded up to 0.1 KSM. But for actual Kusama <>
+/// Polkadot deployment we'll be using the same account for delivering finality (free for mandatory
+/// headers) and messages. It means that we can't predict maximal loss. But to protect funds against
+/// relay/deployment issues, let's limit it so something that is much larger than this estimation -
+/// e.g. to 2 KSM.
+// TODO: https://github.com/paritytech/parity-bridges-common/issues/1307
+pub(crate) const MAXIMAL_BALANCE_DECREASE_PER_DAY: bp_kusama::Balance = 2 * 1_000_000_000_000;
+
+/// Description of Polkadot -> Kusama finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct PolkadotFinalityToKusama {
-	finality_pipeline: FinalityPipelinePolkadotFinalityToKusama,
-}
-
-impl PolkadotFinalityToKusama {
-	pub fn new(target_client: Client<Kusama>, target_sign: KusamaSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelinePolkadotFinalityToKusama::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
-
+pub struct PolkadotFinalityToKusama;
+substrate_relay_helper::generate_mocked_submit_finality_proof_call_builder!(
+	PolkadotFinalityToKusama,
+	PolkadotFinalityToKusamaCallBuilder,
+	relay_kusama_client::runtime::Call::BridgePolkadotGrandpa,
+	relay_kusama_client::runtime::BridgePolkadotGrandpaCall::submit_finality_proof
+);
+
+#[async_trait]
 impl SubstrateFinalitySyncPipeline for PolkadotFinalityToKusama {
-	type FinalitySyncPipeline = FinalityPipelinePolkadotFinalityToKusama;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_polkadot::BEST_FINALIZED_POLKADOT_HEADER_METHOD;
-
+	type SourceChain = relay_polkadot_client::Polkadot;
 	type TargetChain = Kusama;
 
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
-	}
-
-	fn start_relay_guards(&self) {
-		relay_substrate_client::guard::abort_on_spec_version_change(
-			self.finality_pipeline.target_client.clone(),
-			bp_kusama::VERSION.spec_version,
-		);
-		relay_substrate_client::guard::abort_when_account_balance_decreased(
-			self.finality_pipeline.target_client.clone(),
-			self.transactions_author(),
+	type SubmitFinalityProofCallBuilder = PolkadotFinalityToKusamaCallBuilder;
+	type TransactionSignScheme = Kusama;
+
+	async fn start_relay_guards(
+		target_client: &relay_substrate_client::Client<Kusama>,
+		transaction_params: &TransactionParams<sp_core::sr25519::Pair>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		substrate_relay_helper::finality_guards::start::<Kusama, Kusama>(
+			target_client,
+			transaction_params,
+			enable_version_guard,
 			MAXIMAL_BALANCE_DECREASE_PER_DAY,
-		);
-	}
-
-	fn transactions_author(&self) -> bp_kusama::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Kusama>,
-		transaction_nonce: bp_runtime::IndexOf<Kusama>,
-		header: PolkadotSyncHeader,
-		proof: GrandpaJustification<bp_polkadot::Header>,
-	) -> Bytes {
-		let call = relay_kusama_client::runtime::Call::BridgePolkadotGrandpa(
-			relay_kusama_client::runtime::BridgePolkadotGrandpaCall::submit_finality_proof(
-				Box::new(header.into_inner()),
-				proof,
-			),
-		);
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Kusama::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
+		)
+		.await
 	}
 }
 
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_messages_to_kusama.rs b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_messages_to_kusama.rs
index bc7f222430922997e08032448780cc4bef36bec9..9c4a4640eb99d3d6d6e74d1b52907828296f0b4f 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_messages_to_kusama.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/polkadot_messages_to_kusama.rs
@@ -16,315 +16,63 @@
 
 //! Polkadot-to-Kusama messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
 use frame_support::weights::Weight;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_kusama_client::{
-	HeaderId as KusamaHeaderId, Kusama, SigningParams as KusamaSigningParams,
-};
-use relay_polkadot_client::{
-	HeaderId as PolkadotHeaderId, Polkadot, SigningParams as PolkadotSigningParams,
-};
-use relay_substrate_client::{Chain, Client, TransactionSignScheme, UnsignedTransaction};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
-
-/// Polkadot-to-Kusama message lane.
-pub type MessageLanePolkadotMessagesToKusama =
-	SubstrateMessageLaneToSubstrate<Polkadot, PolkadotSigningParams, Kusama, KusamaSigningParams>;
-
-#[derive(Clone)]
-pub struct PolkadotMessagesToKusama {
-	message_lane: MessageLanePolkadotMessagesToKusama,
-}
+use messages_relay::relay_strategy::MixStrategy;
+use relay_kusama_client::Kusama;
+use relay_polkadot_client::Polkadot;
+use substrate_relay_helper::messages_lane::SubstrateMessageLane;
+
+/// Description of Polkadot -> Kusama messages bridge.
+#[derive(Clone, Debug)]
+pub struct PolkadotMessagesToKusama;
+substrate_relay_helper::generate_mocked_receive_message_proof_call_builder!(
+	PolkadotMessagesToKusama,
+	PolkadotMessagesToKusamaReceiveMessagesProofCallBuilder,
+	relay_kusama_client::runtime::Call::BridgePolkadotMessages,
+	relay_kusama_client::runtime::BridgePolkadotMessagesCall::receive_messages_proof
+);
+substrate_relay_helper::generate_mocked_receive_message_delivery_proof_call_builder!(
+	PolkadotMessagesToKusama,
+	PolkadotMessagesToKusamaReceiveMessagesDeliveryProofCallBuilder,
+	relay_polkadot_client::runtime::Call::BridgeKusamaMessages,
+	relay_polkadot_client::runtime::BridgeKusamaMessagesCall::receive_messages_delivery_proof
+);
+substrate_relay_helper::generate_mocked_update_conversion_rate_call_builder!(
+	Polkadot,
+	PolkadotMessagesToKusamaUpdateConversionRateCallBuilder,
+	relay_polkadot_client::runtime::Call::BridgeKusamaMessages,
+	relay_polkadot_client::runtime::BridgeKusamaMessagesCall::update_pallet_parameter,
+	relay_polkadot_client::runtime::BridgeKusamaMessagesParameter::KusamaToPolkadotConversionRate
+);
 
 impl SubstrateMessageLane for PolkadotMessagesToKusama {
-	type MessageLane = MessageLanePolkadotMessagesToKusama;
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_kusama::TO_KUSAMA_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_kusama::TO_KUSAMA_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_kusama::TO_KUSAMA_LATEST_RECEIVED_NONCE_METHOD;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_kusama::POLKADOT_TO_KUSAMA_CONVERSION_RATE_PARAMETER_NAME);
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_polkadot::KUSAMA_TO_POLKADOT_CONVERSION_RATE_PARAMETER_NAME);
 
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_polkadot::FROM_POLKADOT_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_polkadot::FROM_POLKADOT_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_polkadot::FROM_POLKADOT_UNREWARDED_RELAYERS_STATE;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_kusama::POLKADOT_FEE_MULTIPLIER_PARAMETER_NAME);
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_polkadot::KUSAMA_FEE_MULTIPLIER_PARAMETER_NAME);
 
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_polkadot::BEST_FINALIZED_POLKADOT_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_kusama::BEST_FINALIZED_KUSAMA_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str =
-		bp_polkadot::WITH_KUSAMA_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str =
-		bp_kusama::WITH_POLKADOT_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_kusama::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> =
+		Some(bp_polkadot::TRANSACTION_PAYMENT_PALLET_NAME);
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> =
+		Some(bp_kusama::TRANSACTION_PAYMENT_PALLET_NAME);
 
 	type SourceChain = Polkadot;
 	type TargetChain = Kusama;
 
-	fn source_transactions_author(&self) -> bp_polkadot::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: PolkadotHeaderId,
-		transaction_nonce: bp_runtime::IndexOf<Polkadot>,
-		_generated_at_block: KusamaHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call = relay_polkadot_client::runtime::Call::BridgeKusamaMessages(
-			relay_polkadot_client::runtime::BridgeKusamaMessagesCall::receive_messages_delivery_proof(
-				proof,
-				relayers_state,
-			),
-		);
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Polkadot::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Kusama -> Polkadot confirmation transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_polkadot::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_polkadot::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-
-	fn target_transactions_author(&self) -> bp_kusama::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: KusamaHeaderId,
-		transaction_nonce: bp_runtime::IndexOf<Kusama>,
-		_generated_at_header: PolkadotHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
+	type SourceTransactionSignScheme = Polkadot;
+	type TargetTransactionSignScheme = Kusama;
 
-		let call = relay_kusama_client::runtime::Call::BridgePolkadotMessages(
-			relay_kusama_client::runtime::BridgePolkadotMessagesCall::receive_messages_proof(
-				self.message_lane.relayer_id_at_source.clone(),
-				proof,
-				messages_count as _,
-				dispatch_weight,
-			),
-		);
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Kusama::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Polkadot -> Kusama delivery transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_kusama::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_kusama::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Polkadot node as messages source.
-type PolkadotSourceClient = SubstrateMessagesSource<PolkadotMessagesToKusama>;
-
-/// Kusama node as messages target.
-type KusamaTargetClient = SubstrateMessagesTarget<PolkadotMessagesToKusama>;
-
-/// Run Polkadot-to-Kusama messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Polkadot,
-		PolkadotSigningParams,
-		Kusama,
-		KusamaSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Polkadot::AVERAGE_BLOCK_INTERVAL,
-		Kusama::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_polkadot = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = PolkadotMessagesToKusama {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_polkadot,
-		},
-	};
+	type ReceiveMessagesProofCallBuilder = PolkadotMessagesToKusamaReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		PolkadotMessagesToKusamaReceiveMessagesDeliveryProofCallBuilder;
 
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_kusama::max_extrinsic_size() / 3;
-	// we don't know exact weights of the Kusama runtime. So to guess weights we'll be using
-	// weights from Rialto and then simply dividing it by x2.
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>,
-		>(
-			bp_kusama::max_extrinsic_weight(),
-			bp_kusama::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-	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);
-
-	log::info!(
-		target: "bridge",
-		"Starting Polkadot -> Kusama messages relay.\n\t\
-			Polkadot relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Polkadot::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Kusama::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_kusama::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_kusama::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		PolkadotSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		KusamaTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
-
-/// Create standalone metrics for the Polkadot -> Kusama messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Polkadot>,
-	target_client: Client<Kusama>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Polkadot, Kusama>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		Some(crate::chains::polkadot::TOKEN_ID),
-		Some(crate::chains::kusama::TOKEN_ID),
-		Some(crate::chains::kusama::polkadot_to_kusama_conversion_rate_params()),
-		Some(crate::chains::polkadot::kusama_to_polkadot_conversion_rate_params()),
-	)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder =
+		PolkadotMessagesToKusamaUpdateConversionRateCallBuilder;
 
-/// Update Kusama -> Polkadot conversion rate, stored in Polkadot runtime storage.
-pub(crate) async fn update_kusama_to_polkadot_conversion_rate(
-	client: Client<Polkadot>,
-	signer: <Polkadot as TransactionSignScheme>::AccountKeyPair,
-	updated_rate: f64,
-) -> anyhow::Result<()> {
-	let genesis_hash = *client.genesis_hash();
-	let signer_id = (*signer.public().as_array_ref()).into();
-	client
-		.submit_signed_extrinsic(signer_id, move |_, transaction_nonce| {
-			Bytes(
-				Polkadot::sign_transaction(
-					genesis_hash,
-					&signer,
-					relay_substrate_client::TransactionEra::immortal(),
-					UnsignedTransaction::new(
-						relay_polkadot_client::runtime::Call::BridgeKusamaMessages(
-							relay_polkadot_client::runtime::BridgeKusamaMessagesCall::update_pallet_parameter(
-								relay_polkadot_client::runtime::BridgeKusamaMessagesParameter::KusamaToPolkadotConversionRate(
-									sp_runtime::FixedU128::from_float(updated_rate),
-								)
-							)
-						),
-						transaction_nonce,
-					),
-				)
-				.encode(),
-			)
-		})
-		.await
-		.map(drop)
-		.map_err(|err| anyhow::format_err!("{:?}", err))
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rialto.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rialto.rs
index 2d873a24ba7af3090d499c547ee21f4d1348ef41..8f26a64a4e326f6d3cf6647c8e465d223f10bb93 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rialto.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rialto.rs
@@ -25,37 +25,27 @@ use crate::cli::{
 };
 use anyhow::anyhow;
 use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
-use frame_support::weights::{DispatchInfo, GetDispatchInfo, Weight};
+use frame_support::weights::{DispatchInfo, GetDispatchInfo};
 use relay_rialto_client::Rialto;
-use sp_core::storage::StorageKey;
-use sp_runtime::FixedU128;
 use sp_version::RuntimeVersion;
 
-// Millau/Rialto tokens have no any real value, so the conversion rate we use is always 1:1. But we
-// want to test our code that is intended to work with real-value chains. So to keep it close to
-// 1:1, we'll be treating Rialto as BTC and Millau as wBTC (only in relayer).
-
-/// The identifier of token, which value is associated with Rialto token value by relayer.
-pub(crate) const ASSOCIATED_TOKEN_ID: &str = crate::chains::polkadot::TOKEN_ID;
-
 impl CliEncodeCall for Rialto {
-	fn max_extrinsic_size() -> u32 {
-		bp_rialto::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
-			Call::Raw { data } => Decode::decode(&mut &*data.0)?,
+			Call::Raw { data } => Self::Call::decode(&mut &*data.0)?.into(),
 			Call::Remark { remark_payload, .. } =>
 				rialto_runtime::Call::System(rialto_runtime::SystemCall::remark {
 					remark: remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
-				}),
+				})
+				.into(),
 			Call::Transfer { recipient, amount } =>
 				rialto_runtime::Call::Balances(rialto_runtime::BalancesCall::transfer {
 					dest: recipient.raw_id().into(),
 					value: amount.0,
-				}),
+				})
+				.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::RIALTO_TO_MILLAU_INDEX => {
@@ -67,6 +57,7 @@ impl CliEncodeCall for Rialto {
 								delivery_and_dispatch_fee: fee.0,
 							},
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -76,8 +67,8 @@ impl CliEncodeCall for Rialto {
 		})
 	}
 
-	fn get_dispatch_info(call: &rialto_runtime::Call) -> anyhow::Result<DispatchInfo> {
-		Ok(call.get_dispatch_info())
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
+		Ok(call.to_decoded()?.get_dispatch_info())
 	}
 }
 
@@ -96,17 +87,13 @@ impl CliChain for Rialto {
 		rialto_runtime::SS58Prefix::get() as u16
 	}
 
-	fn max_extrinsic_weight() -> Weight {
-		bp_rialto::max_extrinsic_weight()
-	}
-
 	fn encode_message(
 		message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
 		match message {
 			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
 				.map_err(|e| anyhow!("Failed to decode Rialto's MessagePayload: {:?}", e)),
-			encode_message::MessagePayload::Call { mut call, mut sender } => {
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
 				type Source = Rialto;
 				type Target = relay_millau_client::Millau;
 
@@ -118,11 +105,13 @@ impl CliChain for Rialto {
 					bridge::RIALTO_TO_MILLAU_INDEX,
 				);
 				let call = Target::encode_call(&call)?;
-				let weight = call.get_dispatch_info().weight;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					call.to_decoded().map(|call| call.get_dispatch_info().weight)
+				})?;
 
 				Ok(send_message::message_payload(
 					spec_version,
-					weight,
+					dispatch_weight,
 					origin,
 					&call,
 					DispatchFeePayment::AtSourceChain,
@@ -131,11 +120,3 @@ impl CliChain for Rialto {
 		}
 	}
 }
-
-/// Storage key and initial value of Millau -> Rialto conversion rate.
-pub(crate) fn millau_to_rialto_conversion_rate_params() -> (StorageKey, FixedU128) {
-	(
-		StorageKey(rialto_runtime::millau_messages::MillauToRialtoConversionRate::key().to_vec()),
-		rialto_runtime::millau_messages::INITIAL_MILLAU_TO_RIALTO_CONVERSION_RATE,
-	)
-}
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_headers_to_millau.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_headers_to_millau.rs
index 7e76f403c55aae1a9e26cb91b48e220b5874b6d3..a433f3562a70316f47adab6a84564dceebeb7aec 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_headers_to_millau.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_headers_to_millau.rs
@@ -16,73 +16,22 @@
 
 //! Rialto-to-Millau headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_millau_client::{Millau, SigningParams as MillauSigningParams};
-use relay_rialto_client::{Rialto, SyncHeader as RialtoSyncHeader};
-use relay_substrate_client::{Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
 use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
+	DirectSubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
 };
 
-/// Rialto-to-Millau finality sync pipeline.
-pub(crate) type FinalityPipelineRialtoFinalityToMillau =
-	SubstrateFinalityToSubstrate<Rialto, Millau, MillauSigningParams>;
-
+/// Description of Millau -> Rialto finalized headers bridge.
 #[derive(Clone, Debug)]
-pub struct RialtoFinalityToMillau {
-	finality_pipeline: FinalityPipelineRialtoFinalityToMillau,
-}
-
-impl RialtoFinalityToMillau {
-	pub fn new(target_client: Client<Millau>, target_sign: MillauSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelineRialtoFinalityToMillau::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
+pub struct RialtoFinalityToMillau;
 
 impl SubstrateFinalitySyncPipeline for RialtoFinalityToMillau {
-	type FinalitySyncPipeline = FinalityPipelineRialtoFinalityToMillau;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_rialto::BEST_FINALIZED_RIALTO_HEADER_METHOD;
-
-	type TargetChain = Millau;
-
-	fn transactions_author(&self) -> bp_millau::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Millau>,
-		transaction_nonce: IndexOf<Millau>,
-		header: RialtoSyncHeader,
-		proof: GrandpaJustification<bp_rialto::Header>,
-	) -> Bytes {
-		let call = millau_runtime::BridgeGrandpaCall::<
-			millau_runtime::Runtime,
-			millau_runtime::RialtoGrandpaInstance,
-		>::submit_finality_proof {
-			finality_target: Box::new(header.into_inner()),
-			justification: proof,
-		}
-		.into();
-
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Millau::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
-	}
+	type SourceChain = relay_rialto_client::Rialto;
+	type TargetChain = relay_millau_client::Millau;
+
+	type SubmitFinalityProofCallBuilder = DirectSubmitFinalityProofCallBuilder<
+		Self,
+		millau_runtime::Runtime,
+		millau_runtime::RialtoGrandpaInstance,
+	>;
+	type TransactionSignScheme = relay_millau_client::Millau;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs
index 774da017df0c1263a965ccaebf350ceef5b77d2a..d34f47146449253bb10b1c3821ff5226b4525de9 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_messages_to_millau.rs
@@ -16,309 +16,55 @@
 
 //! Rialto-to-Millau messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
-use frame_support::dispatch::GetDispatchInfo;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
-use frame_support::weights::Weight;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_millau_client::{
-	HeaderId as MillauHeaderId, Millau, SigningParams as MillauSigningParams,
-};
-use relay_rialto_client::{
-	HeaderId as RialtoHeaderId, Rialto, SigningParams as RialtoSigningParams,
+use messages_relay::relay_strategy::MixStrategy;
+use relay_millau_client::Millau;
+use relay_rialto_client::Rialto;
+use substrate_relay_helper::messages_lane::{
+	DirectReceiveMessagesDeliveryProofCallBuilder, DirectReceiveMessagesProofCallBuilder,
+	SubstrateMessageLane,
 };
-use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
-
-/// Rialto-to-Millau message lane.
-pub type MessageLaneRialtoMessagesToMillau =
-	SubstrateMessageLaneToSubstrate<Rialto, RialtoSigningParams, Millau, MillauSigningParams>;
 
-#[derive(Clone)]
-pub struct RialtoMessagesToMillau {
-	message_lane: MessageLaneRialtoMessagesToMillau,
-}
+/// Description of Rialto -> Millau messages bridge.
+#[derive(Clone, Debug)]
+pub struct RialtoMessagesToMillau;
+substrate_relay_helper::generate_direct_update_conversion_rate_call_builder!(
+	Rialto,
+	RialtoMessagesToMillauUpdateConversionRateCallBuilder,
+	rialto_runtime::Runtime,
+	rialto_runtime::WithMillauMessagesInstance,
+	rialto_runtime::millau_messages::RialtoToMillauMessagesParameter::MillauToRialtoConversionRate
+);
 
 impl SubstrateMessageLane for RialtoMessagesToMillau {
-	type MessageLane = MessageLaneRialtoMessagesToMillau;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_millau::RIALTO_TO_MILLAU_CONVERSION_RATE_PARAMETER_NAME);
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> =
+		Some(bp_rialto::MILLAU_TO_RIALTO_CONVERSION_RATE_PARAMETER_NAME);
 
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_millau::TO_MILLAU_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_millau::TO_MILLAU_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_millau::TO_MILLAU_LATEST_RECEIVED_NONCE_METHOD;
-
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_rialto::FROM_RIALTO_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_rialto::FROM_RIALTO_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_rialto::FROM_RIALTO_UNREWARDED_RELAYERS_STATE;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_rialto::BEST_FINALIZED_RIALTO_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_millau::BEST_FINALIZED_MILLAU_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str = bp_rialto::WITH_MILLAU_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str = bp_millau::WITH_RIALTO_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_millau::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
 
 	type SourceChain = Rialto;
 	type TargetChain = Millau;
 
-	fn source_transactions_author(&self) -> bp_rialto::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: RialtoHeaderId,
-		transaction_nonce: IndexOf<Rialto>,
-		_generated_at_block: MillauHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call: rialto_runtime::Call =
-			rialto_runtime::MessagesCall::receive_messages_delivery_proof { proof, relayers_state }
-				.into();
-		let call_weight = call.get_dispatch_info().weight;
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Rialto::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Millau -> Rialto confirmation transaction. Weight: {}/{}, size: {}/{}",
-			call_weight,
-			bp_rialto::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_rialto::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
+	type SourceTransactionSignScheme = Rialto;
+	type TargetTransactionSignScheme = Millau;
 
-	fn target_transactions_author(&self) -> bp_millau::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
+	type ReceiveMessagesProofCallBuilder = DirectReceiveMessagesProofCallBuilder<
+		Self,
+		millau_runtime::Runtime,
+		millau_runtime::WithRialtoMessagesInstance,
+	>;
+	type ReceiveMessagesDeliveryProofCallBuilder = DirectReceiveMessagesDeliveryProofCallBuilder<
+		Self,
+		rialto_runtime::Runtime,
+		rialto_runtime::WithMillauMessagesInstance,
+	>;
 
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: MillauHeaderId,
-		transaction_nonce: IndexOf<Millau>,
-		_generated_at_header: RialtoHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
-		let call: millau_runtime::Call = millau_runtime::MessagesCall::receive_messages_proof {
-			relayer_id_at_bridged_chain: self.message_lane.relayer_id_at_source.clone(),
-			proof,
-			messages_count: messages_count as _,
-			dispatch_weight,
-		}
-		.into();
-		let call_weight = call.get_dispatch_info().weight;
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Millau::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Rialto -> Millau delivery transaction. Weight: {}/{}, size: {}/{}",
-			call_weight,
-			bp_millau::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_millau::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Rialto node as messages source.
-type RialtoSourceClient = SubstrateMessagesSource<RialtoMessagesToMillau>;
-
-/// Millau node as messages target.
-type MillauTargetClient = SubstrateMessagesTarget<RialtoMessagesToMillau>;
-
-/// Run Rialto-to-Millau messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Rialto,
-		RialtoSigningParams,
-		Millau,
-		MillauSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Rialto::AVERAGE_BLOCK_INTERVAL,
-		Millau::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_rialto = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = RialtoMessagesToMillau {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_rialto,
-		},
-	};
-
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_millau::max_extrinsic_size() / 3;
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>,
-		>(
-			bp_millau::max_extrinsic_weight(),
-			bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-
-	log::info!(
-		target: "bridge",
-		"Starting Rialto -> Millau messages relay.\n\t\
-			Rialto relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Rialto::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Millau::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_millau::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		RialtoSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		MillauTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
-
-/// Create standalone metrics for the Rialto -> Millau messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Rialto>,
-	target_client: Client<Millau>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Rialto, Millau>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		Some(crate::chains::rialto::ASSOCIATED_TOKEN_ID),
-		Some(crate::chains::millau::ASSOCIATED_TOKEN_ID),
-		Some(crate::chains::millau::rialto_to_millau_conversion_rate_params()),
-		Some(crate::chains::rialto::millau_to_rialto_conversion_rate_params()),
-	)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder =
+		RialtoMessagesToMillauUpdateConversionRateCallBuilder;
 
-/// Update Millau -> Rialto conversion rate, stored in Rialto runtime storage.
-pub(crate) async fn update_millau_to_rialto_conversion_rate(
-	client: Client<Rialto>,
-	signer: <Rialto as TransactionSignScheme>::AccountKeyPair,
-	updated_rate: f64,
-) -> anyhow::Result<()> {
-	let genesis_hash = *client.genesis_hash();
-	let signer_id = (*signer.public().as_array_ref()).into();
-	client
-		.submit_signed_extrinsic(signer_id, move |_, transaction_nonce| {
-			Bytes(
-				Rialto::sign_transaction(
-					genesis_hash,
-					&signer,
-					relay_substrate_client::TransactionEra::immortal(),
-					UnsignedTransaction::new(
-						rialto_runtime::MessagesCall::update_pallet_parameter {
-							parameter: rialto_runtime::millau_messages::RialtoToMillauMessagesParameter::MillauToRialtoConversionRate(
-								sp_runtime::FixedU128::from_float(updated_rate),
-							),
-						}
-						.into(),
-						transaction_nonce,
-					),
-				)
-				.encode(),
-			)
-		})
-		.await
-		.map(drop)
-		.map_err(|err| anyhow::format_err!("{:?}", err))
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
index edd4ca36285406c9b69b8b7d34793f4ae30ea456..0ed39faa543b7f103b67232f082f577f7d16e816 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
@@ -21,37 +21,37 @@ use crate::cli::{
 	encode_message, CliChain,
 };
 use bp_message_dispatch::MessagePayload;
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
-use frame_support::weights::{DispatchInfo, GetDispatchInfo, Weight};
+use frame_support::weights::{DispatchInfo, GetDispatchInfo};
 use relay_rialto_parachain_client::RialtoParachain;
 use sp_version::RuntimeVersion;
 
 impl CliEncodeCall for RialtoParachain {
-	fn max_extrinsic_size() -> u32 {
-		bp_rialto_parachain::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
-			Call::Raw { data } => Decode::decode(&mut &*data.0)?,
+			Call::Raw { data } => Self::Call::decode(&mut &*data.0)?.into(),
 			Call::Remark { remark_payload, .. } => rialto_parachain_runtime::Call::System(
 				rialto_parachain_runtime::SystemCall::remark {
 					remark: remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
 				},
-			),
+			)
+			.into(),
 			Call::Transfer { recipient, amount } => rialto_parachain_runtime::Call::Balances(
 				rialto_parachain_runtime::BalancesCall::transfer {
 					dest: recipient.raw_id().into(),
 					value: amount.0,
 				},
-			),
-			Call::BridgeSendMessage { .. } =>
-				anyhow::bail!("Bridge messages are not (yet) supported here",),
+			)
+			.into(),
+			Call::BridgeSendMessage { .. } => {
+				anyhow::bail!("Bridge messages are not (yet) supported here",)
+			},
 		})
 	}
 
-	fn get_dispatch_info(call: &rialto_parachain_runtime::Call) -> anyhow::Result<DispatchInfo> {
-		Ok(call.get_dispatch_info())
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
+		Ok(call.to_decoded()?.get_dispatch_info())
 	}
 }
 
@@ -70,10 +70,6 @@ impl CliChain for RialtoParachain {
 		rialto_parachain_runtime::SS58Prefix::get() as u16
 	}
 
-	fn max_extrinsic_weight() -> Weight {
-		bp_rialto_parachain::max_extrinsic_weight()
-	}
-
 	fn encode_message(
 		_message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rococo.rs
index 4df60f89faa213679b60613f93279cba2100bb99..ceef4c1f532c9c5f14da4eb69c9b329a7586db15 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rococo.rs
@@ -15,6 +15,8 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use anyhow::anyhow;
+use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
 use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
 use relay_rococo_client::Rococo;
@@ -22,8 +24,10 @@ use sp_version::RuntimeVersion;
 
 use crate::cli::{
 	bridge,
-	encode_call::{Call, CliEncodeCall},
-	encode_message, CliChain,
+	encode_call::{self, Call, CliEncodeCall},
+	encode_message,
+	send_message::{self, DispatchFeePayment},
+	CliChain,
 };
 
 /// Weight of the `system::remark` call at Rococo.
@@ -33,26 +37,25 @@ use crate::cli::{
 pub(crate) const SYSTEM_REMARK_CALL_WEIGHT: Weight = 2 * 1_345_000;
 
 impl CliEncodeCall for Rococo {
-	fn max_extrinsic_size() -> u32 {
-		bp_rococo::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
+			Call::Raw { data } => EncodedOrDecodedCall::Encoded(data.0.clone()),
 			Call::Remark { remark_payload, .. } => relay_rococo_client::runtime::Call::System(
 				relay_rococo_client::runtime::SystemCall::remark(
 					remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
 				),
-			),
+			)
+			.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::ROCOCO_TO_WOCOCO_INDEX => {
 						let payload = Decode::decode(&mut &*payload.0)?;
-						relay_rococo_client::runtime::Call::BridgeMessagesWococo(
-							relay_rococo_client::runtime::BridgeMessagesWococoCall::send_message(
+						relay_rococo_client::runtime::Call::BridgeWococoMessages(
+							relay_rococo_client::runtime::BridgeWococoMessagesCall::send_message(
 								lane.0, payload, fee.0,
 							),
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -63,13 +66,11 @@ impl CliEncodeCall for Rococo {
 		})
 	}
 
-	fn get_dispatch_info(
-		call: &relay_rococo_client::runtime::Call,
-	) -> anyhow::Result<DispatchInfo> {
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
 		match *call {
-			relay_rococo_client::runtime::Call::System(
+			EncodedOrDecodedCall::Decoded(relay_rococo_client::runtime::Call::System(
 				relay_rococo_client::runtime::SystemCall::remark(_),
-			) => Ok(DispatchInfo {
+			)) => Ok(DispatchInfo {
 				weight: SYSTEM_REMARK_CALL_WEIGHT,
 				class: DispatchClass::Normal,
 				pays_fee: Pays::Yes,
@@ -83,19 +84,49 @@ impl CliChain for Rococo {
 	const RUNTIME_VERSION: RuntimeVersion = bp_rococo::VERSION;
 
 	type KeyPair = sp_core::sr25519::Pair;
-	type MessagePayload = ();
+	type MessagePayload = MessagePayload<
+		bp_rococo::AccountId,
+		bp_wococo::AccountPublic,
+		bp_wococo::Signature,
+		Vec<u8>,
+	>;
 
 	fn ss58_format() -> u16 {
 		42
 	}
 
-	fn max_extrinsic_weight() -> Weight {
-		bp_wococo::max_extrinsic_weight()
-	}
-
 	fn encode_message(
-		_message: encode_message::MessagePayload,
+		message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
-		Err(anyhow!("Sending messages from Rococo is not yet supported."))
+		match message {
+			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
+				.map_err(|e| anyhow!("Failed to decode Rococo's MessagePayload: {:?}", e)),
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
+				type Source = Rococo;
+				type Target = relay_wococo_client::Wococo;
+
+				sender.enforce_chain::<Source>();
+				let spec_version = Target::RUNTIME_VERSION.spec_version;
+				let origin = CallOrigin::SourceAccount(sender.raw_id());
+				encode_call::preprocess_call::<Source, Target>(
+					&mut call,
+					bridge::ROCOCO_TO_WOCOCO_INDEX,
+				);
+				let call = Target::encode_call(&call)?;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					Err(anyhow::format_err!(
+						"Please specify dispatch weight of the encoded Wococo call"
+					))
+				})?;
+
+				Ok(send_message::message_payload(
+					spec_version,
+					dispatch_weight,
+					origin,
+					&call,
+					DispatchFeePayment::AtSourceChain,
+				))
+			},
+		}
 	}
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rococo_headers_to_wococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rococo_headers_to_wococo.rs
index ec98cec1ec1e9e4adf403466e73d71093c3b1933..bb66a7422d370ce5e40c4f852780fe707f199a33 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rococo_headers_to_wococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rococo_headers_to_wococo.rs
@@ -16,89 +16,41 @@
 
 //! Rococo-to-Wococo headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_rococo_client::{Rococo, SyncHeader as RococoSyncHeader};
-use relay_substrate_client::{Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use relay_utils::metrics::MetricsParams;
-use relay_wococo_client::{SigningParams as WococoSigningParams, Wococo};
-use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
-};
-
 use crate::chains::wococo_headers_to_rococo::MAXIMAL_BALANCE_DECREASE_PER_DAY;
 
-/// Rococo-to-Wococo finality sync pipeline.
-pub(crate) type FinalityPipelineRococoFinalityToWococo =
-	SubstrateFinalityToSubstrate<Rococo, Wococo, WococoSigningParams>;
+use async_trait::async_trait;
+use relay_wococo_client::Wococo;
+use substrate_relay_helper::{finality_pipeline::SubstrateFinalitySyncPipeline, TransactionParams};
 
+/// Description of Rococo -> Wococo finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct RococoFinalityToWococo {
-	finality_pipeline: FinalityPipelineRococoFinalityToWococo,
-}
-
-impl RococoFinalityToWococo {
-	pub fn new(target_client: Client<Wococo>, target_sign: WococoSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelineRococoFinalityToWococo::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
-
+pub struct RococoFinalityToWococo;
+substrate_relay_helper::generate_mocked_submit_finality_proof_call_builder!(
+	RococoFinalityToWococo,
+	RococoFinalityToWococoCallBuilder,
+	relay_wococo_client::runtime::Call::BridgeGrandpaRococo,
+	relay_wococo_client::runtime::BridgeGrandpaRococoCall::submit_finality_proof
+);
+
+#[async_trait]
 impl SubstrateFinalitySyncPipeline for RococoFinalityToWococo {
-	type FinalitySyncPipeline = FinalityPipelineRococoFinalityToWococo;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_rococo::BEST_FINALIZED_ROCOCO_HEADER_METHOD;
-
+	type SourceChain = relay_rococo_client::Rococo;
 	type TargetChain = Wococo;
 
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
-	}
-
-	fn start_relay_guards(&self) {
-		relay_substrate_client::guard::abort_on_spec_version_change(
-			self.finality_pipeline.target_client.clone(),
-			bp_wococo::VERSION.spec_version,
-		);
-		relay_substrate_client::guard::abort_when_account_balance_decreased(
-			self.finality_pipeline.target_client.clone(),
-			self.transactions_author(),
+	type SubmitFinalityProofCallBuilder = RococoFinalityToWococoCallBuilder;
+	type TransactionSignScheme = Wococo;
+
+	async fn start_relay_guards(
+		target_client: &relay_substrate_client::Client<Wococo>,
+		transaction_params: &TransactionParams<sp_core::sr25519::Pair>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		substrate_relay_helper::finality_guards::start::<Wococo, Wococo>(
+			target_client,
+			transaction_params,
+			enable_version_guard,
 			MAXIMAL_BALANCE_DECREASE_PER_DAY,
-		);
-	}
-
-	fn transactions_author(&self) -> bp_wococo::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Wococo>,
-		transaction_nonce: IndexOf<Wococo>,
-		header: RococoSyncHeader,
-		proof: GrandpaJustification<bp_rococo::Header>,
-	) -> Bytes {
-		let call = relay_wococo_client::runtime::Call::BridgeGrandpaRococo(
-			relay_wococo_client::runtime::BridgeGrandpaRococoCall::submit_finality_proof(
-				Box::new(header.into_inner()),
-				proof,
-			),
-		);
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Wococo::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
+		)
+		.await
 	}
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/rococo_messages_to_wococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/rococo_messages_to_wococo.rs
index d6c9040e1277bc1238cc970b17cf7753c5d9c43b..4e67c87fa8c452bfb21f43c106bb682bf748180d 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/rococo_messages_to_wococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/rococo_messages_to_wococo.rs
@@ -16,280 +16,48 @@
 
 //! Rococo-to-Wococo messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
 use frame_support::weights::Weight;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_rococo_client::{
-	HeaderId as RococoHeaderId, Rococo, SigningParams as RococoSigningParams,
-};
-use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use relay_wococo_client::{
-	HeaderId as WococoHeaderId, SigningParams as WococoSigningParams, Wococo,
-};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
-
-/// Rococo-to-Wococo message lane.
-pub type MessageLaneRococoMessagesToWococo =
-	SubstrateMessageLaneToSubstrate<Rococo, RococoSigningParams, Wococo, WococoSigningParams>;
-
-#[derive(Clone)]
-pub struct RococoMessagesToWococo {
-	message_lane: MessageLaneRococoMessagesToWococo,
-}
+use messages_relay::relay_strategy::MixStrategy;
+use relay_rococo_client::Rococo;
+use relay_wococo_client::Wococo;
+use substrate_relay_helper::messages_lane::SubstrateMessageLane;
+
+/// Description of Rococo -> Wococo messages bridge.
+#[derive(Clone, Debug)]
+pub struct RococoMessagesToWococo;
+substrate_relay_helper::generate_mocked_receive_message_proof_call_builder!(
+	RococoMessagesToWococo,
+	RococoMessagesToWococoReceiveMessagesProofCallBuilder,
+	relay_wococo_client::runtime::Call::BridgeRococoMessages,
+	relay_wococo_client::runtime::BridgeRococoMessagesCall::receive_messages_proof
+);
+substrate_relay_helper::generate_mocked_receive_message_delivery_proof_call_builder!(
+	RococoMessagesToWococo,
+	RococoMessagesToWococoReceiveMessagesDeliveryProofCallBuilder,
+	relay_rococo_client::runtime::Call::BridgeWococoMessages,
+	relay_rococo_client::runtime::BridgeWococoMessagesCall::receive_messages_delivery_proof
+);
 
 impl SubstrateMessageLane for RococoMessagesToWococo {
-	type MessageLane = MessageLaneRococoMessagesToWococo;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> = None;
 
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_wococo::TO_WOCOCO_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_wococo::TO_WOCOCO_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_wococo::TO_WOCOCO_LATEST_RECEIVED_NONCE_METHOD;
-
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_rococo::FROM_ROCOCO_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_rococo::FROM_ROCOCO_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_rococo::FROM_ROCOCO_UNREWARDED_RELAYERS_STATE;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_rococo::BEST_FINALIZED_ROCOCO_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_wococo::BEST_FINALIZED_WOCOCO_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str = bp_rococo::WITH_WOCOCO_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str = bp_wococo::WITH_ROCOCO_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_wococo::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
 
 	type SourceChain = Rococo;
 	type TargetChain = Wococo;
 
-	fn source_transactions_author(&self) -> bp_rococo::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: RococoHeaderId,
-		transaction_nonce: IndexOf<Rococo>,
-		_generated_at_block: WococoHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call = relay_rococo_client::runtime::Call::BridgeMessagesWococo(
-			relay_rococo_client::runtime::BridgeMessagesWococoCall::receive_messages_delivery_proof(
-				proof,
-				relayers_state,
-			),
-		);
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Rococo::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Wococo -> Rococo confirmation transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_rococo::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_rococo::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
+	type SourceTransactionSignScheme = Rococo;
+	type TargetTransactionSignScheme = Wococo;
 
-	fn target_transactions_author(&self) -> bp_wococo::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
+	type ReceiveMessagesProofCallBuilder = RococoMessagesToWococoReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		RococoMessagesToWococoReceiveMessagesDeliveryProofCallBuilder;
 
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: WococoHeaderId,
-		transaction_nonce: IndexOf<Wococo>,
-		_generated_at_header: RococoHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
-
-		let call = relay_wococo_client::runtime::Call::BridgeMessagesRococo(
-			relay_wococo_client::runtime::BridgeMessagesRococoCall::receive_messages_proof(
-				self.message_lane.relayer_id_at_source.clone(),
-				proof,
-				messages_count as _,
-				dispatch_weight,
-			),
-		);
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Wococo::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Rococo -> Wococo delivery transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_wococo::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_wococo::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Rococo node as messages source.
-type RococoSourceClient = SubstrateMessagesSource<RococoMessagesToWococo>;
-
-/// Wococo node as messages target.
-type WococoTargetClient = SubstrateMessagesTarget<RococoMessagesToWococo>;
-
-/// Run Rococo-to-Wococo messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Rococo,
-		RococoSigningParams,
-		Wococo,
-		WococoSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Rococo::AVERAGE_BLOCK_INTERVAL,
-		Wococo::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_rococo = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = RococoMessagesToWococo {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_rococo,
-		},
-	};
-
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_wococo::max_extrinsic_size() / 3;
-	// we don't know exact weights of the Wococo runtime. So to guess weights we'll be using
-	// weights from Rialto and then simply dividing it by x2.
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>,
-		>(
-			bp_wococo::max_extrinsic_weight(),
-			bp_wococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-	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);
-
-	log::info!(
-		target: "bridge",
-		"Starting Rococo -> Wococo messages relay.\n\t\
-			Rococo relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Rococo::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Wococo::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_wococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_wococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		RococoSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		WococoTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder = ();
 
-/// Create standalone metrics for the Rococo -> Wococo messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Rococo>,
-	target_client: Client<Wococo>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Rococo, Wococo>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		None,
-		None,
-		None,
-		None,
-	)
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/westend.rs b/polkadot/bridges/relays/bin-substrate/src/chains/westend.rs
index a42e4805512ca326802e8d4ad8c519f760f55091..8d3b5db9ab37f02869b95b88d88d76807c5d63df 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/westend.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/westend.rs
@@ -18,7 +18,6 @@
 
 use crate::cli::{encode_message, CliChain};
 use anyhow::anyhow;
-use frame_support::weights::Weight;
 use relay_westend_client::Westend;
 use sp_version::RuntimeVersion;
 
@@ -29,11 +28,10 @@ impl CliChain for Westend {
 	type MessagePayload = ();
 
 	fn ss58_format() -> u16 {
-		42
-	}
-
-	fn max_extrinsic_weight() -> Weight {
-		0
+		sp_core::crypto::Ss58AddressFormat::from(
+			sp_core::crypto::Ss58AddressFormatRegistry::SubstrateAccount,
+		)
+		.into()
 	}
 
 	fn encode_message(
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/westend_headers_to_millau.rs b/polkadot/bridges/relays/bin-substrate/src/chains/westend_headers_to_millau.rs
index 211aa9da9bfe3af72ee86593b8230981e402a9b1..2ec20a027ff5fd6505b44c7923d1e3fb9c046a5c 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/westend_headers_to_millau.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/westend_headers_to_millau.rs
@@ -16,78 +16,22 @@
 
 //! Westend-to-Millau headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_millau_client::{Millau, SigningParams as MillauSigningParams};
-use relay_substrate_client::{Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use relay_utils::metrics::MetricsParams;
-use relay_westend_client::{SyncHeader as WestendSyncHeader, Westend};
 use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
+	DirectSubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
 };
 
-/// Westend-to-Millau finality sync pipeline.
-pub(crate) type FinalityPipelineWestendFinalityToMillau =
-	SubstrateFinalityToSubstrate<Westend, Millau, MillauSigningParams>;
-
+/// Description of Westend -> Millau finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct WestendFinalityToMillau {
-	finality_pipeline: FinalityPipelineWestendFinalityToMillau,
-}
-
-impl WestendFinalityToMillau {
-	pub fn new(target_client: Client<Millau>, target_sign: MillauSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelineWestendFinalityToMillau::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
+pub struct WestendFinalityToMillau;
 
 impl SubstrateFinalitySyncPipeline for WestendFinalityToMillau {
-	type FinalitySyncPipeline = FinalityPipelineWestendFinalityToMillau;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_westend::BEST_FINALIZED_WESTEND_HEADER_METHOD;
-
-	type TargetChain = Millau;
-
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
-	}
-
-	fn transactions_author(&self) -> bp_millau::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Millau>,
-		transaction_nonce: IndexOf<Millau>,
-		header: WestendSyncHeader,
-		proof: GrandpaJustification<bp_westend::Header>,
-	) -> Bytes {
-		let call = millau_runtime::BridgeGrandpaCall::<
-			millau_runtime::Runtime,
-			millau_runtime::WestendGrandpaInstance,
-		>::submit_finality_proof {
-			finality_target: Box::new(header.into_inner()),
-			justification: proof,
-		}
-		.into();
-
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Millau::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
-	}
+	type SourceChain = relay_westend_client::Westend;
+	type TargetChain = relay_millau_client::Millau;
+
+	type SubmitFinalityProofCallBuilder = DirectSubmitFinalityProofCallBuilder<
+		Self,
+		millau_runtime::Runtime,
+		millau_runtime::WestendGrandpaInstance,
+	>;
+	type TransactionSignScheme = relay_millau_client::Millau;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/wococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/wococo.rs
index 328397d14ba7c8cc771f8be21df1ba9cf03f7767..46dec2a3c90e31a852f2a6e7ad534540e074edfb 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/wococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/wococo.rs
@@ -15,38 +15,41 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use anyhow::anyhow;
+use bp_message_dispatch::{CallOrigin, MessagePayload};
+use bp_runtime::EncodedOrDecodedCall;
 use codec::Decode;
-use frame_support::weights::{DispatchClass, DispatchInfo, Pays, Weight};
+use frame_support::weights::{DispatchClass, DispatchInfo, Pays};
 use relay_wococo_client::Wococo;
 use sp_version::RuntimeVersion;
 
 use crate::cli::{
 	bridge,
-	encode_call::{Call, CliEncodeCall},
-	encode_message, CliChain,
+	encode_call::{self, Call, CliEncodeCall},
+	encode_message,
+	send_message::{self, DispatchFeePayment},
+	CliChain,
 };
 
 impl CliEncodeCall for Wococo {
-	fn max_extrinsic_size() -> u32 {
-		bp_wococo::max_extrinsic_size()
-	}
-
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call> {
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
 		Ok(match call {
+			Call::Raw { data } => EncodedOrDecodedCall::Encoded(data.0.clone()),
 			Call::Remark { remark_payload, .. } => relay_wococo_client::runtime::Call::System(
 				relay_wococo_client::runtime::SystemCall::remark(
 					remark_payload.as_ref().map(|x| x.0.clone()).unwrap_or_default(),
 				),
-			),
+			)
+			.into(),
 			Call::BridgeSendMessage { lane, payload, fee, bridge_instance_index } =>
 				match *bridge_instance_index {
 					bridge::WOCOCO_TO_ROCOCO_INDEX => {
 						let payload = Decode::decode(&mut &*payload.0)?;
-						relay_wococo_client::runtime::Call::BridgeMessagesRococo(
-							relay_wococo_client::runtime::BridgeMessagesRococoCall::send_message(
+						relay_wococo_client::runtime::Call::BridgeRococoMessages(
+							relay_wococo_client::runtime::BridgeRococoMessagesCall::send_message(
 								lane.0, payload, fee.0,
 							),
 						)
+						.into()
 					},
 					_ => anyhow::bail!(
 						"Unsupported target bridge pallet with instance index: {}",
@@ -57,18 +60,16 @@ impl CliEncodeCall for Wococo {
 		})
 	}
 
-	fn get_dispatch_info(
-		call: &relay_wococo_client::runtime::Call,
-	) -> anyhow::Result<DispatchInfo> {
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo> {
 		match *call {
-			relay_wococo_client::runtime::Call::System(
+			EncodedOrDecodedCall::Decoded(relay_wococo_client::runtime::Call::System(
 				relay_wococo_client::runtime::SystemCall::remark(_),
-			) => Ok(DispatchInfo {
+			)) => Ok(DispatchInfo {
 				weight: crate::chains::rococo::SYSTEM_REMARK_CALL_WEIGHT,
 				class: DispatchClass::Normal,
 				pays_fee: Pays::Yes,
 			}),
-			_ => anyhow::bail!("Unsupported Rococo call: {:?}", call),
+			_ => anyhow::bail!("Unsupported Wococo call: {:?}", call),
 		}
 	}
 }
@@ -77,19 +78,49 @@ impl CliChain for Wococo {
 	const RUNTIME_VERSION: RuntimeVersion = bp_wococo::VERSION;
 
 	type KeyPair = sp_core::sr25519::Pair;
-	type MessagePayload = ();
+	type MessagePayload = MessagePayload<
+		bp_wococo::AccountId,
+		bp_rococo::AccountPublic,
+		bp_rococo::Signature,
+		Vec<u8>,
+	>;
 
 	fn ss58_format() -> u16 {
 		42
 	}
 
-	fn max_extrinsic_weight() -> Weight {
-		bp_wococo::max_extrinsic_weight()
-	}
-
 	fn encode_message(
-		_message: encode_message::MessagePayload,
+		message: encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload> {
-		Err(anyhow!("Sending messages from Wococo is not yet supported."))
+		match message {
+			encode_message::MessagePayload::Raw { data } => MessagePayload::decode(&mut &*data.0)
+				.map_err(|e| anyhow!("Failed to decode Wococo's MessagePayload: {:?}", e)),
+			encode_message::MessagePayload::Call { mut call, mut sender, dispatch_weight } => {
+				type Source = Wococo;
+				type Target = relay_rococo_client::Rococo;
+
+				sender.enforce_chain::<Source>();
+				let spec_version = Target::RUNTIME_VERSION.spec_version;
+				let origin = CallOrigin::SourceAccount(sender.raw_id());
+				encode_call::preprocess_call::<Source, Target>(
+					&mut call,
+					bridge::WOCOCO_TO_ROCOCO_INDEX,
+				);
+				let call = Target::encode_call(&call)?;
+				let dispatch_weight = dispatch_weight.map(Ok).unwrap_or_else(|| {
+					Err(anyhow::format_err!(
+						"Please specify dispatch weight of the encoded Rococo call"
+					))
+				})?;
+
+				Ok(send_message::message_payload(
+					spec_version,
+					dispatch_weight,
+					origin,
+					&call,
+					DispatchFeePayment::AtSourceChain,
+				))
+			},
+		}
 	}
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/wococo_headers_to_rococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/wococo_headers_to_rococo.rs
index fe17976d06a86d53e143413e20ce29cf6c93cb2a..a7bff5951882e40f6e59c078d42bee70d7ba0c25 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/wococo_headers_to_rococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/wococo_headers_to_rococo.rs
@@ -16,17 +16,9 @@
 
 //! Wococo-to-Rococo headers sync entrypoint.
 
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_header_chain::justification::GrandpaJustification;
-use relay_rococo_client::{Rococo, SigningParams as RococoSigningParams};
-use relay_substrate_client::{Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use relay_utils::metrics::MetricsParams;
-use relay_wococo_client::{SyncHeader as WococoSyncHeader, Wococo};
-use substrate_relay_helper::finality_pipeline::{
-	SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate,
-};
+use async_trait::async_trait;
+use relay_rococo_client::Rococo;
+use substrate_relay_helper::{finality_pipeline::SubstrateFinalitySyncPipeline, TransactionParams};
 
 /// Maximal saturating difference between `balance(now)` and `balance(now-24h)` to treat
 /// relay as gone wild.
@@ -35,76 +27,36 @@ use substrate_relay_helper::finality_pipeline::{
 /// Note that this is in plancks, so this corresponds to `1500 UNITS`.
 pub(crate) const MAXIMAL_BALANCE_DECREASE_PER_DAY: bp_rococo::Balance = 1_500_000_000_000_000;
 
-/// Wococo-to-Rococo finality sync pipeline.
-pub(crate) type FinalityPipelineWococoFinalityToRococo =
-	SubstrateFinalityToSubstrate<Wococo, Rococo, RococoSigningParams>;
-
+/// Description of Wococo -> Rococo finalized headers bridge.
 #[derive(Clone, Debug)]
-pub(crate) struct WococoFinalityToRococo {
-	finality_pipeline: FinalityPipelineWococoFinalityToRococo,
-}
-
-impl WococoFinalityToRococo {
-	pub fn new(target_client: Client<Rococo>, target_sign: RococoSigningParams) -> Self {
-		Self {
-			finality_pipeline: FinalityPipelineWococoFinalityToRococo::new(
-				target_client,
-				target_sign,
-			),
-		}
-	}
-}
-
+pub struct WococoFinalityToRococo;
+substrate_relay_helper::generate_mocked_submit_finality_proof_call_builder!(
+	WococoFinalityToRococo,
+	WococoFinalityToRococoCallBuilder,
+	relay_rococo_client::runtime::Call::BridgeGrandpaWococo,
+	relay_rococo_client::runtime::BridgeGrandpaWococoCall::submit_finality_proof
+);
+
+#[async_trait]
 impl SubstrateFinalitySyncPipeline for WococoFinalityToRococo {
-	type FinalitySyncPipeline = FinalityPipelineWococoFinalityToRococo;
-
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_wococo::BEST_FINALIZED_WOCOCO_HEADER_METHOD;
-
+	type SourceChain = relay_wococo_client::Wococo;
 	type TargetChain = Rococo;
 
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		crate::chains::add_polkadot_kusama_price_metrics::<Self::FinalitySyncPipeline>(params)
-	}
-
-	fn start_relay_guards(&self) {
-		relay_substrate_client::guard::abort_on_spec_version_change(
-			self.finality_pipeline.target_client.clone(),
-			bp_rococo::VERSION.spec_version,
-		);
-		relay_substrate_client::guard::abort_when_account_balance_decreased(
-			self.finality_pipeline.target_client.clone(),
-			self.transactions_author(),
+	type SubmitFinalityProofCallBuilder = WococoFinalityToRococoCallBuilder;
+	type TransactionSignScheme = Rococo;
+
+	async fn start_relay_guards(
+		target_client: &relay_substrate_client::Client<Rococo>,
+		transaction_params: &TransactionParams<sp_core::sr25519::Pair>,
+		enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		substrate_relay_helper::finality_guards::start::<Rococo, Rococo>(
+			target_client,
+			transaction_params,
+			enable_version_guard,
 			MAXIMAL_BALANCE_DECREASE_PER_DAY,
-		);
-	}
-
-	fn transactions_author(&self) -> bp_rococo::AccountId {
-		(*self.finality_pipeline.target_sign.public().as_array_ref()).into()
-	}
-
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Rococo>,
-		transaction_nonce: IndexOf<Rococo>,
-		header: WococoSyncHeader,
-		proof: GrandpaJustification<bp_wococo::Header>,
-	) -> Bytes {
-		let call = relay_rococo_client::runtime::Call::BridgeGrandpaWococo(
-			relay_rococo_client::runtime::BridgeGrandpaWococoCall::submit_finality_proof(
-				Box::new(header.into_inner()),
-				proof,
-			),
-		);
-		let genesis_hash = *self.finality_pipeline.target_client.genesis_hash();
-		let transaction = Rococo::sign_transaction(
-			genesis_hash,
-			&self.finality_pipeline.target_sign,
-			era,
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-
-		Bytes(transaction.encode())
+		)
+		.await
 	}
 }
 
diff --git a/polkadot/bridges/relays/bin-substrate/src/chains/wococo_messages_to_rococo.rs b/polkadot/bridges/relays/bin-substrate/src/chains/wococo_messages_to_rococo.rs
index dcba89e43f05ce90302fa03b41afc40f6085cfb5..2c44803f2c06a5a4e3658d826c254cd8285cdad8 100644
--- a/polkadot/bridges/relays/bin-substrate/src/chains/wococo_messages_to_rococo.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/chains/wococo_messages_to_rococo.rs
@@ -16,279 +16,49 @@
 
 //! Wococo-to-Rococo messages sync entrypoint.
 
-use std::ops::RangeInclusive;
-
-use codec::Encode;
-use sp_core::{Bytes, Pair};
-
-use bp_messages::MessageNonce;
-use bridge_runtime_common::messages::target::FromBridgedChainMessagesProof;
 use frame_support::weights::Weight;
-use messages_relay::{message_lane::MessageLane, relay_strategy::MixStrategy};
-use relay_rococo_client::{
-	HeaderId as RococoHeaderId, Rococo, SigningParams as RococoSigningParams,
-};
-use relay_substrate_client::{Chain, Client, IndexOf, TransactionSignScheme, UnsignedTransaction};
-use relay_wococo_client::{
-	HeaderId as WococoHeaderId, SigningParams as WococoSigningParams, Wococo,
-};
-use substrate_relay_helper::{
-	messages_lane::{
-		select_delivery_transaction_limits, MessagesRelayParams, StandaloneMessagesMetrics,
-		SubstrateMessageLane, SubstrateMessageLaneToSubstrate,
-	},
-	messages_source::SubstrateMessagesSource,
-	messages_target::SubstrateMessagesTarget,
-	STALL_TIMEOUT,
-};
 
-/// Wococo-to-Rococo message lane.
-pub type MessageLaneWococoMessagesToRococo =
-	SubstrateMessageLaneToSubstrate<Wococo, WococoSigningParams, Rococo, RococoSigningParams>;
-
-#[derive(Clone)]
-pub struct WococoMessagesToRococo {
-	message_lane: MessageLaneWococoMessagesToRococo,
-}
+use messages_relay::relay_strategy::MixStrategy;
+use relay_rococo_client::Rococo;
+use relay_wococo_client::Wococo;
+use substrate_relay_helper::messages_lane::SubstrateMessageLane;
+
+/// Description of Wococo -> Rococo messages bridge.
+#[derive(Clone, Debug)]
+pub struct WococoMessagesToRococo;
+substrate_relay_helper::generate_mocked_receive_message_proof_call_builder!(
+	WococoMessagesToRococo,
+	WococoMessagesToRococoReceiveMessagesProofCallBuilder,
+	relay_rococo_client::runtime::Call::BridgeWococoMessages,
+	relay_rococo_client::runtime::BridgeWococoMessagesCall::receive_messages_proof
+);
+substrate_relay_helper::generate_mocked_receive_message_delivery_proof_call_builder!(
+	WococoMessagesToRococo,
+	WococoMessagesToRococoReceiveMessagesDeliveryProofCallBuilder,
+	relay_wococo_client::runtime::Call::BridgeRococoMessages,
+	relay_wococo_client::runtime::BridgeRococoMessagesCall::receive_messages_delivery_proof
+);
 
 impl SubstrateMessageLane for WococoMessagesToRococo {
-	type MessageLane = MessageLaneWococoMessagesToRococo;
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str =
-		bp_rococo::TO_ROCOCO_MESSAGE_DETAILS_METHOD;
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str =
-		bp_rococo::TO_ROCOCO_LATEST_GENERATED_NONCE_METHOD;
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_rococo::TO_ROCOCO_LATEST_RECEIVED_NONCE_METHOD;
-
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str =
-		bp_wococo::FROM_WOCOCO_LATEST_RECEIVED_NONCE_METHOD;
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str =
-		bp_wococo::FROM_WOCOCO_LATEST_CONFIRMED_NONCE_METHOD;
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str =
-		bp_wococo::FROM_WOCOCO_UNREWARDED_RELAYERS_STATE;
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str> = None;
 
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str =
-		bp_wococo::BEST_FINALIZED_WOCOCO_HEADER_METHOD;
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str =
-		bp_rococo::BEST_FINALIZED_ROCOCO_HEADER_METHOD;
-
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str = bp_wococo::WITH_ROCOCO_MESSAGES_PALLET_NAME;
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str = bp_rococo::WITH_WOCOCO_MESSAGES_PALLET_NAME;
-
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight =
-		bp_rococo::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str> = None;
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str> = None;
 
 	type SourceChain = Wococo;
 	type TargetChain = Rococo;
 
-	fn source_transactions_author(&self) -> bp_wococo::AccountId {
-		(*self.message_lane.source_sign.public().as_array_ref()).into()
-	}
-
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: WococoHeaderId,
-		transaction_nonce: IndexOf<Wococo>,
-		_generated_at_block: RococoHeaderId,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes {
-		let (relayers_state, proof) = proof;
-		let call = relay_wococo_client::runtime::Call::BridgeMessagesRococo(
-			relay_wococo_client::runtime::BridgeMessagesRococoCall::receive_messages_delivery_proof(
-				proof,
-				relayers_state,
-			),
-		);
-		let genesis_hash = *self.message_lane.source_client.genesis_hash();
-		let transaction = Wococo::sign_transaction(
-			genesis_hash,
-			&self.message_lane.source_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.source_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Rococo -> Wococo confirmation transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_wococo::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_wococo::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-
-	fn target_transactions_author(&self) -> bp_rococo::AccountId {
-		(*self.message_lane.target_sign.public().as_array_ref()).into()
-	}
+	type SourceTransactionSignScheme = Wococo;
+	type TargetTransactionSignScheme = Rococo;
 
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: WococoHeaderId,
-		transaction_nonce: IndexOf<Rococo>,
-		_generated_at_header: WococoHeaderId,
-		_nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes {
-		let (dispatch_weight, proof) = proof;
-		let FromBridgedChainMessagesProof { ref nonces_start, ref nonces_end, .. } = proof;
-		let messages_count = nonces_end - nonces_start + 1;
+	type ReceiveMessagesProofCallBuilder = WococoMessagesToRococoReceiveMessagesProofCallBuilder;
+	type ReceiveMessagesDeliveryProofCallBuilder =
+		WococoMessagesToRococoReceiveMessagesDeliveryProofCallBuilder;
 
-		let call = relay_rococo_client::runtime::Call::BridgeMessagesWococo(
-			relay_rococo_client::runtime::BridgeMessagesWococoCall::receive_messages_proof(
-				self.message_lane.relayer_id_at_source.clone(),
-				proof,
-				messages_count as _,
-				dispatch_weight,
-			),
-		);
-		let genesis_hash = *self.message_lane.target_client.genesis_hash();
-		let transaction = Rococo::sign_transaction(
-			genesis_hash,
-			&self.message_lane.target_sign,
-			relay_substrate_client::TransactionEra::new(
-				best_block_id,
-				self.message_lane.target_transactions_mortality,
-			),
-			UnsignedTransaction::new(call, transaction_nonce),
-		);
-		log::trace!(
-			target: "bridge",
-			"Prepared Wococo -> Rococo delivery transaction. Weight: <unknown>/{}, size: {}/{}",
-			bp_rococo::max_extrinsic_weight(),
-			transaction.encode().len(),
-			bp_rococo::max_extrinsic_size(),
-		);
-		Bytes(transaction.encode())
-	}
-}
-
-/// Wococo node as messages source.
-type WococoSourceClient = SubstrateMessagesSource<WococoMessagesToRococo>;
-
-/// Rococo node as messages target.
-type RococoTargetClient = SubstrateMessagesTarget<WococoMessagesToRococo>;
-
-/// Run Wococo-to-Rococo messages sync.
-pub async fn run(
-	params: MessagesRelayParams<
-		Wococo,
-		WococoSigningParams,
-		Rococo,
-		RococoSigningParams,
-		MixStrategy,
-	>,
-) -> anyhow::Result<()> {
-	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		Wococo::AVERAGE_BLOCK_INTERVAL,
-		Rococo::AVERAGE_BLOCK_INTERVAL,
-		STALL_TIMEOUT,
-	);
-	let relayer_id_at_wococo = (*params.source_sign.public().as_array_ref()).into();
-
-	let lane_id = params.lane_id;
-	let source_client = params.source_client;
-	let target_client = params.target_client;
-	let lane = WococoMessagesToRococo {
-		message_lane: SubstrateMessageLaneToSubstrate {
-			source_client: source_client.clone(),
-			source_sign: params.source_sign,
-			source_transactions_mortality: params.source_transactions_mortality,
-			target_client: target_client.clone(),
-			target_sign: params.target_sign,
-			target_transactions_mortality: params.target_transactions_mortality,
-			relayer_id_at_source: relayer_id_at_wococo,
-		},
-	};
-
-	// 2/3 is reserved for proofs and tx overhead
-	let max_messages_size_in_single_batch = bp_rococo::max_extrinsic_size() / 3;
-	// we don't know exact weights of the Rococo runtime. So to guess weights we'll be using
-	// weights from Rialto and then simply dividing it by x2.
-	let (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
-		select_delivery_transaction_limits::<
-			pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>,
-		>(
-			bp_rococo::max_extrinsic_weight(),
-			bp_rococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-		);
-	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);
-
-	log::info!(
-		target: "bridge",
-		"Starting Wococo -> Rococo messages relay.\n\t\
-			Wococo relayer account id: {:?}\n\t\
-			Max messages in single transaction: {}\n\t\
-			Max messages size in single transaction: {}\n\t\
-			Max messages weight in single transaction: {}\n\t\
-			Tx mortality: {:?}/{:?}\n\t\
-			Stall timeout: {:?}",
-		lane.message_lane.relayer_id_at_source,
-		max_messages_in_single_batch,
-		max_messages_size_in_single_batch,
-		max_messages_weight_in_single_batch,
-		params.source_transactions_mortality,
-		params.target_transactions_mortality,
-		stall_timeout,
-	);
-
-	let standalone_metrics = params
-		.standalone_metrics
-		.map(Ok)
-		.unwrap_or_else(|| standalone_metrics(source_client.clone(), target_client.clone()))?;
-	messages_relay::message_lane_loop::run(
-		messages_relay::message_lane_loop::Params {
-			lane: lane_id,
-			source_tick: Wococo::AVERAGE_BLOCK_INTERVAL,
-			target_tick: Rococo::AVERAGE_BLOCK_INTERVAL,
-			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
-			stall_timeout,
-			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
-				max_unrewarded_relayer_entries_at_target:
-					bp_rococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-				max_unconfirmed_nonces_at_target:
-					bp_rococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
-				max_messages_in_single_batch,
-				max_messages_weight_in_single_batch,
-				max_messages_size_in_single_batch,
-				relay_strategy: params.relay_strategy,
-			},
-		},
-		WococoSourceClient::new(
-			source_client.clone(),
-			lane.clone(),
-			lane_id,
-			params.target_to_source_headers_relay,
-		),
-		RococoTargetClient::new(
-			target_client,
-			lane,
-			lane_id,
-			standalone_metrics.clone(),
-			params.source_to_target_headers_relay,
-		),
-		standalone_metrics.register_and_spawn(params.metrics_params)?,
-		futures::future::pending(),
-	)
-	.await
-	.map_err(Into::into)
-}
+	type TargetToSourceChainConversionRateUpdateBuilder = ();
 
-/// Create standalone metrics for the Wococo -> Rococo messages loop.
-pub(crate) fn standalone_metrics(
-	source_client: Client<Wococo>,
-	target_client: Client<Rococo>,
-) -> anyhow::Result<StandaloneMessagesMetrics<Wococo, Rococo>> {
-	substrate_relay_helper::messages_lane::standalone_metrics(
-		source_client,
-		target_client,
-		None,
-		None,
-		None,
-		None,
-	)
+	type RelayStrategy = MixStrategy;
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/bridge.rs b/polkadot/bridges/relays/bin-substrate/src/cli/bridge.rs
index 1af6142c53eca9e1eff7768b8a6083b6c2685921..2eb836a84a753947b05a1fb0ad667c1788ad6388 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/bridge.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/bridge.rs
@@ -68,7 +68,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::millau_messages_to_rialto::run as relay_messages;
+				use crate::chains::millau_messages_to_rialto::MillauMessagesToRialto as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
@@ -90,7 +90,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::rialto_messages_to_millau::run as relay_messages;
+				use crate::chains::rialto_messages_to_millau::RialtoMessagesToMillau as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
@@ -113,7 +113,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::rococo_messages_to_wococo::run as relay_messages;
+				use crate::chains::rococo_messages_to_wococo::RococoMessagesToWococo as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
@@ -135,7 +135,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::wococo_messages_to_rococo::run as relay_messages;
+				use crate::chains::wococo_messages_to_rococo::WococoMessagesToRococo as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
@@ -157,7 +157,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::kusama_messages_to_polkadot::run as relay_messages;
+				use crate::chains::kusama_messages_to_polkadot::KusamaMessagesToPolkadot as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
@@ -179,7 +179,7 @@ macro_rules! select_full_bridge {
 
 				// Relay-messages
 				#[allow(unused_imports)]
-				use crate::chains::polkadot_messages_to_kusama::run as relay_messages;
+				use crate::chains::polkadot_messages_to_kusama::PolkadotMessagesToKusama as MessagesLane;
 
 				// Send-message / Estimate-fee
 				#[allow(unused_imports)]
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/encode_call.rs b/polkadot/bridges/relays/bin-substrate/src/cli/encode_call.rs
index e17854662e5c4696f3b3cfb62b2758d6e6e5348b..e288e2c13d6cd06ca84c790828f08e47bb1b7c87 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/encode_call.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/encode_call.rs
@@ -20,6 +20,7 @@ use crate::{
 	},
 	select_full_bridge,
 };
+use bp_runtime::EncodedOrDecodedCall;
 use frame_support::weights::DispatchInfo;
 use relay_substrate_client::Chain;
 use structopt::StructOpt;
@@ -84,14 +85,11 @@ pub enum Call {
 }
 
 pub trait CliEncodeCall: Chain {
-	/// Maximal size (in bytes) of any extrinsic (from the runtime).
-	fn max_extrinsic_size() -> u32;
-
 	/// Encode a CLI call.
-	fn encode_call(call: &Call) -> anyhow::Result<Self::Call>;
+	fn encode_call(call: &Call) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>>;
 
 	/// Get dispatch info for the call.
-	fn get_dispatch_info(call: &Self::Call) -> anyhow::Result<DispatchInfo>;
+	fn get_dispatch_info(call: &EncodedOrDecodedCall<Self::Call>) -> anyhow::Result<DispatchInfo>;
 }
 
 impl EncodeCall {
@@ -103,7 +101,10 @@ impl EncodeCall {
 			let encoded = HexBytes::encode(&call);
 
 			log::info!(target: "bridge", "Generated {} call: {:#?}", Source::NAME, call);
-			log::info!(target: "bridge", "Weight of {} call: {}", Source::NAME, Source::get_dispatch_info(&call)?.weight);
+			log::info!(target: "bridge", "Weight of {} call: {}", Source::NAME, Source::get_dispatch_info(&call)
+				.map(|dispatch_info| format!("{}", dispatch_info.weight))
+				.unwrap_or_else(|_| "<unknown>".to_string())
+			);
 			log::info!(target: "bridge", "Encoded {} call: {:?}", Source::NAME, encoded);
 
 			Ok(encoded)
@@ -311,8 +312,8 @@ mod tests {
 		);
 	}
 
-	#[test]
-	fn should_encode_bridge_send_message_call() {
+	#[async_std::test]
+	async fn should_encode_bridge_send_message_call() {
 		// given
 		let encode_message = SendMessage::from_iter(vec![
 			"send-message",
@@ -328,6 +329,7 @@ mod tests {
 			"remark",
 		])
 		.encode_payload()
+		.await
 		.unwrap();
 
 		let mut encode_call = EncodeCall::from_iter(vec![
@@ -345,7 +347,7 @@ mod tests {
 
 		// then
 		assert!(format!("{:?}", call_hex).starts_with(
-			"0x0f030000000001000000381409000000000001d43593c715fdd31c61141abd04a99fd6822c8558854cc\
+			"0x0f030000000001000000000000000000000001d43593c715fdd31c61141abd04a99fd6822c8558854cc\
 			de39a5684e7a56da27d01d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01"
 		))
 	}
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/encode_message.rs b/polkadot/bridges/relays/bin-substrate/src/cli/encode_message.rs
index 98e1269aa68e6002ba463503e2b960cdf14c4814..677fc29ef15313c3f50bb1b1d3b51b5ba0a9f278 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/encode_message.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/encode_message.rs
@@ -18,6 +18,7 @@ use crate::{
 	cli::{bridge::FullBridge, AccountId, CliChain, HexBytes},
 	select_full_bridge,
 };
+use frame_support::weights::Weight;
 use structopt::StructOpt;
 use strum::VariantNames;
 
@@ -37,6 +38,12 @@ pub enum MessagePayload {
 		/// SS58 encoded Source account that will send the payload.
 		#[structopt(long)]
 		sender: AccountId,
+		/// Weight of the call.
+		///
+		/// It must be specified if the chain runtime is not bundled with the relay, or if
+		/// you want to override bundled weight.
+		#[structopt(long)]
+		dispatch_weight: Option<Weight>,
 	},
 }
 
@@ -97,6 +104,8 @@ mod tests {
 			"call",
 			"--sender",
 			&sender,
+			"--dispatch-weight",
+			"42",
 			"remark",
 			"--remark-size",
 			"12",
@@ -106,6 +115,6 @@ mod tests {
 		let hex = encode_message.encode().unwrap();
 
 		// then
-		assert_eq!(format!("{:?}", hex), "0x0100000010f108000000000002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d003c000130000000000000000000000000");
+		assert_eq!(format!("{:?}", hex), "0x010000002a0000000000000002d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d003c000130000000000000000000000000");
 	}
 }
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/estimate_fee.rs b/polkadot/bridges/relays/bin-substrate/src/cli/estimate_fee.rs
index d063ce544cd243099711c9c0b5b048c5b8af90e7..bab625314e824547f1ed839604d1543f3b43d3dc 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/estimate_fee.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/estimate_fee.rs
@@ -15,17 +15,22 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use crate::{
-	cli::{bridge::FullBridge, Balance, CliChain, HexBytes, HexLaneId, SourceConnectionParams},
+	cli::{
+		bridge::FullBridge, relay_headers_and_messages::CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO,
+		Balance, CliChain, HexBytes, HexLaneId, SourceConnectionParams,
+	},
 	select_full_bridge,
 };
 use bp_runtime::BalanceOf;
 use codec::{Decode, Encode};
 use relay_substrate_client::Chain;
+use sp_runtime::FixedU128;
 use structopt::StructOpt;
 use strum::VariantNames;
+use substrate_relay_helper::helpers::tokens_conversion_rate_from_metrics;
 
 /// Estimate Delivery & Dispatch Fee command.
-#[derive(StructOpt, Debug, PartialEq, Eq)]
+#[derive(StructOpt, Debug, PartialEq)]
 pub struct EstimateFee {
 	/// A bridge instance to encode call for.
 	#[structopt(possible_values = FullBridge::VARIANTS, case_insensitive = true)]
@@ -35,15 +40,44 @@ pub struct EstimateFee {
 	/// Hex-encoded id of lane that will be delivering the message.
 	#[structopt(long, default_value = "00000000")]
 	lane: HexLaneId,
+	/// A way to override conversion rate between bridge tokens.
+	///
+	/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
+	/// your message won't be relayed.
+	#[structopt(long)]
+	conversion_rate_override: Option<ConversionRateOverride>,
 	/// Payload to send over the bridge.
 	#[structopt(flatten)]
 	payload: crate::cli::encode_message::MessagePayload,
 }
 
+/// A way to override conversion rate between bridge tokens.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ConversionRateOverride {
+	/// The actual conversion rate is computed in the same way how rate metric works.
+	Metric,
+	/// The actual conversion rate is specified explicitly.
+	Explicit(f64),
+}
+
+impl std::str::FromStr for ConversionRateOverride {
+	type Err = String;
+
+	fn from_str(s: &str) -> Result<Self, Self::Err> {
+		if s.to_lowercase() == "metric" {
+			return Ok(ConversionRateOverride::Metric)
+		}
+
+		f64::from_str(s)
+			.map(ConversionRateOverride::Explicit)
+			.map_err(|e| format!("Failed to parse '{:?}'. Expected 'metric' or explicit value", e))
+	}
+}
+
 impl EstimateFee {
 	/// Run the command.
 	pub async fn run(self) -> anyhow::Result<()> {
-		let Self { source, bridge, lane, payload } = self;
+		let Self { source, bridge, lane, conversion_rate_override, payload } = self;
 
 		select_full_bridge!(bridge, {
 			let source_client = source.to_client::<Source>().await?;
@@ -51,8 +85,9 @@ impl EstimateFee {
 			let payload =
 				Source::encode_message(payload).map_err(|e| anyhow::format_err!("{:?}", e))?;
 
-			let fee: BalanceOf<Source> = estimate_message_delivery_and_dispatch_fee(
+			let fee = estimate_message_delivery_and_dispatch_fee::<Source, Target, _>(
 				&source_client,
+				conversion_rate_override,
 				ESTIMATE_MESSAGE_FEE_METHOD,
 				lane,
 				payload,
@@ -66,16 +101,114 @@ impl EstimateFee {
 	}
 }
 
-pub(crate) async fn estimate_message_delivery_and_dispatch_fee<Fee: Decode, C: Chain, P: Encode>(
-	client: &relay_substrate_client::Client<C>,
+/// The caller may provide target to source tokens conversion rate override to use in fee
+/// computation.
+pub(crate) async fn estimate_message_delivery_and_dispatch_fee<
+	Source: Chain,
+	Target: Chain,
+	P: Clone + Encode,
+>(
+	client: &relay_substrate_client::Client<Source>,
+	conversion_rate_override: Option<ConversionRateOverride>,
+	estimate_fee_method: &str,
+	lane: bp_messages::LaneId,
+	payload: P,
+) -> anyhow::Result<BalanceOf<Source>> {
+	// actual conversion rate CAN be lesser than the rate stored in the runtime. So we may try to
+	// pay lesser fee for the message delivery. But in this case, message may be rejected by the
+	// lane. So we MUST use the larger of two fees - one computed with stored fee and the one
+	// computed with actual fee.
+
+	let conversion_rate_override =
+		match (conversion_rate_override, Source::TOKEN_ID, Target::TOKEN_ID) {
+			(Some(ConversionRateOverride::Explicit(v)), _, _) => {
+				let conversion_rate_override = FixedU128::from_float(v);
+				log::info!(
+					target: "bridge",
+					"{} -> {} conversion rate override: {:?} (explicit)",
+					Target::NAME,
+					Source::NAME,
+					conversion_rate_override.to_float(),
+				);
+				Some(conversion_rate_override)
+			},
+			(
+				Some(ConversionRateOverride::Metric),
+				Some(source_token_id),
+				Some(target_token_id),
+			) => {
+				let conversion_rate_override =
+					tokens_conversion_rate_from_metrics(target_token_id, source_token_id).await?;
+				// So we have current actual conversion rate and rate that is stored in the runtime.
+				// And we may simply choose the maximal of these. But what if right now there's
+				// rate update transaction on the way, that is updating rate to 10 seconds old
+				// actual rate, which is bigger than the current rate? Then our message will be
+				// rejected.
+				//
+				// So let's increase the actual rate by the same value that the conversion rate
+				// updater is using.
+				let increased_conversion_rate_override = FixedU128::from_float(
+					conversion_rate_override * (1.0 + CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO),
+				);
+				log::info!(
+					target: "bridge",
+					"{} -> {} conversion rate override: {} (value from metric - {})",
+					Target::NAME,
+					Source::NAME,
+					increased_conversion_rate_override.to_float(),
+					conversion_rate_override,
+				);
+				Some(increased_conversion_rate_override)
+			},
+			_ => None,
+		};
+
+	let without_override = do_estimate_message_delivery_and_dispatch_fee(
+		client,
+		estimate_fee_method,
+		lane,
+		payload.clone(),
+		None,
+	)
+	.await?;
+	let with_override = do_estimate_message_delivery_and_dispatch_fee(
+		client,
+		estimate_fee_method,
+		lane,
+		payload.clone(),
+		conversion_rate_override,
+	)
+	.await?;
+	let maximal_fee = std::cmp::max(without_override, with_override);
+
+	log::info!(
+		target: "bridge",
+		"Estimated message fee: {:?} = max of {:?} (without rate override) and {:?} (with override to {:?})",
+		maximal_fee,
+		without_override,
+		with_override,
+		conversion_rate_override,
+	);
+
+	Ok(maximal_fee)
+}
+
+/// Estimate message delivery and dispatch fee with given conversion rate override.
+async fn do_estimate_message_delivery_and_dispatch_fee<Source: Chain, P: Encode>(
+	client: &relay_substrate_client::Client<Source>,
 	estimate_fee_method: &str,
 	lane: bp_messages::LaneId,
 	payload: P,
-) -> anyhow::Result<Fee> {
+	conversion_rate_override: Option<FixedU128>,
+) -> anyhow::Result<BalanceOf<Source>> {
 	let encoded_response = client
-		.state_call(estimate_fee_method.into(), (lane, payload).encode().into(), None)
+		.state_call(
+			estimate_fee_method.into(),
+			(lane, payload, conversion_rate_override).encode().into(),
+			None,
+		)
 		.await?;
-	let decoded_response: Option<Fee> = Decode::decode(&mut &encoded_response.0[..])
+	let decoded_response: Option<BalanceOf<Source>> = Decode::decode(&mut &encoded_response.0[..])
 		.map_err(relay_substrate_client::Error::ResponseParseFailed)?;
 	let fee = decoded_response.ok_or_else(|| {
 		anyhow::format_err!("Unable to decode fee from: {:?}", HexBytes(encoded_response.to_vec()))
@@ -86,7 +219,7 @@ pub(crate) async fn estimate_message_delivery_and_dispatch_fee<Fee: Decode, C: C
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use crate::cli::encode_call;
+	use crate::cli::{encode_call, RuntimeVersionType, SourceRuntimeVersionParams};
 	use sp_core::crypto::Ss58Codec;
 
 	#[test]
@@ -100,9 +233,13 @@ mod tests {
 			"rialto-to-millau",
 			"--source-port",
 			"1234",
+			"--conversion-rate-override",
+			"42.5",
 			"call",
 			"--sender",
 			&alice,
+			"--dispatch-weight",
+			"42",
 			"remark",
 			"--remark-payload",
 			"1234",
@@ -114,17 +251,24 @@ mod tests {
 			EstimateFee {
 				bridge: FullBridge::RialtoToMillau,
 				lane: HexLaneId([0, 0, 0, 0]),
+				conversion_rate_override: Some(ConversionRateOverride::Explicit(42.5)),
 				source: SourceConnectionParams {
 					source_host: "127.0.0.1".into(),
 					source_port: 1234,
 					source_secure: false,
+					source_runtime_version: SourceRuntimeVersionParams {
+						source_version_mode: RuntimeVersionType::Bundle,
+						source_spec_version: None,
+						source_transaction_version: None,
+					}
 				},
 				payload: crate::cli::encode_message::MessagePayload::Call {
 					sender: alice.parse().unwrap(),
 					call: encode_call::Call::Remark {
 						remark_payload: Some(HexBytes(vec![0x12, 0x34])),
 						remark_size: None,
-					}
+					},
+					dispatch_weight: Some(42),
 				}
 			}
 		);
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/init_bridge.rs b/polkadot/bridges/relays/bin-substrate/src/cli/init_bridge.rs
index ffda0b1200884bac7924baddbb220af5c8e3ab72..a0129ce9baa469abf8290e769790d943d1609d0c 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/init_bridge.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/init_bridge.rs
@@ -18,7 +18,7 @@ use crate::cli::{SourceConnectionParams, TargetConnectionParams, TargetSigningPa
 use bp_header_chain::InitializationData;
 use bp_runtime::Chain as ChainBase;
 use codec::Encode;
-use relay_substrate_client::{Chain, TransactionSignScheme, UnsignedTransaction};
+use relay_substrate_client::{Chain, SignParam, TransactionSignScheme, UnsignedTransaction};
 use sp_core::{Bytes, Pair};
 use structopt::StructOpt;
 use strum::{EnumString, EnumVariantNames, VariantNames};
@@ -187,23 +187,27 @@ impl InitBridge {
 			let target_client = self.target.to_client::<Target>().await?;
 			let target_sign = self.target_sign.to_keypair::<Target>()?;
 
+			let (spec_version, transaction_version) =
+				target_client.simple_runtime_version().await?;
 			substrate_relay_helper::headers_initialize::initialize(
 				source_client,
 				target_client.clone(),
 				target_sign.public().into(),
 				move |transaction_nonce, initialization_data| {
-					Bytes(
-						Target::sign_transaction(
-							*target_client.genesis_hash(),
-							&target_sign,
-							relay_substrate_client::TransactionEra::immortal(),
-							UnsignedTransaction::new(
-								encode_init_bridge(initialization_data),
+					Ok(Bytes(
+						Target::sign_transaction(SignParam {
+							spec_version,
+							transaction_version,
+							genesis_hash: *target_client.genesis_hash(),
+							signer: target_sign,
+							era: relay_substrate_client::TransactionEra::immortal(),
+							unsigned: UnsignedTransaction::new(
+								encode_init_bridge(initialization_data).into(),
 								transaction_nonce,
 							),
-						)
+						})?
 						.encode(),
-					)
+					))
 				},
 			)
 			.await;
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/mod.rs b/polkadot/bridges/relays/bin-substrate/src/cli/mod.rs
index d98e8af0af084d297a4610fd2cbd72ca6593a5ff..9842f300d1c6b4b072953e19f5a06f3b39443ee0 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/mod.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/mod.rs
@@ -18,11 +18,13 @@
 
 use std::convert::TryInto;
 
-use bp_messages::LaneId;
 use codec::{Decode, Encode};
-use frame_support::weights::Weight;
+use relay_substrate_client::ChainRuntimeVersion;
 use sp_runtime::app_crypto::Ss58Codec;
 use structopt::{clap::arg_enum, StructOpt};
+use strum::{EnumString, EnumVariantNames};
+
+use bp_messages::LaneId;
 
 pub(crate) mod bridge;
 pub(crate) mod encode_call;
@@ -33,6 +35,7 @@ pub(crate) mod send_message;
 mod derive_account;
 mod init_bridge;
 mod register_parachain;
+mod reinit_bridge;
 mod relay_headers;
 mod relay_headers_and_messages;
 mod relay_messages;
@@ -69,6 +72,11 @@ pub enum Command {
 	///
 	/// Sends initialization transaction to bootstrap the bridge with current finalized block data.
 	InitBridge(init_bridge::InitBridge),
+	/// Reinitialize on-chain bridge pallet with current header data.
+	///
+	/// Sends all missing mandatory headers to bootstrap the bridge with current finalized block
+	/// data.
+	ReinitBridge(reinit_bridge::ReinitBridge),
 	/// Send custom message over the bridge.
 	///
 	/// Allows interacting with the bridge by sending messages over `Messages` component.
@@ -124,6 +132,7 @@ impl Command {
 			Self::RelayMessages(arg) => arg.run().await?,
 			Self::RelayHeadersAndMessages(arg) => arg.run().await?,
 			Self::InitBridge(arg) => arg.run().await?,
+			Self::ReinitBridge(arg) => arg.run().await?,
 			Self::SendMessage(arg) => arg.run().await?,
 			Self::EncodeCall(arg) => arg.run().await?,
 			Self::EncodeMessage(arg) => arg.run().await?,
@@ -238,7 +247,7 @@ impl AccountId {
 ///
 /// Used to abstract away CLI commands.
 pub trait CliChain: relay_substrate_client::Chain {
-	/// Chain's current version of the runtime.
+	/// Current version of the chain runtime, known to relay.
 	const RUNTIME_VERSION: sp_version::RuntimeVersion;
 
 	/// Crypto KeyPair type used to send messages.
@@ -258,9 +267,6 @@ pub trait CliChain: relay_substrate_client::Chain {
 	fn encode_message(
 		message: crate::cli::encode_message::MessagePayload,
 	) -> anyhow::Result<Self::MessagePayload>;
-
-	/// Maximal extrinsic weight (from the runtime).
-	fn max_extrinsic_weight() -> Weight;
 }
 
 /// Lane id.
@@ -368,6 +374,17 @@ where
 	}
 }
 
+#[doc = "Runtime version params."]
+#[derive(StructOpt, Debug, PartialEq, Eq, Clone, Copy, EnumString, EnumVariantNames)]
+pub enum RuntimeVersionType {
+	/// Auto query version from chain
+	Auto,
+	/// Custom `spec_version` and `transaction_version`
+	Custom,
+	/// Read version from bundle dependencies directly.
+	Bundle,
+}
+
 /// Create chain-specific set of configuration objects: connection parameters,
 /// signing parameters and bridge initialization parameters.
 #[macro_export]
@@ -381,11 +398,28 @@ macro_rules! declare_chain_options {
 				#[structopt(long, default_value = "127.0.0.1")]
 				pub [<$chain_prefix _host>]: String,
 				#[doc = "Connect to " $chain " node websocket server at given port."]
-				#[structopt(long)]
+				#[structopt(long, default_value = "9944")]
 				pub [<$chain_prefix _port>]: u16,
 				#[doc = "Use secure websocket connection."]
 				#[structopt(long)]
 				pub [<$chain_prefix _secure>]: bool,
+				#[doc = "Custom runtime version"]
+				#[structopt(flatten)]
+				pub [<$chain_prefix _runtime_version>]: [<$chain RuntimeVersionParams>],
+			}
+
+			#[doc = $chain " runtime version params."]
+			#[derive(StructOpt, Debug, PartialEq, Eq, Clone, Copy)]
+			pub struct [<$chain RuntimeVersionParams>] {
+				#[doc = "The type of runtime version for chain " $chain]
+				#[structopt(long, default_value = "Bundle")]
+				pub [<$chain_prefix _version_mode>]: RuntimeVersionType,
+				#[doc = "The custom sepc_version for chain " $chain]
+				#[structopt(long)]
+				pub [<$chain_prefix _spec_version>]: Option<u32>,
+				#[doc = "The custom transaction_version for chain " $chain]
+				#[structopt(long)]
+				pub [<$chain_prefix _transaction_version>]: Option<u32>,
 			}
 
 			#[doc = $chain " signing params."]
@@ -501,18 +535,82 @@ macro_rules! declare_chain_options {
 			}
 
 			impl [<$chain ConnectionParams>] {
+				/// Returns `true` if version guard can be started.
+				///
+				/// There's no reason to run version guard when version mode is set to `Auto`. It can
+				/// lead to relay shutdown when chain is upgraded, even though we have explicitly
+				/// said that we don't want to shutdown.
+				#[allow(dead_code)]
+				pub fn can_start_version_guard(&self) -> bool {
+					self.[<$chain_prefix _runtime_version>].[<$chain_prefix _version_mode>] != RuntimeVersionType::Auto
+				}
+
 				/// Convert connection params into Substrate client.
 				pub async fn to_client<Chain: CliChain>(
 					&self,
 				) -> anyhow::Result<relay_substrate_client::Client<Chain>> {
+					let chain_runtime_version = self
+						.[<$chain_prefix _runtime_version>]
+						.into_runtime_version(Some(Chain::RUNTIME_VERSION))?;
 					Ok(relay_substrate_client::Client::new(relay_substrate_client::ConnectionParams {
 						host: self.[<$chain_prefix _host>].clone(),
 						port: self.[<$chain_prefix _port>],
 						secure: self.[<$chain_prefix _secure>],
+						chain_runtime_version,
 					})
 					.await
 					)
 				}
+
+				/// Return selected `chain_spec` version.
+				///
+				/// This function only connects to the node if version mode is set to `Auto`.
+				#[allow(dead_code)]
+				pub async fn selected_chain_spec_version<Chain: CliChain>(
+					&self,
+				) -> anyhow::Result<u32> {
+					let chain_runtime_version = self
+						.[<$chain_prefix _runtime_version>]
+						.into_runtime_version(Some(Chain::RUNTIME_VERSION))?;
+					Ok(match chain_runtime_version {
+						ChainRuntimeVersion::Auto => self
+							.to_client::<Chain>()
+							.await?
+							.simple_runtime_version()
+							.await?
+							.0,
+						ChainRuntimeVersion::Custom(spec_version, _) => spec_version,
+					})
+				}
+			}
+
+			impl [<$chain RuntimeVersionParams>] {
+				/// Converts self into `ChainRuntimeVersion`.
+				pub fn into_runtime_version(
+					self,
+					bundle_runtime_version: Option<sp_version::RuntimeVersion>,
+				) -> anyhow::Result<ChainRuntimeVersion> {
+					Ok(match self.[<$chain_prefix _version_mode>] {
+						RuntimeVersionType::Auto => ChainRuntimeVersion::Auto,
+						RuntimeVersionType::Custom => {
+							let except_spec_version = self.[<$chain_prefix _spec_version>]
+								.ok_or_else(|| anyhow::Error::msg(format!("The {}-spec-version is required when choose custom mode", stringify!($chain_prefix))))?;
+							let except_transaction_version = self.[<$chain_prefix _transaction_version>]
+								.ok_or_else(|| anyhow::Error::msg(format!("The {}-transaction-version is required when choose custom mode", stringify!($chain_prefix))))?;
+							ChainRuntimeVersion::Custom(
+								except_spec_version,
+								except_transaction_version
+							)
+						},
+						RuntimeVersionType::Bundle => match bundle_runtime_version {
+							Some(runtime_version) => ChainRuntimeVersion::Custom(
+								runtime_version.spec_version,
+								runtime_version.transaction_version
+							),
+							None => ChainRuntimeVersion::Auto
+						},
+					})
+				}
 			}
 		}
 	};
@@ -525,9 +623,10 @@ declare_chain_options!(Parachain, parachain);
 
 #[cfg(test)]
 mod tests {
-	use sp_core::Pair;
 	use std::str::FromStr;
 
+	use sp_core::Pair;
+
 	use super::*;
 
 	#[test]
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/register_parachain.rs b/polkadot/bridges/relays/bin-substrate/src/cli/register_parachain.rs
index eae20ffb868a08215aed8056b17b945d77aa364d..c761a5dd1a6039f4ee61dc6edee5bfa01753aa0d 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/register_parachain.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/register_parachain.rs
@@ -20,6 +20,7 @@ use crate::cli::{
 };
 
 use codec::Encode;
+use frame_support::Twox64Concat;
 use num_traits::Zero;
 use polkadot_parachain::primitives::{
 	HeadData as ParaHeadData, Id as ParaId, ValidationCode as ParaValidationCode,
@@ -29,7 +30,7 @@ use polkadot_runtime_common::{
 };
 use polkadot_runtime_parachains::paras::ParaLifecycle;
 use relay_substrate_client::{
-	AccountIdOf, CallOf, Chain, Client, TransactionSignScheme, UnsignedTransaction,
+	AccountIdOf, CallOf, Chain, Client, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use rialto_runtime::SudoCall;
 use sp_core::{
@@ -116,23 +117,26 @@ impl RegisterParachain {
 			let reserve_parachain_id_call: CallOf<Relaychain> =
 				ParaRegistrarCall::reserve {}.into();
 			let reserve_parachain_signer = relay_sign.clone();
+			let (spec_version, transaction_version) = relay_client.simple_runtime_version().await?;
 			wait_until_transaction_is_finalized::<Relaychain>(
 				relay_client
 					.submit_and_watch_signed_extrinsic(
 						relay_sudo_account.clone(),
 						move |_, transaction_nonce| {
-							Bytes(
-								Relaychain::sign_transaction(
-									relay_genesis_hash,
-									&reserve_parachain_signer,
-									relay_substrate_client::TransactionEra::immortal(),
-									UnsignedTransaction::new(
-										reserve_parachain_id_call,
+							Ok(Bytes(
+								Relaychain::sign_transaction(SignParam {
+									spec_version,
+									transaction_version,
+									genesis_hash: relay_genesis_hash,
+									signer: reserve_parachain_signer,
+									era: relay_substrate_client::TransactionEra::immortal(),
+									unsigned: UnsignedTransaction::new(
+										reserve_parachain_id_call.into(),
 										transaction_nonce,
 									),
-								)
+								})?
 								.encode(),
-							)
+							))
 						},
 					)
 					.await?,
@@ -168,18 +172,20 @@ impl RegisterParachain {
 					.submit_and_watch_signed_extrinsic(
 						relay_sudo_account.clone(),
 						move |_, transaction_nonce| {
-							Bytes(
-								Relaychain::sign_transaction(
-									relay_genesis_hash,
-									&register_parathread_signer,
-									relay_substrate_client::TransactionEra::immortal(),
-									UnsignedTransaction::new(
-										register_parathread_call,
+							Ok(Bytes(
+								Relaychain::sign_transaction(SignParam {
+									spec_version,
+									transaction_version,
+									genesis_hash: relay_genesis_hash,
+									signer: register_parathread_signer,
+									era: relay_substrate_client::TransactionEra::immortal(),
+									unsigned: UnsignedTransaction::new(
+										register_parathread_call.into(),
 										transaction_nonce,
 									),
-								)
+								})?
 								.encode(),
-							)
+							))
 						},
 					)
 					.await?,
@@ -188,7 +194,7 @@ impl RegisterParachain {
 			log::info!(target: "bridge", "Registered parachain: {:?}. Waiting for onboarding", para_id);
 
 			// wait until parathread is onboarded
-			let para_state_key = bp_runtime::storage_map_final_key_twox64_concat(
+			let para_state_key = bp_runtime::storage_map_final_key::<Twox64Concat>(
 				PARAS_PALLET_NAME,
 				PARAS_LIFECYCLES_STORAGE_NAME,
 				&para_id.encode(),
@@ -228,15 +234,20 @@ impl RegisterParachain {
 			let force_lease_signer = relay_sign.clone();
 			relay_client
 				.submit_signed_extrinsic(relay_sudo_account.clone(), move |_, transaction_nonce| {
-					Bytes(
-						Relaychain::sign_transaction(
-							relay_genesis_hash,
-							&force_lease_signer,
-							relay_substrate_client::TransactionEra::immortal(),
-							UnsignedTransaction::new(force_lease_call, transaction_nonce),
-						)
+					Ok(Bytes(
+						Relaychain::sign_transaction(SignParam {
+							spec_version,
+							transaction_version,
+							genesis_hash: relay_genesis_hash,
+							signer: force_lease_signer,
+							era: relay_substrate_client::TransactionEra::immortal(),
+							unsigned: UnsignedTransaction::new(
+								force_lease_call.into(),
+								transaction_nonce,
+							),
+						})?
 						.encode(),
-					)
+					))
 				})
 				.await?;
 			log::info!(target: "bridge", "Registered parachain leases: {:?}. Waiting for onboarding", para_id);
@@ -292,6 +303,9 @@ async fn wait_para_state<Relaychain: Chain>(
 #[cfg(test)]
 mod tests {
 	use super::*;
+	use crate::cli::{
+		ParachainRuntimeVersionParams, RelaychainRuntimeVersionParams, RuntimeVersionType,
+	};
 
 	#[test]
 	fn register_rialto_parachain() {
@@ -327,6 +341,11 @@ mod tests {
 					relaychain_host: "127.0.0.1".into(),
 					relaychain_port: 9944,
 					relaychain_secure: false,
+					relaychain_runtime_version: RelaychainRuntimeVersionParams {
+						relaychain_version_mode: RuntimeVersionType::Bundle,
+						relaychain_spec_version: None,
+						relaychain_transaction_version: None,
+					}
 				},
 				relay_sign: RelaychainSigningParams {
 					relaychain_signer: Some("//Alice".into()),
@@ -339,6 +358,11 @@ mod tests {
 					parachain_host: "127.0.0.1".into(),
 					parachain_port: 11949,
 					parachain_secure: false,
+					parachain_runtime_version: ParachainRuntimeVersionParams {
+						parachain_version_mode: RuntimeVersionType::Bundle,
+						parachain_spec_version: None,
+						parachain_transaction_version: None,
+					}
 				},
 			}
 		);
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/reinit_bridge.rs b/polkadot/bridges/relays/bin-substrate/src/cli/reinit_bridge.rs
new file mode 100644
index 0000000000000000000000000000000000000000..89470872cb2eefee543c85b702cfd50a5243d485
--- /dev/null
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/reinit_bridge.rs
@@ -0,0 +1,553 @@
+// 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/>.
+
+use crate::{
+	chains::{
+		kusama_headers_to_polkadot::KusamaFinalityToPolkadot,
+		polkadot_headers_to_kusama::PolkadotFinalityToKusama,
+	},
+	cli::{
+		swap_tokens::wait_until_transaction_is_finalized, SourceConnectionParams,
+		TargetConnectionParams, TargetSigningParams,
+	},
+};
+use bp_header_chain::justification::GrandpaJustification;
+use bp_runtime::Chain;
+use codec::Encode;
+use finality_relay::{SourceClient, SourceHeader};
+use frame_support::weights::Weight;
+use num_traits::One;
+use pallet_bridge_grandpa::weights::WeightInfo;
+use relay_substrate_client::{
+	AccountIdOf, BlockNumberOf, Chain as _, Client, Error as SubstrateError, HeaderOf, SignParam,
+	SyncHeader, TransactionEra, TransactionSignScheme, UnsignedTransaction,
+};
+use sp_core::{Bytes, Pair};
+use std::convert::{TryFrom, TryInto};
+use structopt::StructOpt;
+use strum::{EnumString, EnumVariantNames, VariantNames};
+use substrate_relay_helper::{
+	finality_pipeline::SubstrateFinalitySyncPipeline, finality_source::SubstrateFinalitySource,
+	finality_target::SubstrateFinalityTarget, messages_source::read_client_state,
+	TransactionParams,
+};
+
+/// Reinitialize bridge pallet.
+#[derive(Debug, PartialEq, StructOpt)]
+pub struct ReinitBridge {
+	/// A bridge instance to reinitialize.
+	#[structopt(possible_values = ReinitBridgeName::VARIANTS, case_insensitive = true)]
+	bridge: ReinitBridgeName,
+	#[structopt(flatten)]
+	source: SourceConnectionParams,
+	#[structopt(flatten)]
+	target: TargetConnectionParams,
+	#[structopt(flatten)]
+	target_sign: TargetSigningParams,
+}
+
+#[derive(Debug, EnumString, EnumVariantNames, PartialEq)]
+#[strum(serialize_all = "kebab_case")]
+/// Bridge to initialize.
+pub enum ReinitBridgeName {
+	KusamaToPolkadot,
+	PolkadotToKusama,
+}
+
+macro_rules! select_bridge {
+	($bridge: expr, $generic: tt) => {
+		match $bridge {
+			ReinitBridgeName::KusamaToPolkadot => {
+				use relay_polkadot_client::runtime;
+
+				type Finality = KusamaFinalityToPolkadot;
+				type Call = runtime::Call;
+
+				fn submit_finality_proof_call(
+					header_and_proof: HeaderAndProof<Finality>,
+				) -> runtime::Call {
+					runtime::Call::BridgeKusamaGrandpa(
+						runtime::BridgeKusamaGrandpaCall::submit_finality_proof(
+							Box::new(header_and_proof.0.into_inner()),
+							header_and_proof.1,
+						),
+					)
+				}
+
+				fn set_pallet_operation_mode_call(operational: bool) -> runtime::Call {
+					runtime::Call::BridgeKusamaGrandpa(
+						runtime::BridgeKusamaGrandpaCall::set_operational(operational),
+					)
+				}
+
+				fn batch_all_call(calls: Vec<Call>) -> runtime::Call {
+					runtime::Call::Utility(runtime::UtilityCall::batch_all(calls))
+				}
+
+				$generic
+			},
+			ReinitBridgeName::PolkadotToKusama => {
+				use relay_kusama_client::runtime;
+
+				type Finality = PolkadotFinalityToKusama;
+				type Call = runtime::Call;
+
+				fn submit_finality_proof_call(
+					header_and_proof: HeaderAndProof<Finality>,
+				) -> runtime::Call {
+					runtime::Call::BridgePolkadotGrandpa(
+						runtime::BridgePolkadotGrandpaCall::submit_finality_proof(
+							Box::new(header_and_proof.0.into_inner()),
+							header_and_proof.1,
+						),
+					)
+				}
+
+				fn set_pallet_operation_mode_call(operational: bool) -> runtime::Call {
+					runtime::Call::BridgePolkadotGrandpa(
+						runtime::BridgePolkadotGrandpaCall::set_operational(operational),
+					)
+				}
+
+				fn batch_all_call(calls: Vec<Call>) -> runtime::Call {
+					runtime::Call::Utility(runtime::UtilityCall::batch_all(calls))
+				}
+
+				$generic
+			},
+		}
+	};
+}
+
+impl ReinitBridge {
+	/// Run the command.
+	pub async fn run(self) -> anyhow::Result<()> {
+		select_bridge!(self.bridge, {
+			type Source = <Finality as SubstrateFinalitySyncPipeline>::SourceChain;
+			type Target = <Finality as SubstrateFinalitySyncPipeline>::TargetChain;
+
+			let source_client = self.source.to_client::<Source>().await?;
+			let target_client = self.target.to_client::<Target>().await?;
+			let target_sign = self.target_sign.to_keypair::<Target>()?;
+			let transaction_params = TransactionParams {
+				signer: target_sign,
+				mortality: self.target_sign.target_transactions_mortality,
+			};
+
+			let finality_source =
+				SubstrateFinalitySource::<Finality>::new(source_client.clone(), None);
+			let finality_target = SubstrateFinalityTarget::<Finality>::new(
+				target_client.clone(),
+				transaction_params.clone(),
+			);
+
+			// this subcommand assumes that the pallet at the target chain is halted
+			ensure_pallet_operating_mode(&finality_target, false).await?;
+
+			// we can't call `finality_target.best_finalized_source_block_id()`, because pallet is
+			// halted and the call will fail => just use what it uses internally
+			let current_number =
+				best_source_block_number_at_target::<Finality>(&target_client).await?;
+			let target_number = finality_source.best_finalized_block_number().await?;
+			log::info!(
+				target: "bridge",
+				"Best finalized {} header: at {}: {}, at {}: {}",
+				Source::NAME,
+				Source::NAME,
+				target_number,
+				Target::NAME,
+				current_number,
+			);
+
+			// prepare list of mandatory headers from the range `(current_number; target_number]`
+			let headers_to_submit = find_mandatory_headers_in_range(
+				&finality_source,
+				(current_number + 1, target_number),
+			)
+			.await?;
+			let latest_andatory_header_number = headers_to_submit.last().map(|(h, _)| h.number());
+			log::info!(
+				target: "bridge",
+				"Missing {} mandatory {} headers at {}",
+				headers_to_submit.len(),
+				Source::NAME,
+				Target::NAME,
+			);
+
+			// split all mandatory headers into batches
+			let headers_batches =
+				make_mandatory_headers_batches::<Finality, _>(headers_to_submit, |(_, proof)| {
+					// we don't have an access to the Kusama/Polkadot chain runtimes here, so we'll
+					// be using Millau weights. It isn't super-critical, unless real weights are
+					// magnitude higher or so
+					pallet_bridge_grandpa::weights::MillauWeight::<millau_runtime::Runtime>::submit_finality_proof(
+						proof.commit.precommits.len().try_into().unwrap_or(u32::MAX),
+						proof.votes_ancestries.len().try_into().unwrap_or(u32::MAX),
+					)
+				});
+			log::info!(
+				target: "bridge",
+				"We're going to submit {} transactions to {} node",
+				headers_batches.len(),
+				Target::NAME,
+			);
+
+			// each batch is submitted as a separate transaction
+			let signer_account_id: AccountIdOf<Target> = transaction_params.signer.public().into();
+			let genesis_hash = *target_client.genesis_hash();
+			let (spec_version, transaction_version) =
+				target_client.simple_runtime_version().await?;
+			let last_batch_index = headers_batches.len() - 1;
+			for (i, headers_batch) in headers_batches.into_iter().enumerate() {
+				let is_last_batch = i == last_batch_index;
+				let expected_number =
+					headers_batch.last().expect("all batches are non-empty").0.number();
+				let transaction_params = transaction_params.clone();
+				log::info!(
+					target: "bridge",
+					"Going to submit transaction that updates best {} header at {} to {}",
+					Source::NAME,
+					Target::NAME,
+					expected_number,
+				);
+
+				// prepare `batch_all` call
+				let mut batch_calls = Vec::with_capacity(headers_batch.len() + 2);
+				// the first call is always resumes pallet operation
+				batch_calls.push(set_pallet_operation_mode_call(true));
+				// followed by submit-finality-proofs calls
+				for header_and_proof in headers_batch {
+					batch_calls.push(submit_finality_proof_call(header_and_proof));
+				}
+				// if it isn't the last batch, we shall halt pallet again
+				if !is_last_batch {
+					batch_calls.push(set_pallet_operation_mode_call(false));
+				}
+				let submit_batch_call = batch_all_call(batch_calls);
+
+				let batch_transaction_events = target_client
+					.submit_and_watch_signed_extrinsic(
+						signer_account_id.clone(),
+						move |best_block_id, transaction_nonce| {
+							Ok(Bytes(
+								Target::sign_transaction(SignParam {
+									spec_version,
+									transaction_version,
+									genesis_hash,
+									signer: transaction_params.signer.clone(),
+									era: TransactionEra::new(
+										best_block_id,
+										transaction_params.mortality,
+									),
+									unsigned: UnsignedTransaction::new(
+										submit_batch_call.into(),
+										transaction_nonce,
+									),
+								})?
+								.encode(),
+							))
+						},
+					)
+					.await?;
+				wait_until_transaction_is_finalized::<Target>(batch_transaction_events).await?;
+
+				// verify that the best finalized header at target has been updated
+				let current_number =
+					best_source_block_number_at_target::<Finality>(&target_client).await?;
+				if current_number != expected_number {
+					return Err(anyhow::format_err!(
+						"Transaction has failed to update best {} header at {} to {}. It is {}",
+						Source::NAME,
+						Target::NAME,
+						expected_number,
+						current_number,
+					))
+				}
+
+				// verify that the pallet is still halted (or operational if it is the last batch)
+				ensure_pallet_operating_mode(&finality_target, is_last_batch).await?;
+			}
+
+			if let Some(latest_andatory_header_number) = latest_andatory_header_number {
+				log::info!(
+					target: "bridge",
+					"Successfully updated best {} header at {} to {}. Pallet is now operational",
+					Source::NAME,
+					Target::NAME,
+					latest_andatory_header_number,
+				);
+			}
+
+			Ok(())
+		})
+	}
+}
+
+/// Mandatory header and its finality proof.
+type HeaderAndProof<P> = (
+	SyncHeader<HeaderOf<<P as SubstrateFinalitySyncPipeline>::SourceChain>>,
+	GrandpaJustification<HeaderOf<<P as SubstrateFinalitySyncPipeline>::SourceChain>>,
+);
+/// Vector of mandatory headers and their finality proofs.
+type HeadersAndProofs<P> = Vec<HeaderAndProof<P>>;
+
+/// Returns best finalized source header number known to the bridge GRANDPA pallet at the target
+/// chain.
+///
+/// This function works even if bridge GRANDPA pallet at the target chain is halted.
+async fn best_source_block_number_at_target<P: SubstrateFinalitySyncPipeline>(
+	target_client: &Client<P::TargetChain>,
+) -> anyhow::Result<BlockNumberOf<P::SourceChain>> {
+	Ok(read_client_state::<P::TargetChain, P::SourceChain>(
+		target_client,
+		None,
+		P::SourceChain::BEST_FINALIZED_HEADER_ID_METHOD,
+	)
+	.await?
+	.best_finalized_peer_at_best_self
+	.0)
+}
+
+/// Verify that the bridge GRANDPA pallet at the target chain is either halted, or operational.
+async fn ensure_pallet_operating_mode<P: SubstrateFinalitySyncPipeline>(
+	finality_target: &SubstrateFinalityTarget<P>,
+	operational: bool,
+) -> anyhow::Result<()> {
+	match (operational, finality_target.ensure_pallet_active().await) {
+		(true, Ok(())) => Ok(()),
+		(false, Err(SubstrateError::BridgePalletIsHalted)) => Ok(()),
+		_ =>
+			return Err(anyhow::format_err!(
+				"Bridge GRANDPA pallet at {} is expected to be {}, but it isn't",
+				P::TargetChain::NAME,
+				if operational { "operational" } else { "halted" },
+			)),
+	}
+}
+
+/// Returns list of all mandatory headers in given range.
+async fn find_mandatory_headers_in_range<P: SubstrateFinalitySyncPipeline>(
+	finality_source: &SubstrateFinalitySource<P>,
+	range: (BlockNumberOf<P::SourceChain>, BlockNumberOf<P::SourceChain>),
+) -> anyhow::Result<HeadersAndProofs<P>> {
+	let mut mandatory_headers = Vec::new();
+	let mut current = range.0;
+	while current <= range.1 {
+		let (header, proof) = finality_source.header_and_finality_proof(current).await?;
+		if header.is_mandatory() {
+			match proof {
+				Some(proof) => mandatory_headers.push((header, proof)),
+				None =>
+					return Err(anyhow::format_err!(
+						"Missing GRANDPA justification for {} header {}",
+						P::SourceChain::NAME,
+						current,
+					)),
+			}
+		}
+
+		current += One::one();
+	}
+
+	Ok(mandatory_headers)
+}
+
+/// Given list of mandatory headers, prepare batches of headers, so that every batch may fit into
+/// single transaction.
+fn make_mandatory_headers_batches<
+	P: SubstrateFinalitySyncPipeline,
+	F: Fn(&HeaderAndProof<P>) -> Weight,
+>(
+	mut headers_to_submit: HeadersAndProofs<P>,
+	submit_header_weight: F,
+) -> Vec<HeadersAndProofs<P>> {
+	// now that we have all mandatory headers, let's prepare transactions
+	// (let's keep all our transactions below 2/3 of max tx size/weight to have some reserve
+	// for utility overhead + for halting transaction)
+	let maximal_tx_size = P::TargetChain::max_extrinsic_size() * 2 / 3;
+	let maximal_tx_weight = P::TargetChain::max_extrinsic_weight() * 2 / 3;
+	let mut current_batch_size: u32 = 0;
+	let mut current_batch_weight: Weight = 0;
+	let mut batches = Vec::new();
+	let mut i = 0;
+	while i < headers_to_submit.len() {
+		let header_and_proof_size =
+			headers_to_submit[i].0.encode().len() + headers_to_submit[i].1.encode().len();
+		let header_and_proof_weight = submit_header_weight(&headers_to_submit[i]);
+
+		let new_batch_size = current_batch_size
+			.saturating_add(u32::try_from(header_and_proof_size).unwrap_or(u32::MAX));
+		let new_batch_weight = current_batch_weight.saturating_add(header_and_proof_weight);
+
+		let is_exceeding_tx_size = new_batch_size > maximal_tx_size;
+		let is_exceeding_tx_weight = new_batch_weight > maximal_tx_weight;
+		let is_new_batch_required = is_exceeding_tx_size || is_exceeding_tx_weight;
+
+		if is_new_batch_required {
+			// if `i` is 0 and we're here, it is a weird situation: even single header submission is
+			// larger than we've planned for a bunch of headers. Let's be optimistic and hope that
+			// the tx will still succeed.
+			let spit_off_index = std::cmp::max(i, 1);
+			let remaining_headers_to_submit = headers_to_submit.split_off(spit_off_index);
+			batches.push(headers_to_submit);
+
+			// we'll reiterate the same header again => so set `current_*` to zero
+			current_batch_size = 0;
+			current_batch_weight = 0;
+			headers_to_submit = remaining_headers_to_submit;
+			i = 0;
+		} else {
+			current_batch_size = new_batch_size;
+			current_batch_weight = new_batch_weight;
+			i += 1;
+		}
+	}
+	if !headers_to_submit.is_empty() {
+		batches.push(headers_to_submit);
+	}
+	batches
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::cli::{RuntimeVersionType, SourceRuntimeVersionParams, TargetRuntimeVersionParams};
+	use bp_test_utils::{make_default_justification, test_header};
+	use relay_polkadot_client::Polkadot;
+	use sp_runtime::{traits::Header as _, DigestItem};
+
+	fn make_header_and_justification(
+		i: u32,
+		size: u32,
+	) -> (SyncHeader<bp_kusama::Header>, GrandpaJustification<bp_kusama::Header>) {
+		let size = size as usize;
+		let mut header: bp_kusama::Header = test_header(i);
+		let justification = make_default_justification(&header);
+		let actual_size = header.encode().len() + justification.encode().len();
+		// additional digest means some additional bytes, so let's decrease `additional_digest_size`
+		// a bit
+		let additional_digest_size = size.saturating_sub(actual_size).saturating_sub(100);
+		header.digest_mut().push(DigestItem::Other(vec![0u8; additional_digest_size]));
+		let justification = make_default_justification(&header);
+		println!("{} {}", size, header.encode().len() + justification.encode().len());
+		(header.into(), justification)
+	}
+
+	#[test]
+	fn should_parse_cli_options() {
+		// when
+		let res = ReinitBridge::from_iter(vec![
+			"reinit-bridge",
+			"kusama-to-polkadot",
+			"--source-host",
+			"127.0.0.1",
+			"--source-port",
+			"42",
+			"--target-host",
+			"127.0.0.1",
+			"--target-port",
+			"43",
+			"--target-signer",
+			"//Alice",
+		]);
+
+		// then
+		assert_eq!(
+			res,
+			ReinitBridge {
+				bridge: ReinitBridgeName::KusamaToPolkadot,
+				source: SourceConnectionParams {
+					source_host: "127.0.0.1".into(),
+					source_port: 42,
+					source_secure: false,
+					source_runtime_version: SourceRuntimeVersionParams {
+						source_version_mode: RuntimeVersionType::Bundle,
+						source_spec_version: None,
+						source_transaction_version: None,
+					}
+				},
+				target: TargetConnectionParams {
+					target_host: "127.0.0.1".into(),
+					target_port: 43,
+					target_secure: false,
+					target_runtime_version: TargetRuntimeVersionParams {
+						target_version_mode: RuntimeVersionType::Bundle,
+						target_spec_version: None,
+						target_transaction_version: None,
+					}
+				},
+				target_sign: TargetSigningParams {
+					target_signer: Some("//Alice".into()),
+					target_signer_password: None,
+					target_signer_file: None,
+					target_signer_password_file: None,
+					target_transactions_mortality: None,
+				},
+			}
+		);
+	}
+
+	#[test]
+	fn make_mandatory_headers_batches_and_empty_headers() {
+		let batches = make_mandatory_headers_batches::<KusamaFinalityToPolkadot, _>(vec![], |_| 0);
+		assert!(batches.is_empty());
+	}
+
+	#[test]
+	fn make_mandatory_headers_batches_with_single_batch() {
+		let headers_to_submit =
+			vec![make_header_and_justification(10, Polkadot::max_extrinsic_size() / 3)];
+		let batches =
+			make_mandatory_headers_batches::<KusamaFinalityToPolkadot, _>(headers_to_submit, |_| 0);
+		assert_eq!(batches.into_iter().map(|x| x.len()).collect::<Vec<_>>(), vec![1],);
+	}
+
+	#[test]
+	fn make_mandatory_headers_batches_group_by_size() {
+		let headers_to_submit = vec![
+			make_header_and_justification(10, Polkadot::max_extrinsic_size() / 3),
+			make_header_and_justification(20, Polkadot::max_extrinsic_size() / 3),
+			make_header_and_justification(30, Polkadot::max_extrinsic_size() * 2 / 3),
+			make_header_and_justification(40, Polkadot::max_extrinsic_size()),
+		];
+		let batches =
+			make_mandatory_headers_batches::<KusamaFinalityToPolkadot, _>(headers_to_submit, |_| 0);
+		assert_eq!(batches.into_iter().map(|x| x.len()).collect::<Vec<_>>(), vec![2, 1, 1],);
+	}
+
+	#[test]
+	fn make_mandatory_headers_batches_group_by_weight() {
+		let headers_to_submit = vec![
+			make_header_and_justification(10, 0),
+			make_header_and_justification(20, 0),
+			make_header_and_justification(30, 0),
+			make_header_and_justification(40, 0),
+		];
+		let batches = make_mandatory_headers_batches::<KusamaFinalityToPolkadot, _>(
+			headers_to_submit,
+			|(header, _)| {
+				if header.number() == 10 || header.number() == 20 {
+					Polkadot::max_extrinsic_weight() / 3
+				} else if header.number() == 30 {
+					Polkadot::max_extrinsic_weight() * 2 / 3
+				} else {
+					Polkadot::max_extrinsic_weight()
+				}
+			},
+		);
+		assert_eq!(batches.into_iter().map(|x| x.len()).collect::<Vec<_>>(), vec![2, 1, 1],);
+	}
+}
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers.rs b/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers.rs
index 82c55965a991aa7870fca627bbd61993b6aa29b0..45034aba4b5e8761883482a59e24d1fea4a034c3 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers.rs
@@ -121,18 +121,26 @@ impl RelayHeaders {
 			let target_client = self.target.to_client::<Target>().await?;
 			let target_transactions_mortality = self.target_sign.target_transactions_mortality;
 			let target_sign = self.target_sign.to_keypair::<Target>()?;
-			let metrics_params = Finality::customize_metrics(self.prometheus_params.into())?;
+
+			let metrics_params: relay_utils::metrics::MetricsParams = self.prometheus_params.into();
 			GlobalMetrics::new()?.register_and_spawn(&metrics_params.registry)?;
 
-			let finality = Finality::new(target_client.clone(), target_sign);
-			finality.start_relay_guards();
+			let target_transactions_params = substrate_relay_helper::TransactionParams {
+				signer: target_sign,
+				mortality: target_transactions_mortality,
+			};
+			Finality::start_relay_guards(
+				&target_client,
+				&target_transactions_params,
+				self.target.can_start_version_guard(),
+			)
+			.await?;
 
-			substrate_relay_helper::finality_pipeline::run(
-				finality,
+			substrate_relay_helper::finality_pipeline::run::<Finality>(
 				source_client,
 				target_client,
 				self.only_mandatory_headers,
-				target_transactions_mortality,
+				target_transactions_params,
 				metrics_params,
 			)
 			.await
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs b/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs
index 9d76a0296fb2cc432cd9046e2e230162de3cd4fc..4ff6ee0947cb2a5c6648532cf5bded67eb81671d 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/relay_headers_and_messages.rs
@@ -29,16 +29,18 @@ use strum::VariantNames;
 use codec::Encode;
 use messages_relay::relay_strategy::MixStrategy;
 use relay_substrate_client::{
-	AccountIdOf, Chain, Client, TransactionSignScheme, UnsignedTransaction,
+	AccountIdOf, CallOf, Chain, ChainRuntimeVersion, Client, SignParam, TransactionSignScheme,
+	UnsignedTransaction,
 };
 use relay_utils::metrics::MetricsParams;
 use sp_core::{Bytes, Pair};
 use substrate_relay_helper::{
-	messages_lane::MessagesRelayParams, on_demand_headers::OnDemandHeadersRelay,
+	finality_pipeline::SubstrateFinalitySyncPipeline, messages_lane::MessagesRelayParams,
+	on_demand_headers::OnDemandHeadersRelay, TransactionParams,
 };
 
 use crate::{
-	cli::{relay_messages::RelayerMode, CliChain, HexLaneId, PrometheusParams},
+	cli::{relay_messages::RelayerMode, CliChain, HexLaneId, PrometheusParams, RuntimeVersionType},
 	declare_chain_options,
 };
 
@@ -48,7 +50,7 @@ use crate::{
 /// stored and real conversion rates. If it is large enough (e.g. > than 10 percents, which is 0.1),
 /// then rational relayers may stop relaying messages because they were submitted using
 /// lesser conversion rate.
-const CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO: f64 = 0.05;
+pub(crate) const CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO: f64 = 0.05;
 
 /// Start headers+messages relayer process.
 #[derive(StructOpt)]
@@ -131,21 +133,9 @@ macro_rules! select_bridge {
 				type LeftAccountIdConverter = bp_millau::AccountIdConverter;
 				type RightAccountIdConverter = bp_rialto::AccountIdConverter;
 
-				const MAX_MISSING_LEFT_HEADERS_AT_RIGHT: bp_millau::BlockNumber =
-					bp_millau::SESSION_LENGTH;
-				const MAX_MISSING_RIGHT_HEADERS_AT_LEFT: bp_rialto::BlockNumber =
-					bp_rialto::SESSION_LENGTH;
-
 				use crate::chains::{
-					millau_messages_to_rialto::{
-						standalone_metrics as left_to_right_standalone_metrics,
-						run as left_to_right_messages,
-						update_rialto_to_millau_conversion_rate as update_right_to_left_conversion_rate,
-					},
-					rialto_messages_to_millau::{
-						run as right_to_left_messages,
-						update_millau_to_rialto_conversion_rate as update_left_to_right_conversion_rate,
-					},
+					millau_messages_to_rialto::MillauMessagesToRialto as LeftToRightMessageLane,
+					rialto_messages_to_millau::RialtoMessagesToMillau as RightToLeftMessageLane,
 				};
 
 				async fn left_create_account(
@@ -180,51 +170,45 @@ macro_rules! select_bridge {
 				type LeftAccountIdConverter = bp_rococo::AccountIdConverter;
 				type RightAccountIdConverter = bp_wococo::AccountIdConverter;
 
-				const MAX_MISSING_LEFT_HEADERS_AT_RIGHT: bp_rococo::BlockNumber =
-					bp_rococo::SESSION_LENGTH;
-				const MAX_MISSING_RIGHT_HEADERS_AT_LEFT: bp_wococo::BlockNumber =
-					bp_wococo::SESSION_LENGTH;
-
 				use crate::chains::{
-					rococo_messages_to_wococo::{
-						standalone_metrics as left_to_right_standalone_metrics,
-						run as left_to_right_messages,
-					},
-					wococo_messages_to_rococo::{
-						run as right_to_left_messages,
-					},
+					rococo_messages_to_wococo::RococoMessagesToWococo as LeftToRightMessageLane,
+					wococo_messages_to_rococo::WococoMessagesToRococo as RightToLeftMessageLane,
 				};
 
-				async fn update_right_to_left_conversion_rate(
-					_client: Client<Left>,
-					_signer: <Left as TransactionSignScheme>::AccountKeyPair,
-					_updated_rate: f64,
-				) -> anyhow::Result<()> {
-					Err(anyhow::format_err!("Conversion rate is not supported by this bridge"))
-				}
-
-				async fn update_left_to_right_conversion_rate(
-					_client: Client<Right>,
-					_signer: <Right as TransactionSignScheme>::AccountKeyPair,
-					_updated_rate: f64,
-				) -> anyhow::Result<()> {
-					Err(anyhow::format_err!("Conversion rate is not supported by this bridge"))
-				}
-
 				async fn left_create_account(
-					_left_client: Client<Left>,
-					_left_sign: <Left as TransactionSignScheme>::AccountKeyPair,
-					_account_id: AccountIdOf<Left>,
+					left_client: Client<Left>,
+					left_sign: <Left as TransactionSignScheme>::AccountKeyPair,
+					account_id: AccountIdOf<Left>,
 				) -> anyhow::Result<()> {
-					Err(anyhow::format_err!("Account creation is not supported by this bridge"))
+					submit_signed_extrinsic(
+						left_client,
+						left_sign,
+						relay_rococo_client::runtime::Call::Balances(
+							relay_rococo_client::runtime::BalancesCall::transfer(
+								bp_rococo::AccountAddress::Id(account_id),
+								bp_rococo::EXISTENTIAL_DEPOSIT.into(),
+							),
+						),
+					)
+					.await
 				}
 
 				async fn right_create_account(
-					_right_client: Client<Right>,
-					_right_sign: <Right as TransactionSignScheme>::AccountKeyPair,
-					_account_id: AccountIdOf<Right>,
+					right_client: Client<Right>,
+					right_sign: <Right as TransactionSignScheme>::AccountKeyPair,
+					account_id: AccountIdOf<Right>,
 				) -> anyhow::Result<()> {
-					Err(anyhow::format_err!("Account creation is not supported by this bridge"))
+					submit_signed_extrinsic(
+						right_client,
+						right_sign,
+						relay_wococo_client::runtime::Call::Balances(
+							relay_wococo_client::runtime::BalancesCall::transfer(
+								bp_wococo::AccountAddress::Id(account_id),
+								bp_wococo::EXISTENTIAL_DEPOSIT.into(),
+							),
+						),
+					)
+					.await
 				}
 
 				$generic
@@ -243,21 +227,9 @@ macro_rules! select_bridge {
 				type LeftAccountIdConverter = bp_kusama::AccountIdConverter;
 				type RightAccountIdConverter = bp_polkadot::AccountIdConverter;
 
-				const MAX_MISSING_LEFT_HEADERS_AT_RIGHT: bp_kusama::BlockNumber =
-					bp_kusama::SESSION_LENGTH;
-				const MAX_MISSING_RIGHT_HEADERS_AT_LEFT: bp_polkadot::BlockNumber =
-					bp_polkadot::SESSION_LENGTH;
-
 				use crate::chains::{
-					kusama_messages_to_polkadot::{
-						standalone_metrics as left_to_right_standalone_metrics,
-						run as left_to_right_messages,
-						update_polkadot_to_kusama_conversion_rate as update_right_to_left_conversion_rate,
-					},
-					polkadot_messages_to_kusama::{
-						run as right_to_left_messages,
-						update_kusama_to_polkadot_conversion_rate as update_left_to_right_conversion_rate,
-					},
+					kusama_messages_to_polkadot::KusamaMessagesToPolkadot as LeftToRightMessageLane,
+					polkadot_messages_to_kusama::PolkadotMessagesToKusama as RightToLeftMessageLane,
 				};
 
 				async fn left_create_account(
@@ -265,29 +237,17 @@ macro_rules! select_bridge {
 					left_sign: <Left as TransactionSignScheme>::AccountKeyPair,
 					account_id: AccountIdOf<Left>,
 				) -> anyhow::Result<()> {
-					let left_genesis_hash = *left_client.genesis_hash();
-					left_client
-						.submit_signed_extrinsic(
-							left_sign.public().into(),
-							move |_, transaction_nonce| {
-								Bytes(
-									Left::sign_transaction(left_genesis_hash, &left_sign, relay_substrate_client::TransactionEra::immortal(),
-										UnsignedTransaction::new(
-											relay_kusama_client::runtime::Call::Balances(
-												relay_kusama_client::runtime::BalancesCall::transfer(
-													bp_kusama::AccountAddress::Id(account_id),
-													bp_kusama::EXISTENTIAL_DEPOSIT.into(),
-												),
-											),
-											transaction_nonce,
-										),
-									).encode()
-								)
-							},
-						)
-						.await
-						.map(drop)
-						.map_err(|e| anyhow::format_err!("{}", e))
+					submit_signed_extrinsic(
+						left_client,
+						left_sign,
+						relay_kusama_client::runtime::Call::Balances(
+							relay_kusama_client::runtime::BalancesCall::transfer(
+								bp_kusama::AccountAddress::Id(account_id),
+								bp_kusama::EXISTENTIAL_DEPOSIT.into(),
+							),
+						),
+					)
+					.await
 				}
 
 				async fn right_create_account(
@@ -295,29 +255,17 @@ macro_rules! select_bridge {
 					right_sign: <Right as TransactionSignScheme>::AccountKeyPair,
 					account_id: AccountIdOf<Right>,
 				) -> anyhow::Result<()> {
-					let right_genesis_hash = *right_client.genesis_hash();
-					right_client
-						.submit_signed_extrinsic(
-							right_sign.public().into(),
-							move |_, transaction_nonce| {
-								Bytes(
-									Right::sign_transaction(right_genesis_hash, &right_sign, relay_substrate_client::TransactionEra::immortal(),
-										UnsignedTransaction::new(
-											relay_polkadot_client::runtime::Call::Balances(
-												relay_polkadot_client::runtime::BalancesCall::transfer(
-													bp_polkadot::AccountAddress::Id(account_id),
-													bp_polkadot::EXISTENTIAL_DEPOSIT.into(),
-												),
-											),
-											transaction_nonce,
-										),
-									).encode()
-								)
-							},
-						)
-						.await
-						.map(drop)
-						.map_err(|e| anyhow::format_err!("{}", e))
+					submit_signed_extrinsic(
+						right_client,
+						right_sign,
+						relay_polkadot_client::runtime::Call::Balances(
+							relay_polkadot_client::runtime::BalancesCall::transfer(
+								bp_polkadot::AccountAddress::Id(account_id),
+								bp_polkadot::EXISTENTIAL_DEPOSIT.into(),
+							),
+						),
+					)
+					.await
 				}
 
 				$generic
@@ -363,11 +311,13 @@ impl RelayHeadersAndMessages {
 			let metrics_params: MetricsParams = params.shared.prometheus_params.into();
 			let metrics_params = relay_utils::relay_metrics(metrics_params).into_params();
 			let left_to_right_metrics =
-				left_to_right_standalone_metrics(left_client.clone(), right_client.clone())?;
+				substrate_relay_helper::messages_metrics::standalone_metrics::<
+					LeftToRightMessageLane,
+				>(left_client.clone(), right_client.clone())?;
 			let right_to_left_metrics = left_to_right_metrics.clone().reverse();
 
 			// start conversion rate update loops for left/right chains
-			if let Some(left_messages_pallet_owner) = left_messages_pallet_owner {
+			if let Some(left_messages_pallet_owner) = left_messages_pallet_owner.clone() {
 				let left_client = left_client.clone();
 				let format_err = || {
 					anyhow::format_err!(
@@ -376,7 +326,15 @@ impl RelayHeadersAndMessages {
 						Left::NAME
 					)
 				};
-				substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop(
+				substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop::<
+					LeftToRightMessageLane,
+					Left,
+				>(
+					left_client.clone(),
+					TransactionParams {
+						signer: left_messages_pallet_owner.clone(),
+						mortality: left_transactions_mortality,
+					},
 					left_to_right_metrics
 						.target_to_source_conversion_rate
 						.as_ref()
@@ -393,24 +351,9 @@ impl RelayHeadersAndMessages {
 						.ok_or_else(format_err)?
 						.shared_value_ref(),
 					CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO,
-					move |new_rate| {
-						log::info!(
-							target: "bridge",
-							"Going to update {} -> {} (on {}) conversion rate to {}.",
-							Right::NAME,
-							Left::NAME,
-							Left::NAME,
-							new_rate,
-						);
-						update_right_to_left_conversion_rate(
-							left_client.clone(),
-							left_messages_pallet_owner.clone(),
-							new_rate,
-						)
-					},
 				);
 			}
-			if let Some(right_messages_pallet_owner) = right_messages_pallet_owner {
+			if let Some(right_messages_pallet_owner) = right_messages_pallet_owner.clone() {
 				let right_client = right_client.clone();
 				let format_err = || {
 					anyhow::format_err!(
@@ -419,38 +362,31 @@ impl RelayHeadersAndMessages {
 						Right::NAME
 					)
 				};
-				substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop(
+				substrate_relay_helper::conversion_rate_update::run_conversion_rate_update_loop::<
+					RightToLeftMessageLane,
+					Right,
+				>(
+					right_client.clone(),
+					TransactionParams {
+						signer: right_messages_pallet_owner.clone(),
+						mortality: right_transactions_mortality,
+					},
 					right_to_left_metrics
 						.target_to_source_conversion_rate
 						.as_ref()
 						.ok_or_else(format_err)?
 						.shared_value_ref(),
-					left_to_right_metrics
-						.source_to_base_conversion_rate
+					right_to_left_metrics
+						.target_to_base_conversion_rate
 						.as_ref()
 						.ok_or_else(format_err)?
 						.shared_value_ref(),
-					left_to_right_metrics
-						.target_to_base_conversion_rate
+					right_to_left_metrics
+						.source_to_base_conversion_rate
 						.as_ref()
 						.ok_or_else(format_err)?
 						.shared_value_ref(),
 					CONVERSION_RATE_ALLOWED_DIFFERENCE_RATIO,
-					move |new_rate| {
-						log::info!(
-							target: "bridge",
-							"Going to update {} -> {} (on {}) conversion rate to {}.",
-							Left::NAME,
-							Right::NAME,
-							Right::NAME,
-							new_rate,
-						);
-						update_left_to_right_conversion_rate(
-							right_client.clone(),
-							right_messages_pallet_owner.clone(),
-							new_rate,
-						)
-					},
 				);
 			}
 
@@ -493,21 +429,55 @@ impl RelayHeadersAndMessages {
 				}
 			}
 
+			// add balance-related metrics
+			let metrics_params =
+				substrate_relay_helper::messages_metrics::add_relay_balances_metrics(
+					left_client.clone(),
+					metrics_params,
+					Some(left_sign.public().into()),
+					left_messages_pallet_owner.map(|kp| kp.public().into()),
+				)
+				.await?;
+			let metrics_params =
+				substrate_relay_helper::messages_metrics::add_relay_balances_metrics(
+					right_client.clone(),
+					metrics_params,
+					Some(right_sign.public().into()),
+					right_messages_pallet_owner.map(|kp| kp.public().into()),
+				)
+				.await?;
+
 			// start on-demand header relays
-			let left_to_right_on_demand_headers = OnDemandHeadersRelay::new(
+			let left_to_right_transaction_params = TransactionParams {
+				mortality: right_transactions_mortality,
+				signer: right_sign.clone(),
+			};
+			let right_to_left_transaction_params = TransactionParams {
+				mortality: left_transactions_mortality,
+				signer: left_sign.clone(),
+			};
+			LeftToRightFinality::start_relay_guards(
+				&right_client,
+				&left_to_right_transaction_params,
+				params.right.can_start_version_guard(),
+			)
+			.await?;
+			RightToLeftFinality::start_relay_guards(
+				&left_client,
+				&right_to_left_transaction_params,
+				params.left.can_start_version_guard(),
+			)
+			.await?;
+			let left_to_right_on_demand_headers = OnDemandHeadersRelay::new::<LeftToRightFinality>(
 				left_client.clone(),
 				right_client.clone(),
-				right_transactions_mortality,
-				LeftToRightFinality::new(right_client.clone(), right_sign.clone()),
-				MAX_MISSING_LEFT_HEADERS_AT_RIGHT,
+				left_to_right_transaction_params,
 				params.shared.only_mandatory_headers,
 			);
-			let right_to_left_on_demand_headers = OnDemandHeadersRelay::new(
+			let right_to_left_on_demand_headers = OnDemandHeadersRelay::new::<RightToLeftFinality>(
 				right_client.clone(),
 				left_client.clone(),
-				left_transactions_mortality,
-				RightToLeftFinality::new(left_client.clone(), left_sign.clone()),
-				MAX_MISSING_RIGHT_HEADERS_AT_LEFT,
+				right_to_left_transaction_params,
 				params.shared.only_mandatory_headers,
 			);
 
@@ -515,13 +485,19 @@ impl RelayHeadersAndMessages {
 			let mut message_relays = Vec::with_capacity(lanes.len() * 2);
 			for lane in lanes {
 				let lane = lane.into();
-				let left_to_right_messages = left_to_right_messages(MessagesRelayParams {
+				let left_to_right_messages = substrate_relay_helper::messages_lane::run::<
+					LeftToRightMessageLane,
+				>(MessagesRelayParams {
 					source_client: left_client.clone(),
-					source_sign: left_sign.clone(),
-					source_transactions_mortality: left_transactions_mortality,
+					source_transaction_params: TransactionParams {
+						signer: left_sign.clone(),
+						mortality: left_transactions_mortality,
+					},
 					target_client: right_client.clone(),
-					target_sign: right_sign.clone(),
-					target_transactions_mortality: right_transactions_mortality,
+					target_transaction_params: TransactionParams {
+						signer: right_sign.clone(),
+						mortality: right_transactions_mortality,
+					},
 					source_to_target_headers_relay: Some(left_to_right_on_demand_headers.clone()),
 					target_to_source_headers_relay: Some(right_to_left_on_demand_headers.clone()),
 					lane_id: lane,
@@ -531,13 +507,19 @@ impl RelayHeadersAndMessages {
 				})
 				.map_err(|e| anyhow::format_err!("{}", e))
 				.boxed();
-				let right_to_left_messages = right_to_left_messages(MessagesRelayParams {
+				let right_to_left_messages = substrate_relay_helper::messages_lane::run::<
+					RightToLeftMessageLane,
+				>(MessagesRelayParams {
 					source_client: right_client.clone(),
-					source_sign: right_sign.clone(),
-					source_transactions_mortality: right_transactions_mortality,
+					source_transaction_params: TransactionParams {
+						signer: right_sign.clone(),
+						mortality: right_transactions_mortality,
+					},
 					target_client: left_client.clone(),
-					target_sign: left_sign.clone(),
-					target_transactions_mortality: left_transactions_mortality,
+					target_transaction_params: TransactionParams {
+						signer: left_sign.clone(),
+						mortality: left_transactions_mortality,
+					},
 					source_to_target_headers_relay: Some(right_to_left_on_demand_headers.clone()),
 					target_to_source_headers_relay: Some(left_to_right_on_demand_headers.clone()),
 					lane_id: lane,
@@ -561,3 +543,34 @@ impl RelayHeadersAndMessages {
 		})
 	}
 }
+
+/// Sign and submit transaction with given call to the chain.
+async fn submit_signed_extrinsic<C: Chain + TransactionSignScheme<Chain = C>>(
+	client: Client<C>,
+	sign: C::AccountKeyPair,
+	call: CallOf<C>,
+) -> anyhow::Result<()>
+where
+	AccountIdOf<C>: From<<<C as TransactionSignScheme>::AccountKeyPair as Pair>::Public>,
+	CallOf<C>: Send,
+{
+	let genesis_hash = *client.genesis_hash();
+	let (spec_version, transaction_version) = client.simple_runtime_version().await?;
+	client
+		.submit_signed_extrinsic(sign.public().into(), move |_, transaction_nonce| {
+			Ok(Bytes(
+				C::sign_transaction(SignParam {
+					spec_version,
+					transaction_version,
+					genesis_hash,
+					signer: sign,
+					era: relay_substrate_client::TransactionEra::immortal(),
+					unsigned: UnsignedTransaction::new(call.into(), transaction_nonce),
+				})?
+				.encode(),
+			))
+		})
+		.await
+		.map(drop)
+		.map_err(|e| anyhow::format_err!("{}", e))
+}
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/relay_messages.rs b/polkadot/bridges/relays/bin-substrate/src/cli/relay_messages.rs
index e47abfc5d94e34389456659b7a3ab94dc439daaa..45087fad5eb3fa1782bdb6a72e391a06cf5d5505 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/relay_messages.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/relay_messages.rs
@@ -18,7 +18,7 @@ use structopt::StructOpt;
 use strum::{EnumString, EnumVariantNames, VariantNames};
 
 use messages_relay::relay_strategy::MixStrategy;
-use substrate_relay_helper::messages_lane::MessagesRelayParams;
+use substrate_relay_helper::{messages_lane::MessagesRelayParams, TransactionParams};
 
 use crate::{
 	cli::{
@@ -84,13 +84,17 @@ impl RelayMessages {
 			let relayer_mode = self.relayer_mode.into();
 			let relay_strategy = MixStrategy::new(relayer_mode);
 
-			relay_messages(MessagesRelayParams {
+			substrate_relay_helper::messages_lane::run::<MessagesLane>(MessagesRelayParams {
 				source_client,
-				source_sign,
-				source_transactions_mortality,
+				source_transaction_params: TransactionParams {
+					signer: source_sign,
+					mortality: source_transactions_mortality,
+				},
 				target_client,
-				target_sign,
-				target_transactions_mortality,
+				target_transaction_params: TransactionParams {
+					signer: target_sign,
+					mortality: target_transactions_mortality,
+				},
 				source_to_target_headers_relay: None,
 				target_to_source_headers_relay: None,
 				lane_id: self.lane.into(),
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/resubmit_transactions.rs b/polkadot/bridges/relays/bin-substrate/src/cli/resubmit_transactions.rs
index 64663d7e8ec026032cdb67c26fbf96508cdf3b39..f92c035082cbd9df1be4a903a0ca8263b0fb2a87 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/resubmit_transactions.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/resubmit_transactions.rs
@@ -19,9 +19,10 @@ use crate::cli::{Balance, TargetConnectionParams, TargetSigningParams};
 use codec::{Decode, Encode};
 use num_traits::{One, Zero};
 use relay_substrate_client::{
-	BlockWithJustification, Chain, Client, Error as SubstrateError, HeaderOf, TransactionSignScheme,
+	BlockWithJustification, Chain, Client, Error as SubstrateError, HeaderIdOf, HeaderOf,
+	SignParam, TransactionSignScheme,
 };
-use relay_utils::FailedClient;
+use relay_utils::{FailedClient, HeaderId};
 use sp_core::Bytes;
 use sp_runtime::{
 	traits::{Hash, Header as HeaderT},
@@ -29,6 +30,7 @@ use sp_runtime::{
 };
 use structopt::StructOpt;
 use strum::{EnumString, EnumVariantNames, VariantNames};
+use substrate_relay_helper::TransactionParams;
 
 /// Start resubmit transactions process.
 #[derive(StructOpt)]
@@ -115,13 +117,16 @@ impl ResubmitTransactions {
 		select_bridge!(self.chain, {
 			let relay_loop_name = format!("ResubmitTransactions{}", Target::NAME);
 			let client = self.target.to_client::<Target>().await?;
-			let key_pair = self.target_sign.to_keypair::<Target>()?;
+			let transaction_params = TransactionParams {
+				signer: self.target_sign.to_keypair::<Target>()?,
+				mortality: self.target_sign.target_transactions_mortality,
+			};
 
 			relay_utils::relay_loop((), client)
 				.run(relay_loop_name, move |_, client, _| {
 					run_until_connection_lost::<Target, TargetSign>(
 						client,
-						key_pair.clone(),
+						transaction_params.clone(),
 						Context {
 							strategy: self.strategy,
 							best_header: HeaderOf::<Target>::new(
@@ -212,13 +217,14 @@ impl<C: Chain> Context<C> {
 /// Run resubmit transactions loop.
 async fn run_until_connection_lost<C: Chain, S: TransactionSignScheme<Chain = C>>(
 	client: Client<C>,
-	key_pair: S::AccountKeyPair,
+	transaction_params: TransactionParams<S::AccountKeyPair>,
 	mut context: Context<C>,
 ) -> Result<(), FailedClient> {
 	loop {
 		async_std::task::sleep(C::AVERAGE_BLOCK_INTERVAL).await;
 
-		let result = run_loop_iteration::<C, S>(client.clone(), key_pair.clone(), context).await;
+		let result =
+			run_loop_iteration::<C, S>(client.clone(), transaction_params.clone(), context).await;
 		context = match result {
 			Ok(context) => context,
 			Err(error) => {
@@ -237,20 +243,21 @@ async fn run_until_connection_lost<C: Chain, S: TransactionSignScheme<Chain = C>
 /// Run single loop iteration.
 async fn run_loop_iteration<C: Chain, S: TransactionSignScheme<Chain = C>>(
 	client: Client<C>,
-	key_pair: S::AccountKeyPair,
+	transaction_params: TransactionParams<S::AccountKeyPair>,
 	mut context: Context<C>,
 ) -> Result<Context<C>, SubstrateError> {
 	// correct best header is required for all other actions
 	context.best_header = client.best_header().await?;
 
 	// check if there's queued transaction, signed by given author
-	let original_transaction = match lookup_signer_transaction::<C, S>(&client, &key_pair).await? {
-		Some(original_transaction) => original_transaction,
-		None => {
-			log::trace!(target: "bridge", "No {} transactions from required signer in the txpool", C::NAME);
-			return Ok(context)
-		},
-	};
+	let original_transaction =
+		match lookup_signer_transaction::<C, S>(&client, &transaction_params.signer).await? {
+			Some(original_transaction) => original_transaction,
+			None => {
+				log::trace!(target: "bridge", "No {} transactions from required signer in the txpool", C::NAME);
+				return Ok(context)
+			},
+		};
 	let original_transaction_hash = C::Hasher::hash(&original_transaction.encode());
 	let context = context.notice_transaction(original_transaction_hash);
 
@@ -280,8 +287,8 @@ async fn run_loop_iteration<C: Chain, S: TransactionSignScheme<Chain = C>>(
 	// update transaction tip
 	let (is_updated, updated_transaction) = update_transaction_tip::<C, S>(
 		&client,
-		&key_pair,
-		context.best_header.hash(),
+		&transaction_params,
+		HeaderId(*context.best_header.number(), context.best_header.hash()),
 		original_transaction,
 		context.tip_step,
 		context.tip_limit,
@@ -397,20 +404,21 @@ fn select_transaction_from_queue<C: Chain>(
 /// Try to find appropriate tip for transaction so that its priority is larger than given.
 async fn update_transaction_tip<C: Chain, S: TransactionSignScheme<Chain = C>>(
 	client: &Client<C>,
-	key_pair: &S::AccountKeyPair,
-	at_block: C::Hash,
+	transaction_params: &TransactionParams<S::AccountKeyPair>,
+	at_block: HeaderIdOf<C>,
 	tx: S::SignedTransaction,
 	tip_step: C::Balance,
 	tip_limit: C::Balance,
 	target_priority: TransactionPriority,
 ) -> Result<(bool, S::SignedTransaction), SubstrateError> {
 	let stx = format!("{:?}", tx);
-	let mut current_priority = client.validate_transaction(at_block, tx.clone()).await??.priority;
+	let mut current_priority = client.validate_transaction(at_block.1, tx.clone()).await??.priority;
 	let mut unsigned_tx = S::parse_transaction(tx).ok_or_else(|| {
 		SubstrateError::Custom(format!("Failed to parse {} transaction {}", C::NAME, stx,))
 	})?;
 	let old_tip = unsigned_tx.tip;
 
+	let (spec_version, transaction_version) = client.simple_runtime_version().await?;
 	while current_priority < target_priority {
 		let next_tip = unsigned_tx.tip + tip_step;
 		if next_tip > tip_limit {
@@ -429,13 +437,15 @@ async fn update_transaction_tip<C: Chain, S: TransactionSignScheme<Chain = C>>(
 		unsigned_tx.tip = next_tip;
 		current_priority = client
 			.validate_transaction(
-				at_block,
-				S::sign_transaction(
-					*client.genesis_hash(),
-					key_pair,
-					relay_substrate_client::TransactionEra::immortal(),
-					unsigned_tx.clone(),
-				),
+				at_block.1,
+				S::sign_transaction(SignParam {
+					spec_version,
+					transaction_version,
+					genesis_hash: *client.genesis_hash(),
+					signer: transaction_params.signer.clone(),
+					era: relay_substrate_client::TransactionEra::immortal(),
+					unsigned: unsigned_tx.clone(),
+				})?,
 			)
 			.await??
 			.priority;
@@ -451,12 +461,17 @@ async fn update_transaction_tip<C: Chain, S: TransactionSignScheme<Chain = C>>(
 
 	Ok((
 		old_tip != unsigned_tx.tip,
-		S::sign_transaction(
-			*client.genesis_hash(),
-			key_pair,
-			relay_substrate_client::TransactionEra::immortal(),
-			unsigned_tx,
-		),
+		S::sign_transaction(SignParam {
+			spec_version,
+			transaction_version,
+			genesis_hash: *client.genesis_hash(),
+			signer: transaction_params.signer.clone(),
+			era: relay_substrate_client::TransactionEra::new(
+				at_block,
+				transaction_params.mortality,
+			),
+			unsigned: unsigned_tx,
+		})?,
 	))
 }
 
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/send_message.rs b/polkadot/bridges/relays/bin-substrate/src/cli/send_message.rs
index 3e77ad8342927bdfc5a16f85b72098d22ec246a4..ddb1ff59b5d087f4e1a7577bd17082932dbf81f3 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/send_message.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/send_message.rs
@@ -17,15 +17,15 @@
 use crate::cli::{
 	bridge::FullBridge,
 	encode_call::{self, CliEncodeCall},
-	estimate_fee::estimate_message_delivery_and_dispatch_fee,
-	Balance, CliChain, ExplicitOrMaximal, HexBytes, HexLaneId, Origins, SourceConnectionParams,
-	SourceSigningParams, TargetSigningParams,
+	estimate_fee::{estimate_message_delivery_and_dispatch_fee, ConversionRateOverride},
+	Balance, ExplicitOrMaximal, HexBytes, HexLaneId, Origins, SourceConnectionParams,
+	SourceSigningParams, TargetConnectionParams, TargetSigningParams,
 };
 use bp_message_dispatch::{CallOrigin, MessagePayload};
-use bp_runtime::BalanceOf;
+use bp_runtime::Chain as _;
 use codec::Encode;
 use frame_support::weights::Weight;
-use relay_substrate_client::{Chain, TransactionSignScheme, UnsignedTransaction};
+use relay_substrate_client::{Chain, SignParam, TransactionSignScheme, UnsignedTransaction};
 use sp_core::{Bytes, Pair};
 use sp_runtime::{traits::IdentifyAccount, AccountId32, MultiSignature, MultiSigner};
 use std::fmt::Debug;
@@ -66,6 +66,12 @@ pub struct SendMessage {
 	/// Hex-encoded lane id. Defaults to `00000000`.
 	#[structopt(long, default_value = "00000000")]
 	lane: HexLaneId,
+	/// A way to override conversion rate between bridge tokens.
+	///
+	/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
+	/// your message won't be relayed.
+	#[structopt(long)]
+	conversion_rate_override: Option<ConversionRateOverride>,
 	/// Where dispatch fee is paid?
 	#[structopt(
 		long,
@@ -88,10 +94,16 @@ pub struct SendMessage {
 	/// `SourceAccount`.
 	#[structopt(long, possible_values = &Origins::variants(), default_value = "Source")]
 	origin: Origins,
+
+	// Normally we don't need to connect to the target chain to send message. But for testing
+	// we may want to use **actual** `spec_version` of the target chain when composing a message.
+	// Then we'll need to read version from the target chain node.
+	#[structopt(flatten)]
+	target: TargetConnectionParams,
 }
 
 impl SendMessage {
-	pub fn encode_payload(
+	pub async fn encode_payload(
 		&mut self,
 	) -> anyhow::Result<MessagePayload<AccountId32, MultiSigner, MultiSignature, Vec<u8>>> {
 		crate::select_full_bridge!(self.bridge, {
@@ -110,18 +122,23 @@ impl SendMessage {
 
 			encode_call::preprocess_call::<Source, Target>(message, bridge.bridge_instance_index());
 			let target_call = Target::encode_call(message)?;
+			let target_spec_version = self.target.selected_chain_spec_version::<Target>().await?;
 
 			let payload = {
 				let target_call_weight = prepare_call_dispatch_weight(
 					dispatch_weight,
-					ExplicitOrMaximal::Explicit(Target::get_dispatch_info(&target_call)?.weight),
+					|| {
+						Ok(ExplicitOrMaximal::Explicit(
+							Target::get_dispatch_info(&target_call)?.weight,
+						))
+					},
 					compute_maximal_message_dispatch_weight(Target::max_extrinsic_weight()),
-				);
+				)?;
 				let source_sender_public: MultiSigner = source_sign.public().into();
 				let source_account_id = source_sender_public.into_account();
 
 				message_payload(
-					Target::RUNTIME_VERSION.spec_version,
+					target_spec_version,
 					target_call_weight,
 					match origin {
 						Origins::Source => CallOrigin::SourceAccount(source_account_id),
@@ -130,7 +147,7 @@ impl SendMessage {
 							let digest = account_ownership_digest(
 								&target_call,
 								source_account_id.clone(),
-								Target::RUNTIME_VERSION.spec_version,
+								target_spec_version,
 							);
 							let target_origin_public = target_sign.public();
 							let digest_signature = target_sign.sign(&digest);
@@ -152,17 +169,19 @@ impl SendMessage {
 	/// Run the command.
 	pub async fn run(mut self) -> anyhow::Result<()> {
 		crate::select_full_bridge!(self.bridge, {
-			let payload = self.encode_payload()?;
+			let payload = self.encode_payload().await?;
 
 			let source_client = self.source.to_client::<Source>().await?;
 			let source_sign = self.source_sign.to_keypair::<Source>()?;
 
 			let lane = self.lane.clone().into();
+			let conversion_rate_override = self.conversion_rate_override;
 			let fee = match self.fee {
 				Some(fee) => fee,
 				None => Balance(
-					estimate_message_delivery_and_dispatch_fee::<BalanceOf<Source>, _, _>(
+					estimate_message_delivery_and_dispatch_fee::<Source, Target, _>(
 						&source_client,
+						conversion_rate_override,
 						ESTIMATE_MESSAGE_FEE_METHOD,
 						lane,
 						payload.clone(),
@@ -171,6 +190,7 @@ impl SendMessage {
 				),
 			};
 			let dispatch_weight = payload.weight;
+			let payload_len = payload.encode().len();
 			let send_message_call = Source::encode_call(&encode_call::Call::BridgeSendMessage {
 				bridge_instance_index: self.bridge.bridge_instance_index(),
 				lane: self.lane,
@@ -179,25 +199,31 @@ impl SendMessage {
 			})?;
 
 			let source_genesis_hash = *source_client.genesis_hash();
+			let (spec_version, transaction_version) =
+				source_client.simple_runtime_version().await?;
 			let estimated_transaction_fee = source_client
 				.estimate_extrinsic_fee(Bytes(
-					Source::sign_transaction(
-						source_genesis_hash,
-						&source_sign,
-						relay_substrate_client::TransactionEra::immortal(),
-						UnsignedTransaction::new(send_message_call.clone(), 0),
-					)
+					Source::sign_transaction(SignParam {
+						spec_version,
+						transaction_version,
+						genesis_hash: source_genesis_hash,
+						signer: source_sign.clone(),
+						era: relay_substrate_client::TransactionEra::immortal(),
+						unsigned: UnsignedTransaction::new(send_message_call.clone(), 0),
+					})?
 					.encode(),
 				))
 				.await?;
 			source_client
 				.submit_signed_extrinsic(source_sign.public().into(), move |_, transaction_nonce| {
-					let signed_source_call = Source::sign_transaction(
-						source_genesis_hash,
-						&source_sign,
-						relay_substrate_client::TransactionEra::immortal(),
-						UnsignedTransaction::new(send_message_call, transaction_nonce),
-					)
+					let signed_source_call = Source::sign_transaction(SignParam {
+						spec_version,
+						transaction_version,
+						genesis_hash: source_genesis_hash,
+						signer: source_sign.clone(),
+						era: relay_substrate_client::TransactionEra::immortal(),
+						unsigned: UnsignedTransaction::new(send_message_call, transaction_nonce),
+					})?
 					.encode();
 
 					log::info!(
@@ -205,7 +231,7 @@ impl SendMessage {
 						"Sending message to {}. Lane: {:?}. Size: {}. Dispatch weight: {}. Fee: {}",
 						Target::NAME,
 						lane,
-						signed_source_call.len(),
+						payload_len,
 						dispatch_weight,
 						fee,
 					);
@@ -225,7 +251,7 @@ impl SendMessage {
 						HexBytes::encode(&signed_source_call)
 					);
 
-					Bytes(signed_source_call)
+					Ok(Bytes(signed_source_call))
 				})
 				.await?;
 		});
@@ -236,12 +262,16 @@ impl SendMessage {
 
 fn prepare_call_dispatch_weight(
 	user_specified_dispatch_weight: &Option<ExplicitOrMaximal<Weight>>,
-	weight_from_pre_dispatch_call: ExplicitOrMaximal<Weight>,
+	weight_from_pre_dispatch_call: impl Fn() -> anyhow::Result<ExplicitOrMaximal<Weight>>,
 	maximal_allowed_weight: Weight,
-) -> Weight {
-	match user_specified_dispatch_weight.clone().unwrap_or(weight_from_pre_dispatch_call) {
-		ExplicitOrMaximal::Explicit(weight) => weight,
-		ExplicitOrMaximal::Maximal => maximal_allowed_weight,
+) -> anyhow::Result<Weight> {
+	match user_specified_dispatch_weight
+		.clone()
+		.map(Ok)
+		.unwrap_or_else(weight_from_pre_dispatch_call)?
+	{
+		ExplicitOrMaximal::Explicit(weight) => Ok(weight),
+		ExplicitOrMaximal::Maximal => Ok(maximal_allowed_weight),
 	}
 }
 
@@ -283,10 +313,11 @@ pub(crate) fn compute_maximal_message_dispatch_weight(maximal_extrinsic_weight:
 #[cfg(test)]
 mod tests {
 	use super::*;
+	use crate::cli::CliChain;
 	use hex_literal::hex;
 
-	#[test]
-	fn send_remark_rialto_to_millau() {
+	#[async_std::test]
+	async fn send_remark_rialto_to_millau() {
 		// given
 		let mut send_message = SendMessage::from_iter(vec![
 			"send-message",
@@ -295,20 +326,22 @@ mod tests {
 			"1234",
 			"--source-signer",
 			"//Alice",
+			"--conversion-rate-override",
+			"0.75",
 			"remark",
 			"--remark-payload",
 			"1234",
 		]);
 
 		// when
-		let payload = send_message.encode_payload().unwrap();
+		let payload = send_message.encode_payload().await.unwrap();
 
 		// then
 		assert_eq!(
 			payload,
 			MessagePayload {
 				spec_version: relay_millau_client::Millau::RUNTIME_VERSION.spec_version,
-				weight: 576000,
+				weight: 0,
 				origin: CallOrigin::SourceAccount(
 					sp_keyring::AccountKeyring::Alice.to_account_id()
 				),
@@ -318,8 +351,8 @@ mod tests {
 		);
 	}
 
-	#[test]
-	fn send_remark_millau_to_rialto() {
+	#[async_std::test]
+	async fn send_remark_millau_to_rialto() {
 		// given
 		let mut send_message = SendMessage::from_iter(vec![
 			"send-message",
@@ -332,13 +365,15 @@ mod tests {
 			"Target",
 			"--target-signer",
 			"//Bob",
+			"--conversion-rate-override",
+			"metric",
 			"remark",
 			"--remark-payload",
 			"1234",
 		]);
 
 		// when
-		let payload = send_message.encode_payload().unwrap();
+		let payload = send_message.encode_payload().await.unwrap();
 
 		// then
 		// Since signatures are randomized we extract it from here and only check the rest.
@@ -350,7 +385,7 @@ mod tests {
 			payload,
 			MessagePayload {
 				spec_version: relay_millau_client::Millau::RUNTIME_VERSION.spec_version,
-				weight: 576000,
+				weight: 0,
 				origin: CallOrigin::TargetAccount(
 					sp_keyring::AccountKeyring::Alice.to_account_id(),
 					sp_keyring::AccountKeyring::Bob.into(),
@@ -382,8 +417,8 @@ mod tests {
 		assert!(send_message.is_ok());
 	}
 
-	#[test]
-	fn accepts_non_default_dispatch_fee_payment() {
+	#[async_std::test]
+	async fn accepts_non_default_dispatch_fee_payment() {
 		// given
 		let mut send_message = SendMessage::from_iter(vec![
 			"send-message",
@@ -398,7 +433,7 @@ mod tests {
 		]);
 
 		// when
-		let payload = send_message.encode_payload().unwrap();
+		let payload = send_message.encode_payload().await.unwrap();
 
 		// then
 		assert_eq!(
diff --git a/polkadot/bridges/relays/bin-substrate/src/cli/swap_tokens.rs b/polkadot/bridges/relays/bin-substrate/src/cli/swap_tokens.rs
index dbe46f469070982383624cad63214adce761b6c8..0758deddfd103f57da3c4d66aeebea7d480a86ba 100644
--- a/polkadot/bridges/relays/bin-substrate/src/cli/swap_tokens.rs
+++ b/polkadot/bridges/relays/bin-substrate/src/cli/swap_tokens.rs
@@ -29,15 +29,15 @@ use strum::{EnumString, EnumVariantNames, VariantNames};
 use frame_support::dispatch::GetDispatchInfo;
 use relay_substrate_client::{
 	AccountIdOf, AccountPublicOf, BalanceOf, BlockNumberOf, CallOf, Chain, ChainWithBalances,
-	Client, Error as SubstrateError, HashOf, SignatureOf, Subscription, TransactionSignScheme,
-	TransactionStatusOf, UnsignedTransaction,
+	Client, Error as SubstrateError, HashOf, SignParam, SignatureOf, Subscription,
+	TransactionSignScheme, TransactionStatusOf, UnsignedTransaction,
 };
-use sp_core::{blake2_256, storage::StorageKey, Bytes, Pair, H256, U256};
+use sp_core::{blake2_256, storage::StorageKey, Bytes, Pair, U256};
 use sp_runtime::traits::{Convert, Header as HeaderT};
 
 use crate::cli::{
-	Balance, CliChain, SourceConnectionParams, SourceSigningParams, TargetConnectionParams,
-	TargetSigningParams,
+	estimate_fee::ConversionRateOverride, Balance, CliChain, SourceConnectionParams,
+	SourceSigningParams, TargetConnectionParams, TargetSigningParams,
 };
 
 /// Swap tokens.
@@ -65,6 +65,18 @@ pub struct SwapTokens {
 	/// Target chain balance that target signer wants to swap.
 	#[structopt(long)]
 	target_balance: Balance,
+	/// A way to override conversion rate from target to source tokens.
+	///
+	/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
+	/// your message won't be relayed.
+	#[structopt(long)]
+	target_to_source_conversion_rate_override: Option<ConversionRateOverride>,
+	/// A way to override conversion rate from source to target tokens.
+	///
+	/// If not specified, conversion rate from runtime storage is used. It may be obsolete and
+	/// your message won't be relayed.
+	#[structopt(long)]
+	source_to_target_conversion_rate_override: Option<ConversionRateOverride>,
 }
 
 /// Token swap type.
@@ -98,6 +110,8 @@ macro_rules! select_bridge {
 			SwapTokensBridge::MillauToRialto => {
 				type Source = relay_millau_client::Millau;
 				type Target = relay_rialto_client::Rialto;
+				const SOURCE_SPEC_VERSION: u32 = millau_runtime::VERSION.spec_version;
+				const TARGET_SPEC_VERSION: u32 = rialto_runtime::VERSION.spec_version;
 
 				type FromSwapToThisAccountIdConverter = bp_rialto::AccountIdConverter;
 
@@ -114,9 +128,6 @@ macro_rules! select_bridge {
 				const SOURCE_CHAIN_ID: bp_runtime::ChainId = bp_runtime::MILLAU_CHAIN_ID;
 				const TARGET_CHAIN_ID: bp_runtime::ChainId = bp_runtime::RIALTO_CHAIN_ID;
 
-				const SOURCE_SPEC_VERSION: u32 = millau_runtime::VERSION.spec_version;
-				const TARGET_SPEC_VERSION: u32 = rialto_runtime::VERSION.spec_version;
-
 				const SOURCE_TO_TARGET_LANE_ID: bp_messages::LaneId = *b"swap";
 				const TARGET_TO_SOURCE_LANE_ID: bp_messages::LaneId = [0, 0, 0, 0];
 
@@ -134,6 +145,10 @@ impl SwapTokens {
 			let source_sign = self.source_sign.to_keypair::<Target>()?;
 			let target_client = self.target.to_client::<Target>().await?;
 			let target_sign = self.target_sign.to_keypair::<Target>()?;
+			let target_to_source_conversion_rate_override =
+				self.target_to_source_conversion_rate_override;
+			let source_to_target_conversion_rate_override =
+				self.source_to_target_conversion_rate_override;
 
 			// names of variables in this function are matching names used by the
 			// `pallet-bridge-token-swap`
@@ -199,9 +214,14 @@ impl SwapTokens {
 			// prepare `create_swap` call
 			let target_public_at_bridged_chain: AccountPublicOf<Target> =
 				target_sign.public().into();
-			let swap_delivery_and_dispatch_fee: BalanceOf<Source> =
-				crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee(
+			let swap_delivery_and_dispatch_fee =
+				crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee::<
+					Source,
+					Target,
+					_,
+				>(
 					&source_client,
+					target_to_source_conversion_rate_override.clone(),
 					ESTIMATE_SOURCE_TO_TARGET_MESSAGE_FEE_METHOD,
 					SOURCE_TO_TARGET_LANE_ID,
 					bp_message_dispatch::MessagePayload {
@@ -234,20 +254,27 @@ impl SwapTokens {
 			// start tokens swap
 			let source_genesis_hash = *source_client.genesis_hash();
 			let create_swap_signer = source_sign.clone();
+			let (spec_version, transaction_version) =
+				source_client.simple_runtime_version().await?;
 			let swap_created_at = wait_until_transaction_is_finalized::<Source>(
 				source_client
 					.submit_and_watch_signed_extrinsic(
 						accounts.source_account_at_this_chain.clone(),
 						move |_, transaction_nonce| {
-							Bytes(
-								Source::sign_transaction(
-									source_genesis_hash,
-									&create_swap_signer,
-									relay_substrate_client::TransactionEra::immortal(),
-									UnsignedTransaction::new(create_swap_call, transaction_nonce),
-								)
+							Ok(Bytes(
+								Source::sign_transaction(SignParam {
+									spec_version,
+									transaction_version,
+									genesis_hash: source_genesis_hash,
+									signer: create_swap_signer,
+									era: relay_substrate_client::TransactionEra::immortal(),
+									unsigned: UnsignedTransaction::new(
+										create_swap_call.into(),
+										transaction_nonce,
+									),
+								})?
 								.encode(),
-							)
+							))
 						},
 					)
 					.await?,
@@ -255,11 +282,10 @@ impl SwapTokens {
 			.await?;
 
 			// read state of swap after it has been created
-			let token_swap_hash: H256 = token_swap.using_encoded(blake2_256).into();
-			let token_swap_storage_key = bp_runtime::storage_map_final_key_identity(
+			let token_swap_hash = token_swap.hash();
+			let token_swap_storage_key = bp_token_swap::storage_keys::pending_swaps_key(
 				TOKEN_SWAP_PALLET_NAME,
-				pallet_bridge_token_swap::PENDING_SWAPS_MAP_NAME,
-				token_swap_hash.as_ref(),
+				token_swap_hash,
 			);
 			match read_token_swap_state(&source_client, swap_created_at, &token_swap_storage_key)
 				.await?
@@ -337,7 +363,7 @@ impl SwapTokens {
 			//
 
 			if is_transfer_succeeded {
-				log::info!(target: "bridge", "Claiming the swap swap");
+				log::info!(target: "bridge", "Claiming the swap");
 
 				// prepare `claim_swap` message that will be sent over the bridge
 				let claim_swap_call: CallOf<Source> =
@@ -351,9 +377,14 @@ impl SwapTokens {
 					dispatch_fee_payment: bp_runtime::messages::DispatchFeePayment::AtSourceChain,
 					call: claim_swap_call.encode(),
 				};
-				let claim_swap_delivery_and_dispatch_fee: BalanceOf<Target> =
-					crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee(
+				let claim_swap_delivery_and_dispatch_fee =
+					crate::cli::estimate_fee::estimate_message_delivery_and_dispatch_fee::<
+						Target,
+						Source,
+						_,
+					>(
 						&target_client,
+						source_to_target_conversion_rate_override.clone(),
 						ESTIMATE_TARGET_TO_SOURCE_MESSAGE_FEE_METHOD,
 						TARGET_TO_SOURCE_LANE_ID,
 						claim_swap_message.clone(),
@@ -369,23 +400,27 @@ impl SwapTokens {
 
 				// send `claim_swap` message
 				let target_genesis_hash = *target_client.genesis_hash();
+				let (spec_version, transaction_version) =
+					target_client.simple_runtime_version().await?;
 				let _ = wait_until_transaction_is_finalized::<Target>(
 					target_client
 						.submit_and_watch_signed_extrinsic(
 							accounts.target_account_at_bridged_chain.clone(),
 							move |_, transaction_nonce| {
-								Bytes(
-									Target::sign_transaction(
-										target_genesis_hash,
-										&target_sign,
-										relay_substrate_client::TransactionEra::immortal(),
-										UnsignedTransaction::new(
-											send_message_call,
+								Ok(Bytes(
+									Target::sign_transaction(SignParam {
+										spec_version,
+										transaction_version,
+										genesis_hash: target_genesis_hash,
+										signer: target_sign,
+										era: relay_substrate_client::TransactionEra::immortal(),
+										unsigned: UnsignedTransaction::new(
+											send_message_call.into(),
 											transaction_nonce,
 										),
-									)
+									})?
 									.encode(),
-								)
+								))
 							},
 						)
 						.await?,
@@ -409,23 +444,27 @@ impl SwapTokens {
 				log::info!(target: "bridge", "Cancelling the swap");
 				let cancel_swap_call: CallOf<Source> =
 					pallet_bridge_token_swap::Call::cancel_swap { swap: token_swap.clone() }.into();
+				let (spec_version, transaction_version) =
+					source_client.simple_runtime_version().await?;
 				let _ = wait_until_transaction_is_finalized::<Source>(
 					source_client
 						.submit_and_watch_signed_extrinsic(
 							accounts.source_account_at_this_chain.clone(),
 							move |_, transaction_nonce| {
-								Bytes(
-									Source::sign_transaction(
-										source_genesis_hash,
-										&source_sign,
-										relay_substrate_client::TransactionEra::immortal(),
-										UnsignedTransaction::new(
-											cancel_swap_call,
+								Ok(Bytes(
+									Source::sign_transaction(SignParam {
+										spec_version,
+										transaction_version,
+										genesis_hash: source_genesis_hash,
+										signer: source_sign,
+										era: relay_substrate_client::TransactionEra::immortal(),
+										unsigned: UnsignedTransaction::new(
+											cancel_swap_call.into(),
 											transaction_nonce,
 										),
-									)
+									})?
 									.encode(),
-								)
+								))
 							},
 						)
 						.await?,
@@ -673,6 +712,7 @@ async fn read_token_swap_state<C: Chain>(
 #[cfg(test)]
 mod tests {
 	use super::*;
+	use crate::cli::{RuntimeVersionType, SourceRuntimeVersionParams, TargetRuntimeVersionParams};
 
 	#[test]
 	fn swap_tokens_millau_to_rialto_no_lock() {
@@ -706,6 +746,11 @@ mod tests {
 					source_host: "127.0.0.1".into(),
 					source_port: 9000,
 					source_secure: false,
+					source_runtime_version: SourceRuntimeVersionParams {
+						source_version_mode: RuntimeVersionType::Bundle,
+						source_spec_version: None,
+						source_transaction_version: None,
+					}
 				},
 				source_sign: SourceSigningParams {
 					source_signer: Some("//Alice".into()),
@@ -718,6 +763,11 @@ mod tests {
 					target_host: "127.0.0.1".into(),
 					target_port: 9001,
 					target_secure: false,
+					target_runtime_version: TargetRuntimeVersionParams {
+						target_version_mode: RuntimeVersionType::Bundle,
+						target_spec_version: None,
+						target_transaction_version: None,
+					}
 				},
 				target_sign: TargetSigningParams {
 					target_signer: Some("//Bob".into()),
@@ -729,6 +779,8 @@ mod tests {
 				swap_type: TokenSwapType::NoLock,
 				source_balance: Balance(8000000000),
 				target_balance: Balance(9000000000),
+				target_to_source_conversion_rate_override: None,
+				source_to_target_conversion_rate_override: None,
 			}
 		);
 	}
@@ -754,6 +806,10 @@ mod tests {
 			"//Bob",
 			"--target-balance",
 			"9000000000",
+			"--target-to-source-conversion-rate-override",
+			"metric",
+			"--source-to-target-conversion-rate-override",
+			"84.56",
 			"lock-until-block",
 			"--blocks-before-expire",
 			"1",
@@ -767,6 +823,11 @@ mod tests {
 					source_host: "127.0.0.1".into(),
 					source_port: 9000,
 					source_secure: false,
+					source_runtime_version: SourceRuntimeVersionParams {
+						source_version_mode: RuntimeVersionType::Bundle,
+						source_spec_version: None,
+						source_transaction_version: None,
+					}
 				},
 				source_sign: SourceSigningParams {
 					source_signer: Some("//Alice".into()),
@@ -779,6 +840,11 @@ mod tests {
 					target_host: "127.0.0.1".into(),
 					target_port: 9001,
 					target_secure: false,
+					target_runtime_version: TargetRuntimeVersionParams {
+						target_version_mode: RuntimeVersionType::Bundle,
+						target_spec_version: None,
+						target_transaction_version: None,
+					}
 				},
 				target_sign: TargetSigningParams {
 					target_signer: Some("//Bob".into()),
@@ -793,6 +859,10 @@ mod tests {
 				},
 				source_balance: Balance(8000000000),
 				target_balance: Balance(9000000000),
+				target_to_source_conversion_rate_override: Some(ConversionRateOverride::Metric),
+				source_to_target_conversion_rate_override: Some(ConversionRateOverride::Explicit(
+					84.56
+				)),
 			}
 		);
 	}
diff --git a/polkadot/bridges/relays/client-kusama/Cargo.toml b/polkadot/bridges/relays/client-kusama/Cargo.toml
index a48d82f641b701f2907c75f436f039f7589d373e..35c24c1089e60248315d4bedd5d38a78c9d61631 100644
--- a/polkadot/bridges/relays/client-kusama/Cargo.toml
+++ b/polkadot/bridges/relays/client-kusama/Cargo.toml
@@ -2,14 +2,14 @@
 name = "relay-kusama-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
-scale-info = { version = "1.0", features = ["derive"] }
+scale-info = { version = "2.0.1", features = ["derive"] }
 
 # Bridge dependencies
 
diff --git a/polkadot/bridges/relays/client-kusama/src/lib.rs b/polkadot/bridges/relays/client-kusama/src/lib.rs
index a93726620ff61924e4457ad90da6a2623b2e1ef1..e228f2dc24de481209c30a5b7be7508fe62807f5 100644
--- a/polkadot/bridges/relays/client-kusama/src/lib.rs
+++ b/polkadot/bridges/relays/client-kusama/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Kusama chain.
 
+use bp_messages::MessageNonce;
 use codec::Encode;
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	Chain, ChainBase, ChainWithBalances, TransactionEraOf, TransactionSignScheme,
-	UnsignedTransaction,
+	Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -44,10 +46,21 @@ impl ChainBase for Kusama {
 	type Balance = bp_kusama::Balance;
 	type Index = bp_kusama::Nonce;
 	type Signature = bp_kusama::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_kusama::Kusama::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_kusama::Kusama::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Kusama {
 	const NAME: &'static str = "Kusama";
+	const TOKEN_ID: Option<&'static str> = Some("kusama");
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_kusama::BEST_FINALIZED_KUSAMA_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_kusama::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_kusama::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -57,6 +70,24 @@ impl Chain for Kusama {
 	type WeightToFee = bp_kusama::WeightToFee;
 }
 
+impl ChainWithGrandpa for Kusama {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = bp_kusama::WITH_KUSAMA_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Kusama {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_kusama::WITH_KUSAMA_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_kusama::TO_KUSAMA_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_kusama::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_kusama::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_kusama::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
+}
+
 impl ChainWithBalances for Kusama {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		StorageKey(bp_kusama::account_info_storage_key(account_id))
@@ -68,34 +99,30 @@ impl TransactionSignScheme for Kusama {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = crate::runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::new(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			bp_kusama::SignedExtensions::new(
-				bp_kusama::VERSION,
-				era,
-				genesis_hash,
-				unsigned.nonce,
-				unsigned.tip,
+				param.spec_version,
+				param.transaction_version,
+				param.era,
+				param.genesis_hash,
+				param.unsigned.nonce,
+				param.unsigned.tip,
 			),
 		)
 		.expect("SignedExtension never fails.");
 
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		bp_kusama::UncheckedExtrinsic::new_signed(
+		Ok(bp_kusama::UncheckedExtrinsic::new_signed(
 			call,
 			sp_runtime::MultiAddress::Id(signer.into_account()),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
diff --git a/polkadot/bridges/relays/client-kusama/src/runtime.rs b/polkadot/bridges/relays/client-kusama/src/runtime.rs
index 6d0ab5462d7c8418ca8c71ea2f5815762143df4c..59a919e6cb97164a20d959c5d9de8313b694f098 100644
--- a/polkadot/bridges/relays/client-kusama/src/runtime.rs
+++ b/polkadot/bridges/relays/client-kusama/src/runtime.rs
@@ -70,6 +70,9 @@ pub enum Call {
 	/// Balances pallet.
 	#[codec(index = 4)]
 	Balances(BalancesCall),
+	/// Utility pallet.
+	#[codec(index = 24)]
+	Utility(UtilityCall),
 	/// Polkadot bridge pallet.
 	#[codec(index = 110)]
 	BridgePolkadotGrandpa(BridgePolkadotGrandpaCall),
@@ -102,6 +105,8 @@ pub enum BridgePolkadotGrandpaCall {
 	),
 	#[codec(index = 1)]
 	initialize(bp_header_chain::InitializationData<<PolkadotLike as Chain>::Header>),
+	#[codec(index = 3)]
+	set_operational(bool),
 }
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
@@ -136,6 +141,13 @@ pub enum BridgePolkadotMessagesCall {
 	),
 }
 
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum UtilityCall {
+	#[codec(index = 2)]
+	batch_all(Vec<Call>),
+}
+
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 pub enum BridgePolkadotMessagesParameter {
 	#[codec(index = 0)]
diff --git a/polkadot/bridges/relays/client-millau/Cargo.toml b/polkadot/bridges/relays/client-millau/Cargo.toml
index 49d9dade154c2d2cb6321b926eccad48459a87cb..9893243345518b3b9a11a043e1d82c3298086a00 100644
--- a/polkadot/bridges/relays/client-millau/Cargo.toml
+++ b/polkadot/bridges/relays/client-millau/Cargo.toml
@@ -2,16 +2,17 @@
 name = "relay-millau-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
 
 # Supported Chains
 
+bp-messages = { path = "../../primitives/messages" }
 bp-millau = { path = "../../primitives/chain-millau" }
 millau-runtime = { path = "../../bin/millau/runtime" }
 
diff --git a/polkadot/bridges/relays/client-millau/src/lib.rs b/polkadot/bridges/relays/client-millau/src/lib.rs
index 3f1aba1f3b372493b26d6217f206dbac66695bda..eae9d9b4586a539efdede9d1308f64c66b8844bf 100644
--- a/polkadot/bridges/relays/client-millau/src/lib.rs
+++ b/polkadot/bridges/relays/client-millau/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Millau-Substrate chain.
 
+use bp_messages::MessageNonce;
 use codec::{Compact, Decode, Encode};
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	BalanceOf, Chain, ChainBase, ChainWithBalances, IndexOf, TransactionEraOf,
-	TransactionSignScheme, UnsignedTransaction,
+	BalanceOf, Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, IndexOf, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -29,7 +31,7 @@ use std::time::Duration;
 pub type HeaderId = relay_utils::HeaderId<millau_runtime::Hash, millau_runtime::BlockNumber>;
 
 /// Millau chain definition.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
 pub struct Millau;
 
 impl ChainBase for Millau {
@@ -42,10 +44,40 @@ impl ChainBase for Millau {
 	type Balance = millau_runtime::Balance;
 	type Index = millau_runtime::Index;
 	type Signature = millau_runtime::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_millau::Millau::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_millau::Millau::max_extrinsic_weight()
+	}
+}
+
+impl ChainWithGrandpa for Millau {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = bp_millau::WITH_MILLAU_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Millau {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_millau::WITH_MILLAU_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_millau::TO_MILLAU_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_millau::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_millau::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_millau::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
 }
 
 impl Chain for Millau {
 	const NAME: &'static str = "Millau";
+	// Rialto token has no value, but we associate it with KSM token
+	const TOKEN_ID: Option<&'static str> = Some("kusama");
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_millau::BEST_FINALIZED_MILLAU_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_millau::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_millau::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -69,43 +101,40 @@ impl TransactionSignScheme for Millau {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = millau_runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::from_raw(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			(
+				frame_system::CheckNonZeroSender::<millau_runtime::Runtime>::new(),
 				frame_system::CheckSpecVersion::<millau_runtime::Runtime>::new(),
 				frame_system::CheckTxVersion::<millau_runtime::Runtime>::new(),
 				frame_system::CheckGenesis::<millau_runtime::Runtime>::new(),
-				frame_system::CheckEra::<millau_runtime::Runtime>::from(era.frame_era()),
-				frame_system::CheckNonce::<millau_runtime::Runtime>::from(unsigned.nonce),
+				frame_system::CheckEra::<millau_runtime::Runtime>::from(param.era.frame_era()),
+				frame_system::CheckNonce::<millau_runtime::Runtime>::from(param.unsigned.nonce),
 				frame_system::CheckWeight::<millau_runtime::Runtime>::new(),
-				pallet_transaction_payment::ChargeTransactionPayment::<millau_runtime::Runtime>::from(unsigned.tip),
+				pallet_transaction_payment::ChargeTransactionPayment::<millau_runtime::Runtime>::from(param.unsigned.tip),
 			),
 			(
-				millau_runtime::VERSION.spec_version,
-				millau_runtime::VERSION.transaction_version,
-				genesis_hash,
-				era.signed_payload(genesis_hash),
+				(),
+				param.spec_version,
+				param.transaction_version,
+				param.genesis_hash,
+				param.era.signed_payload(param.genesis_hash),
 				(),
 				(),
 				(),
 			),
 		);
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		millau_runtime::UncheckedExtrinsic::new_signed(
-			call,
+		Ok(millau_runtime::UncheckedExtrinsic::new_signed(
+			call.into_decoded()?,
 			signer.into_account(),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
@@ -124,9 +153,9 @@ impl TransactionSignScheme for Millau {
 	fn parse_transaction(tx: Self::SignedTransaction) -> Option<UnsignedTransaction<Self::Chain>> {
 		let extra = &tx.signature.as_ref()?.2;
 		Some(UnsignedTransaction {
-			call: tx.function,
-			nonce: Compact::<IndexOf<Self::Chain>>::decode(&mut &extra.4.encode()[..]).ok()?.into(),
-			tip: Compact::<BalanceOf<Self::Chain>>::decode(&mut &extra.6.encode()[..])
+			call: tx.function.into(),
+			nonce: Compact::<IndexOf<Self::Chain>>::decode(&mut &extra.5.encode()[..]).ok()?.into(),
+			tip: Compact::<BalanceOf<Self::Chain>>::decode(&mut &extra.7.encode()[..])
 				.ok()?
 				.into(),
 		})
@@ -138,3 +167,32 @@ pub type SigningParams = sp_core::sr25519::Pair;
 
 /// Millau header type used in headers sync.
 pub type SyncHeader = relay_substrate_client::SyncHeader<millau_runtime::Header>;
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use relay_substrate_client::TransactionEra;
+
+	#[test]
+	fn parse_transaction_works() {
+		let unsigned = UnsignedTransaction {
+			call: millau_runtime::Call::System(millau_runtime::SystemCall::remark {
+				remark: b"Hello world!".to_vec(),
+			})
+			.into(),
+			nonce: 777,
+			tip: 888,
+		};
+		let signed_transaction = Millau::sign_transaction(SignParam {
+			spec_version: 42,
+			transaction_version: 50000,
+			genesis_hash: [42u8; 64].into(),
+			signer: sp_core::sr25519::Pair::from_seed_slice(&[1u8; 32]).unwrap(),
+			era: TransactionEra::immortal(),
+			unsigned: unsigned.clone(),
+		})
+		.unwrap();
+		let parsed_transaction = Millau::parse_transaction(signed_transaction).unwrap();
+		assert_eq!(parsed_transaction, unsigned);
+	}
+}
diff --git a/polkadot/bridges/relays/client-polkadot/Cargo.toml b/polkadot/bridges/relays/client-polkadot/Cargo.toml
index ff7748657941195d962df03f2f1c8374a570cff6..96cfa2ce1bacf4cdecc65ef4c9f8b6687e25c308 100644
--- a/polkadot/bridges/relays/client-polkadot/Cargo.toml
+++ b/polkadot/bridges/relays/client-polkadot/Cargo.toml
@@ -2,14 +2,14 @@
 name = "relay-polkadot-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
-scale-info = { version = "1.0", features = ["derive"] }
+scale-info = { version = "2.0.1", features = ["derive"] }
 
 # Bridge dependencies
 
diff --git a/polkadot/bridges/relays/client-polkadot/src/lib.rs b/polkadot/bridges/relays/client-polkadot/src/lib.rs
index e6ceabf583e0bfa3e27ebbce9641d57340cbb94d..d4ada45e36cc8284ab43a60206efa74b668f6d0f 100644
--- a/polkadot/bridges/relays/client-polkadot/src/lib.rs
+++ b/polkadot/bridges/relays/client-polkadot/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Polkadot chain.
 
+use bp_messages::MessageNonce;
 use codec::Encode;
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	Chain, ChainBase, ChainWithBalances, TransactionEraOf, TransactionSignScheme,
-	UnsignedTransaction,
+	Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -44,10 +46,21 @@ impl ChainBase for Polkadot {
 	type Balance = bp_polkadot::Balance;
 	type Index = bp_polkadot::Nonce;
 	type Signature = bp_polkadot::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_polkadot::Polkadot::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_polkadot::Polkadot::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Polkadot {
 	const NAME: &'static str = "Polkadot";
+	const TOKEN_ID: Option<&'static str> = Some("polkadot");
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_polkadot::BEST_FINALIZED_POLKADOT_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_polkadot::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_polkadot::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -57,6 +70,25 @@ impl Chain for Polkadot {
 	type WeightToFee = bp_polkadot::WeightToFee;
 }
 
+impl ChainWithGrandpa for Polkadot {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str =
+		bp_polkadot::WITH_POLKADOT_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Polkadot {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_polkadot::WITH_POLKADOT_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_polkadot::TO_POLKADOT_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_polkadot::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_polkadot::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_polkadot::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
+}
+
 impl ChainWithBalances for Polkadot {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		StorageKey(bp_polkadot::account_info_storage_key(account_id))
@@ -68,34 +100,30 @@ impl TransactionSignScheme for Polkadot {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = crate::runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::new(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			bp_polkadot::SignedExtensions::new(
-				bp_polkadot::VERSION,
-				era,
-				genesis_hash,
-				unsigned.nonce,
-				unsigned.tip,
+				param.spec_version,
+				param.transaction_version,
+				param.era,
+				param.genesis_hash,
+				param.unsigned.nonce,
+				param.unsigned.tip,
 			),
 		)
 		.expect("SignedExtension never fails.");
 
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		bp_polkadot::UncheckedExtrinsic::new_signed(
+		Ok(bp_polkadot::UncheckedExtrinsic::new_signed(
 			call,
 			sp_runtime::MultiAddress::Id(signer.into_account()),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
diff --git a/polkadot/bridges/relays/client-polkadot/src/runtime.rs b/polkadot/bridges/relays/client-polkadot/src/runtime.rs
index 8b125a37843c84198d919be6298a05df27520c72..fa45115a6b5c24da5464de7cff33fa5f4884d367 100644
--- a/polkadot/bridges/relays/client-polkadot/src/runtime.rs
+++ b/polkadot/bridges/relays/client-polkadot/src/runtime.rs
@@ -70,6 +70,9 @@ pub enum Call {
 	/// Balances pallet.
 	#[codec(index = 5)]
 	Balances(BalancesCall),
+	/// Utility pallet.
+	#[codec(index = 26)]
+	Utility(UtilityCall),
 	/// Kusama bridge pallet.
 	#[codec(index = 110)]
 	BridgeKusamaGrandpa(BridgeKusamaGrandpaCall),
@@ -102,6 +105,8 @@ pub enum BridgeKusamaGrandpaCall {
 	),
 	#[codec(index = 1)]
 	initialize(bp_header_chain::InitializationData<<PolkadotLike as Chain>::Header>),
+	#[codec(index = 3)]
+	set_operational(bool),
 }
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
@@ -136,6 +141,13 @@ pub enum BridgeKusamaMessagesCall {
 	),
 }
 
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum UtilityCall {
+	#[codec(index = 2)]
+	batch_all(Vec<Call>),
+}
+
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 pub enum BridgeKusamaMessagesParameter {
 	#[codec(index = 0)]
diff --git a/polkadot/bridges/relays/client-rialto-parachain/Cargo.toml b/polkadot/bridges/relays/client-rialto-parachain/Cargo.toml
index e4518c6877652fec50440ac9b432344827733a12..ebc28560643186e8684940215b8fbbf9c87053af 100644
--- a/polkadot/bridges/relays/client-rialto-parachain/Cargo.toml
+++ b/polkadot/bridges/relays/client-rialto-parachain/Cargo.toml
@@ -2,7 +2,7 @@
 name = "relay-rialto-parachain-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
diff --git a/polkadot/bridges/relays/client-rialto-parachain/src/lib.rs b/polkadot/bridges/relays/client-rialto-parachain/src/lib.rs
index ca299a0eeb78bdbb7c11eca5859c0e7bd375a60f..65bf46f660cc784a380e3ea335f94856eab2e907 100644
--- a/polkadot/bridges/relays/client-rialto-parachain/src/lib.rs
+++ b/polkadot/bridges/relays/client-rialto-parachain/src/lib.rs
@@ -16,6 +16,7 @@
 
 //! Types used to connect to the Rialto-Substrate chain.
 
+use frame_support::weights::Weight;
 use relay_substrate_client::{Chain, ChainBase};
 use std::time::Duration;
 
@@ -37,10 +38,21 @@ impl ChainBase for RialtoParachain {
 	type Balance = rialto_parachain_runtime::Balance;
 	type Index = rialto_parachain_runtime::Index;
 	type Signature = rialto_parachain_runtime::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_rialto::Rialto::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_rialto::Rialto::max_extrinsic_weight()
+	}
 }
 
 impl Chain for RialtoParachain {
 	const NAME: &'static str = "RialtoParachain";
+	const TOKEN_ID: Option<&'static str> = None;
+	// should be fixed/changed in https://github.com/paritytech/parity-bridges-common/pull/1199
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "<UNIMPLEMENTED>";
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rialto::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
diff --git a/polkadot/bridges/relays/client-rialto/Cargo.toml b/polkadot/bridges/relays/client-rialto/Cargo.toml
index 3132b26d27fc183e8e334747888c0bb6fcf2eb03..37c55dd5f153fcba803888d131809eb0f948d3bf 100644
--- a/polkadot/bridges/relays/client-rialto/Cargo.toml
+++ b/polkadot/bridges/relays/client-rialto/Cargo.toml
@@ -2,16 +2,17 @@
 name = "relay-rialto-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
 
 # Bridge dependencies
 
+bp-messages = { path = "../../primitives/messages" }
 bp-rialto = { path = "../../primitives/chain-rialto" }
 rialto-runtime = { path = "../../bin/rialto/runtime" }
 
diff --git a/polkadot/bridges/relays/client-rialto/src/lib.rs b/polkadot/bridges/relays/client-rialto/src/lib.rs
index 42ed8bce3bd9b432726d7ba138f16668e50ebd6e..858227e8083f653f56e0b5af177dd40dd2f608af 100644
--- a/polkadot/bridges/relays/client-rialto/src/lib.rs
+++ b/polkadot/bridges/relays/client-rialto/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Rialto-Substrate chain.
 
+use bp_messages::MessageNonce;
 use codec::{Compact, Decode, Encode};
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	BalanceOf, Chain, ChainBase, ChainWithBalances, IndexOf, TransactionEraOf,
-	TransactionSignScheme, UnsignedTransaction,
+	BalanceOf, Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, IndexOf, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -29,7 +31,7 @@ use std::time::Duration;
 pub type HeaderId = relay_utils::HeaderId<rialto_runtime::Hash, rialto_runtime::BlockNumber>;
 
 /// Rialto chain definition
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq)]
 pub struct Rialto;
 
 impl ChainBase for Rialto {
@@ -42,10 +44,22 @@ impl ChainBase for Rialto {
 	type Balance = rialto_runtime::Balance;
 	type Index = rialto_runtime::Index;
 	type Signature = rialto_runtime::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_rialto::Rialto::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_rialto::Rialto::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Rialto {
 	const NAME: &'static str = "Rialto";
+	// Rialto token has no value, but we associate it with DOT token
+	const TOKEN_ID: Option<&'static str> = Some("polkadot");
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_rialto::BEST_FINALIZED_RIALTO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(5);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rialto::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_rialto::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -55,6 +69,24 @@ impl Chain for Rialto {
 	type WeightToFee = bp_rialto::WeightToFee;
 }
 
+impl ChainWithGrandpa for Rialto {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = bp_rialto::WITH_RIALTO_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Rialto {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_rialto::WITH_RIALTO_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_rialto::TO_RIALTO_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_rialto::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_rialto::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
+}
+
 impl ChainWithBalances for Rialto {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		use frame_support::storage::generator::StorageMap;
@@ -69,43 +101,40 @@ impl TransactionSignScheme for Rialto {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = rialto_runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::from_raw(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			(
+				frame_system::CheckNonZeroSender::<rialto_runtime::Runtime>::new(),
 				frame_system::CheckSpecVersion::<rialto_runtime::Runtime>::new(),
 				frame_system::CheckTxVersion::<rialto_runtime::Runtime>::new(),
 				frame_system::CheckGenesis::<rialto_runtime::Runtime>::new(),
-				frame_system::CheckEra::<rialto_runtime::Runtime>::from(era.frame_era()),
-				frame_system::CheckNonce::<rialto_runtime::Runtime>::from(unsigned.nonce),
+				frame_system::CheckEra::<rialto_runtime::Runtime>::from(param.era.frame_era()),
+				frame_system::CheckNonce::<rialto_runtime::Runtime>::from(param.unsigned.nonce),
 				frame_system::CheckWeight::<rialto_runtime::Runtime>::new(),
-				pallet_transaction_payment::ChargeTransactionPayment::<rialto_runtime::Runtime>::from(unsigned.tip),
+				pallet_transaction_payment::ChargeTransactionPayment::<rialto_runtime::Runtime>::from(param.unsigned.tip),
 			),
 			(
-				rialto_runtime::VERSION.spec_version,
-				rialto_runtime::VERSION.transaction_version,
-				genesis_hash,
-				era.signed_payload(genesis_hash),
+				(),
+				param.spec_version,
+				param.transaction_version,
+				param.genesis_hash,
+				param.era.signed_payload(param.genesis_hash),
 				(),
 				(),
 				(),
 			),
 		);
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		rialto_runtime::UncheckedExtrinsic::new_signed(
-			call,
+		Ok(rialto_runtime::UncheckedExtrinsic::new_signed(
+			call.into_decoded()?,
 			signer.into_account().into(),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
@@ -122,9 +151,9 @@ impl TransactionSignScheme for Rialto {
 	fn parse_transaction(tx: Self::SignedTransaction) -> Option<UnsignedTransaction<Self::Chain>> {
 		let extra = &tx.signature.as_ref()?.2;
 		Some(UnsignedTransaction {
-			call: tx.function,
-			nonce: Compact::<IndexOf<Self::Chain>>::decode(&mut &extra.4.encode()[..]).ok()?.into(),
-			tip: Compact::<BalanceOf<Self::Chain>>::decode(&mut &extra.6.encode()[..])
+			call: tx.function.into(),
+			nonce: Compact::<IndexOf<Self::Chain>>::decode(&mut &extra.5.encode()[..]).ok()?.into(),
+			tip: Compact::<BalanceOf<Self::Chain>>::decode(&mut &extra.7.encode()[..])
 				.ok()?
 				.into(),
 		})
@@ -136,3 +165,32 @@ pub type SigningParams = sp_core::sr25519::Pair;
 
 /// Rialto header type used in headers sync.
 pub type SyncHeader = relay_substrate_client::SyncHeader<rialto_runtime::Header>;
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use relay_substrate_client::TransactionEra;
+
+	#[test]
+	fn parse_transaction_works() {
+		let unsigned = UnsignedTransaction {
+			call: rialto_runtime::Call::System(rialto_runtime::SystemCall::remark {
+				remark: b"Hello world!".to_vec(),
+			})
+			.into(),
+			nonce: 777,
+			tip: 888,
+		};
+		let signed_transaction = Rialto::sign_transaction(SignParam {
+			spec_version: 42,
+			transaction_version: 50000,
+			genesis_hash: [42u8; 32].into(),
+			signer: sp_core::sr25519::Pair::from_seed_slice(&[1u8; 32]).unwrap(),
+			era: TransactionEra::immortal(),
+			unsigned: unsigned.clone(),
+		})
+		.unwrap();
+		let parsed_transaction = Rialto::parse_transaction(signed_transaction).unwrap();
+		assert_eq!(parsed_transaction, unsigned);
+	}
+}
diff --git a/polkadot/bridges/relays/client-rococo/Cargo.toml b/polkadot/bridges/relays/client-rococo/Cargo.toml
index 28e97d3bf0cec3226402ac704eafe75dc3d7d4ad..2b78684a853cb491c46ca8382737ed0d96a5b2de 100644
--- a/polkadot/bridges/relays/client-rococo/Cargo.toml
+++ b/polkadot/bridges/relays/client-rococo/Cargo.toml
@@ -2,14 +2,14 @@
 name = "relay-rococo-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
-scale-info = { version = "1.0", features = ["derive"] }
+scale-info = { version = "2.0.1", features = ["derive"] }
 
 # Bridge dependencies
 
diff --git a/polkadot/bridges/relays/client-rococo/src/lib.rs b/polkadot/bridges/relays/client-rococo/src/lib.rs
index ad61e3cfd6437be5cf2c964d9f3f569beda51bdd..f63041df9eddbd708f3b710827408f6149d9168a 100644
--- a/polkadot/bridges/relays/client-rococo/src/lib.rs
+++ b/polkadot/bridges/relays/client-rococo/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Rococo-Substrate chain.
 
+use bp_messages::MessageNonce;
 use codec::Encode;
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	Chain, ChainBase, ChainWithBalances, TransactionEraOf, TransactionSignScheme,
-	UnsignedTransaction,
+	Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -47,10 +49,21 @@ impl ChainBase for Rococo {
 	type Balance = bp_rococo::Balance;
 	type Index = bp_rococo::Nonce;
 	type Signature = bp_rococo::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_rococo::Rococo::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_rococo::Rococo::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Rococo {
 	const NAME: &'static str = "Rococo";
+	const TOKEN_ID: Option<&'static str> = None;
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_rococo::BEST_FINALIZED_ROCOCO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_rococo::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_rococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -60,6 +73,24 @@ impl Chain for Rococo {
 	type WeightToFee = bp_rococo::WeightToFee;
 }
 
+impl ChainWithGrandpa for Rococo {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = bp_rococo::WITH_ROCOCO_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Rococo {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_rococo::WITH_ROCOCO_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_rococo::TO_ROCOCO_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_rococo::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_rococo::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_rococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
+}
+
 impl ChainWithBalances for Rococo {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		StorageKey(bp_rococo::account_info_storage_key(account_id))
@@ -71,34 +102,30 @@ impl TransactionSignScheme for Rococo {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = crate::runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::new(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			bp_rococo::SignedExtensions::new(
-				bp_rococo::VERSION,
-				era,
-				genesis_hash,
-				unsigned.nonce,
-				unsigned.tip,
+				param.spec_version,
+				param.transaction_version,
+				param.era,
+				param.genesis_hash,
+				param.unsigned.nonce,
+				param.unsigned.tip,
 			),
 		)
 		.expect("SignedExtension never fails.");
 
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		bp_rococo::UncheckedExtrinsic::new_signed(
+		Ok(bp_rococo::UncheckedExtrinsic::new_signed(
 			call,
 			sp_runtime::MultiAddress::Id(signer.into_account()),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
diff --git a/polkadot/bridges/relays/client-rococo/src/runtime.rs b/polkadot/bridges/relays/client-rococo/src/runtime.rs
index effe6e5c60a9d87fe7163c156934a0e966d83f5c..b13808059964a2250e1c4a5edcb8b189e1632bd4 100644
--- a/polkadot/bridges/relays/client-rococo/src/runtime.rs
+++ b/polkadot/bridges/relays/client-rococo/src/runtime.rs
@@ -17,9 +17,9 @@
 //! Types that are specific to the Rococo runtime.
 
 use bp_messages::{LaneId, UnrewardedRelayersState};
-use bp_polkadot_core::PolkadotLike;
+use bp_polkadot_core::{AccountAddress, Balance, PolkadotLike};
 use bp_runtime::Chain;
-use codec::{Decode, Encode};
+use codec::{Compact, Decode, Encode};
 use frame_support::weights::Weight;
 use scale_info::TypeInfo;
 
@@ -66,12 +66,15 @@ pub enum Call {
 	/// System pallet.
 	#[codec(index = 0)]
 	System(SystemCall),
+	/// Balances pallet.
+	#[codec(index = 4)]
+	Balances(BalancesCall),
 	/// Wococo bridge pallet.
 	#[codec(index = 41)]
 	BridgeGrandpaWococo(BridgeGrandpaWococoCall),
 	/// Wococo messages pallet.
 	#[codec(index = 44)]
-	BridgeMessagesWococo(BridgeMessagesWococoCall),
+	BridgeWococoMessages(BridgeWococoMessagesCall),
 }
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
@@ -81,6 +84,13 @@ pub enum SystemCall {
 	remark(Vec<u8>),
 }
 
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum BalancesCall {
+	#[codec(index = 0)]
+	transfer(AccountAddress, Compact<Balance>),
+}
+
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 #[allow(non_camel_case_types)]
 pub enum BridgeGrandpaWococoCall {
@@ -95,7 +105,7 @@ pub enum BridgeGrandpaWococoCall {
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 #[allow(non_camel_case_types)]
-pub enum BridgeMessagesWococoCall {
+pub enum BridgeWococoMessagesCall {
 	#[codec(index = 3)]
 	send_message(
 		LaneId,
diff --git a/polkadot/bridges/relays/client-substrate/Cargo.toml b/polkadot/bridges/relays/client-substrate/Cargo.toml
index 2eb07fdcde4674c6ad286fba51f8d1f5374bc3b1..dad864965e29ed5a195daac7bbf3e8c76139f59c 100644
--- a/polkadot/bridges/relays/client-substrate/Cargo.toml
+++ b/polkadot/bridges/relays/client-substrate/Cargo.toml
@@ -2,25 +2,27 @@
 name = "relay-substrate-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
 async-std = { version = "1.6.5", features = ["attributes"] }
 async-trait = "0.1.40"
-codec = { package = "parity-scale-codec", version = "2.2.0" }
-jsonrpsee-proc-macros = "0.3.1"
-jsonrpsee-ws-client = "0.3.1"
+codec = { package = "parity-scale-codec", version = "3.0.0" }
+jsonrpsee = { version = "0.8", features = ["macros", "ws-client"] }
 log = "0.4.11"
 num-traits = "0.2"
 rand = "0.7"
-tokio = "1.8"
+serde = { version = "1.0" }
+tokio = { version = "1.8", features = ["rt-multi-thread"] }
 thiserror = "1.0.26"
 
 # Bridge dependencies
 
 bp-header-chain = { path = "../../primitives/header-chain" }
+bp-messages = { path = "../../primitives/messages" }
 bp-runtime = { path = "../../primitives/runtime" }
+pallet-bridge-messages = { path = "../../modules/messages" }
 finality-relay = { path = "../finality" }
 relay-utils = { path = "../utils" }
 
@@ -31,9 +33,10 @@ frame-system = { git = "https://github.com/paritytech/substrate", branch = "mast
 pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
 pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master" }
 pallet-transaction-payment-rpc-runtime-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
+sc-chain-spec = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-rpc-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
-sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sc-transaction-pool-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-rpc = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/relays/client-substrate/src/chain.rs b/polkadot/bridges/relays/client-substrate/src/chain.rs
index 75789ce37f308572ce96dac8d8886e4924db5672..60adfb0a88ac2dd97ddaed1974b30bd68f14a6ad 100644
--- a/polkadot/bridges/relays/client-substrate/src/chain.rs
+++ b/polkadot/bridges/relays/client-substrate/src/chain.rs
@@ -14,10 +14,11 @@
 // 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/>.
 
-use bp_runtime::{Chain as ChainBase, HashOf, TransactionEraOf};
+use bp_messages::MessageNonce;
+use bp_runtime::{Chain as ChainBase, EncodedOrDecodedCall, HashOf, TransactionEraOf};
 use codec::{Codec, Encode};
-use frame_support::weights::WeightToFeePolynomial;
-use jsonrpsee_ws_client::types::{DeserializeOwned, Serialize};
+use frame_support::weights::{Weight, WeightToFeePolynomial};
+use jsonrpsee::core::{DeserializeOwned, Serialize};
 use num_traits::Zero;
 use sc_transaction_pool_api::TransactionStatus;
 use sp_core::{storage::StorageKey, Pair};
@@ -32,6 +33,18 @@ use std::{fmt::Debug, time::Duration};
 pub trait Chain: ChainBase + Clone {
 	/// Chain name.
 	const NAME: &'static str;
+	/// Identifier of the basic token of the chain (if applicable).
+	///
+	/// This identifier is used to fetch token price. In case of testnets, you may either
+	/// set it to `None`, or associate testnet with one of the existing tokens.
+	const TOKEN_ID: Option<&'static str>;
+	/// Name of the runtime API method that is returning best known finalized header number
+	/// and hash (as tuple).
+	///
+	/// Keep in mind that this method is normally provided by the other chain, which is
+	/// bridged with this chain.
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str;
+
 	/// Average block interval.
 	///
 	/// How often blocks are produced on that chain. It's suggested to set this value
@@ -45,12 +58,54 @@ pub trait Chain: ChainBase + Clone {
 	/// Block type.
 	type SignedBlock: Member + Serialize + DeserializeOwned + BlockWithJustification<Self::Header>;
 	/// The aggregated `Call` type.
-	type Call: Clone + Dispatchable + Debug;
+	type Call: Clone + Codec + Dispatchable + Debug + Send;
 
 	/// Type that is used by the chain, to convert from weight to fee.
 	type WeightToFee: WeightToFeePolynomial<Balance = Self::Balance>;
 }
 
+/// Substrate-based chain that is using direct GRANDPA finality from minimal relay-client point of
+/// view.
+///
+/// Keep in mind that parachains are relying on relay chain GRANDPA, so they should not implement
+/// this trait.
+pub trait ChainWithGrandpa: Chain {
+	/// Name of the bridge GRANDPA pallet (used in `construct_runtime` macro call) that is deployed
+	/// at some other chain to bridge with this `ChainWithGrandpa`.
+	///
+	/// We assume that all chains that are bridging with this `ChainWithGrandpa` are using
+	/// the same name.
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str;
+}
+
+/// Substrate-based chain with messaging support from minimal relay-client point of view.
+pub trait ChainWithMessages: Chain {
+	/// Name of the bridge messages pallet (used in `construct_runtime` macro call) that is deployed
+	/// at some other chain to bridge with this `ChainWithMessages`.
+	///
+	/// We assume that all chains that are bridging with this `ChainWithMessages` are using
+	/// the same name.
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str;
+
+	/// Name of the `To<ChainWithMessages>OutboundLaneApi::message_details` runtime API method.
+	/// The method is provided by the runtime that is bridged with this `ChainWithMessages`.
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str;
+
+	/// Additional weight of the dispatch fee payment if dispatch is paid at the target chain
+	/// and this `ChainWithMessages` is the target chain.
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight;
+
+	/// Maximal number of unrewarded relayers in a single confirmation transaction at this
+	/// `ChainWithMessages`.
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce;
+	/// Maximal number of unconfirmed messages in a single confirmation transaction at this
+	/// `ChainWithMessages`.
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce;
+
+	/// Weights of message pallet calls.
+	type WeightInfo: pallet_bridge_messages::WeightInfoExt;
+}
+
 /// Call type used by the chain.
 pub type CallOf<C> = <C as Chain>::Call;
 /// Weight-to-Fee type used by the chain.
@@ -58,7 +113,7 @@ pub type WeightToFeeOf<C> = <C as Chain>::WeightToFee;
 /// Transaction status of the chain.
 pub type TransactionStatusOf<C> = TransactionStatus<HashOf<C>, HashOf<C>>;
 
-/// Substrate-based chain with `frame_system::Config::AccountData` set to
+/// Substrate-based chain with `AccountData` generic argument of `frame_system::AccountInfo` set to
 /// the `pallet_balances::AccountData<Balance>`.
 pub trait ChainWithBalances: Chain {
 	/// Return runtime storage key for getting `frame_system::AccountInfo` of given account.
@@ -79,10 +134,10 @@ pub trait BlockWithJustification<Header> {
 }
 
 /// Transaction before it is signed.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct UnsignedTransaction<C: Chain> {
 	/// Runtime call of this transaction.
-	pub call: C::Call,
+	pub call: EncodedOrDecodedCall<C::Call>,
 	/// Transaction nonce.
 	pub nonce: C::Index,
 	/// Tip included into transaction.
@@ -91,7 +146,7 @@ pub struct UnsignedTransaction<C: Chain> {
 
 impl<C: Chain> UnsignedTransaction<C> {
 	/// Create new unsigned transaction with given call, nonce and zero tip.
-	pub fn new(call: C::Call, nonce: C::Index) -> Self {
+	pub fn new(call: EncodedOrDecodedCall<C::Call>, nonce: C::Index) -> Self {
 		Self { call, nonce, tip: Zero::zero() }
 	}
 
@@ -102,6 +157,9 @@ impl<C: Chain> UnsignedTransaction<C> {
 	}
 }
 
+/// Account key pair used by transactions signing scheme.
+pub type AccountKeyPairOf<S> = <S as TransactionSignScheme>::AccountKeyPair;
+
 /// Substrate-based chain transactions signing scheme.
 pub trait TransactionSignScheme {
 	/// Chain that this scheme is to be used.
@@ -112,12 +170,9 @@ pub trait TransactionSignScheme {
 	type SignedTransaction: Clone + Debug + Codec + Send + 'static;
 
 	/// Create transaction for given runtime call, signed by given account.
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction;
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, crate::Error>
+	where
+		Self: Sized;
 
 	/// Returns true if transaction is signed.
 	fn is_signed(tx: &Self::SignedTransaction) -> bool;
@@ -131,6 +186,22 @@ pub trait TransactionSignScheme {
 	fn parse_transaction(tx: Self::SignedTransaction) -> Option<UnsignedTransaction<Self::Chain>>;
 }
 
+/// Sign transaction parameters
+pub struct SignParam<T: TransactionSignScheme> {
+	/// Version of the runtime specification.
+	pub spec_version: u32,
+	/// Transaction version
+	pub transaction_version: u32,
+	/// Hash of the genesis block.
+	pub genesis_hash: <T::Chain as ChainBase>::Hash,
+	/// Signer account
+	pub signer: T::AccountKeyPair,
+	/// Transaction era used by the chain.
+	pub era: TransactionEraOf<T::Chain>,
+	/// Transaction before it is signed.
+	pub unsigned: UnsignedTransaction<T::Chain>,
+}
+
 impl<Block: BlockT> BlockWithJustification<Block::Header> for SignedBlock<Block> {
 	fn header(&self) -> Block::Header {
 		self.block.header().clone()
diff --git a/polkadot/bridges/relays/client-substrate/src/client.rs b/polkadot/bridges/relays/client-substrate/src/client.rs
index 67bb5c1ad9ae03914e35a6b15ed8f842125f689c..1e48bc3339668cdfafe8e8165e50c232e2b37f06 100644
--- a/polkadot/bridges/relays/client-substrate/src/client.rs
+++ b/polkadot/bridges/relays/client-substrate/src/client.rs
@@ -18,8 +18,9 @@
 
 use crate::{
 	chain::{Chain, ChainWithBalances, TransactionStatusOf},
-	rpc::Substrate,
-	ConnectionParams, Error, HashOf, HeaderIdOf, Result,
+	rpc::SubstrateClient,
+	AccountIdOf, BlockNumberOf, ConnectionParams, Error, HashOf, HeaderIdOf, HeaderOf, IndexOf,
+	Result,
 };
 
 use async_std::sync::{Arc, Mutex};
@@ -27,14 +28,12 @@ use async_trait::async_trait;
 use codec::{Decode, Encode};
 use frame_system::AccountInfo;
 use futures::{SinkExt, StreamExt};
-use jsonrpsee_ws_client::{
-	types::{
-		self as jsonrpsee_types, traits::SubscriptionClient, v2::params::JsonRpcParams,
-		DeserializeOwned,
-	},
-	WsClient as RpcClient, WsClientBuilder as RpcClientBuilder,
+use jsonrpsee::{
+	core::{client::SubscriptionClientT, DeserializeOwned},
+	types::params::ParamsSer,
+	ws_client::{WsClient as RpcClient, WsClientBuilder as RpcClientBuilder},
 };
-use num_traits::{Bounded, Zero};
+use num_traits::{Bounded, CheckedSub, One, Zero};
 use pallet_balances::AccountData;
 use pallet_transaction_payment::InclusionFee;
 use relay_utils::{relay_loop::RECONNECT_DELAY, HeaderId};
@@ -60,6 +59,17 @@ pub struct Subscription<T>(Mutex<futures::channel::mpsc::Receiver<Option<T>>>);
 /// Opaque GRANDPA authorities set.
 pub type OpaqueGrandpaAuthoritiesSet = Vec<u8>;
 
+/// Chain runtime version in client
+#[derive(Clone, Debug)]
+pub enum ChainRuntimeVersion {
+	/// Auto query from chain.
+	Auto,
+	/// Custom runtime version, defined by user.
+	/// the first is `spec_version`
+	/// the second is `transaction_version`
+	Custom(u32, u32),
+}
+
 /// Substrate client type.
 ///
 /// Cloning `Client` is a cheap operation.
@@ -77,6 +87,8 @@ pub struct Client<C: Chain> {
 	/// transactions will be rejected from the pool. This lock is here to prevent situations like
 	/// that.
 	submit_signed_extrinsic_lock: Arc<Mutex<()>>,
+	/// Saved chain runtime version
+	chain_runtime_version: ChainRuntimeVersion,
 }
 
 #[async_trait]
@@ -99,6 +111,7 @@ impl<C: Chain> Clone for Client<C> {
 			client: self.client.clone(),
 			genesis_hash: self.genesis_hash,
 			submit_signed_extrinsic_lock: self.submit_signed_extrinsic_lock.clone(),
+			chain_runtime_version: self.chain_runtime_version.clone(),
 		}
 	}
 }
@@ -140,16 +153,26 @@ impl<C: Chain> Client<C> {
 		let genesis_hash_client = client.clone();
 		let genesis_hash = tokio
 			.spawn(async move {
-				Substrate::<C>::chain_get_block_hash(&*genesis_hash_client, number).await
+				SubstrateClient::<
+					AccountIdOf<C>,
+					BlockNumberOf<C>,
+					HashOf<C>,
+					HeaderOf<C>,
+					IndexOf<C>,
+					C::SignedBlock,
+				>::chain_get_block_hash(&*genesis_hash_client, Some(number))
+				.await
 			})
 			.await??;
 
+		let chain_runtime_version = params.chain_runtime_version.clone();
 		Ok(Self {
 			tokio,
 			params,
 			client,
 			genesis_hash,
 			submit_signed_extrinsic_lock: Arc::new(Mutex::new(())),
+			chain_runtime_version,
 		})
 	}
 
@@ -178,10 +201,31 @@ impl<C: Chain> Client<C> {
 }
 
 impl<C: Chain> Client<C> {
+	/// Return simple runtime version, only include `spec_version` and `transaction_version`.
+	pub async fn simple_runtime_version(&self) -> Result<(u32, u32)> {
+		let (spec_version, transaction_version) = match self.chain_runtime_version {
+			ChainRuntimeVersion::Auto => {
+				let runtime_version = self.runtime_version().await?;
+				(runtime_version.spec_version, runtime_version.transaction_version)
+			},
+			ChainRuntimeVersion::Custom(spec_version, transaction_version) =>
+				(spec_version, transaction_version),
+		};
+		Ok((spec_version, transaction_version))
+	}
+
 	/// Returns true if client is connected to at least one peer and is in synced state.
 	pub async fn ensure_synced(&self) -> Result<()> {
 		self.jsonrpsee_execute(|client| async move {
-			let health = Substrate::<C>::system_health(&*client).await?;
+			let health = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::system_health(&*client)
+			.await?;
 			let is_synced = !health.is_syncing && (!health.should_have_peers || health.peers > 0);
 			if is_synced {
 				Ok(())
@@ -200,7 +244,15 @@ impl<C: Chain> Client<C> {
 	/// Return hash of the best finalized block.
 	pub async fn best_finalized_header_hash(&self) -> Result<C::Hash> {
 		self.jsonrpsee_execute(|client| async move {
-			Ok(Substrate::<C>::chain_get_finalized_head(&*client).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::chain_get_finalized_head(&*client)
+			.await?)
 		})
 		.await
 	}
@@ -216,7 +268,15 @@ impl<C: Chain> Client<C> {
 		C::Header: DeserializeOwned,
 	{
 		self.jsonrpsee_execute(|client| async move {
-			Ok(Substrate::<C>::chain_get_header(&*client, None).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::chain_get_header(&*client, None)
+			.await?)
 		})
 		.await
 	}
@@ -224,7 +284,15 @@ impl<C: Chain> Client<C> {
 	/// Get a Substrate block from its hash.
 	pub async fn get_block(&self, block_hash: Option<C::Hash>) -> Result<C::SignedBlock> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::chain_get_block(&*client, block_hash).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::chain_get_block(&*client, block_hash)
+			.await?)
 		})
 		.await
 	}
@@ -235,7 +303,15 @@ impl<C: Chain> Client<C> {
 		C::Header: DeserializeOwned,
 	{
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::chain_get_header(&*client, block_hash).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::chain_get_header(&*client, Some(block_hash))
+			.await?)
 		})
 		.await
 	}
@@ -243,7 +319,15 @@ impl<C: Chain> Client<C> {
 	/// Get a Substrate block hash by its number.
 	pub async fn block_hash_by_number(&self, number: C::BlockNumber) -> Result<C::Hash> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::chain_get_block_hash(&*client, number).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::chain_get_block_hash(&*client, Some(number))
+			.await?)
 		})
 		.await
 	}
@@ -261,7 +345,15 @@ impl<C: Chain> Client<C> {
 	/// Return runtime version.
 	pub async fn runtime_version(&self) -> Result<RuntimeVersion> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::state_runtime_version(&*client).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_runtime_version(&*client)
+			.await?)
 		})
 		.await
 	}
@@ -287,7 +379,15 @@ impl<C: Chain> Client<C> {
 		block_hash: Option<C::Hash>,
 	) -> Result<Option<StorageData>> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::state_get_storage(&*client, storage_key, block_hash).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_get_storage(&*client, storage_key, block_hash)
+			.await?)
 		})
 		.await
 	}
@@ -299,10 +399,16 @@ impl<C: Chain> Client<C> {
 	{
 		self.jsonrpsee_execute(move |client| async move {
 			let storage_key = C::account_info_storage_key(&account);
-			let encoded_account_data =
-				Substrate::<C>::state_get_storage(&*client, storage_key, None)
-					.await?
-					.ok_or(Error::AccountDoesNotExist)?;
+			let encoded_account_data = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_get_storage(&*client, storage_key, None)
+			.await?
+			.ok_or(Error::AccountDoesNotExist)?;
 			let decoded_account_data = AccountInfo::<C::Index, AccountData<C::Balance>>::decode(
 				&mut &encoded_account_data.0[..],
 			)
@@ -317,7 +423,15 @@ impl<C: Chain> Client<C> {
 	/// Note: It's the caller's responsibility to make sure `account` is a valid SS58 address.
 	pub async fn next_account_index(&self, account: C::AccountId) -> Result<C::Index> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::system_account_next_index(&*client, account).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::system_account_next_index(&*client, account)
+			.await?)
 		})
 		.await
 	}
@@ -327,7 +441,15 @@ impl<C: Chain> Client<C> {
 	/// Note: The given transaction needs to be SCALE encoded beforehand.
 	pub async fn submit_unsigned_extrinsic(&self, transaction: Bytes) -> Result<C::Hash> {
 		self.jsonrpsee_execute(move |client| async move {
-			let tx_hash = Substrate::<C>::author_submit_extrinsic(&*client, transaction).await?;
+			let tx_hash = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::author_submit_extrinsic(&*client, transaction)
+			.await?;
 			log::trace!(target: "bridge", "Sent transaction to Substrate node: {:?}", tx_hash);
 			Ok(tx_hash)
 		})
@@ -344,15 +466,33 @@ impl<C: Chain> Client<C> {
 	pub async fn submit_signed_extrinsic(
 		&self,
 		extrinsic_signer: C::AccountId,
-		prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, C::Index) -> Bytes + Send + 'static,
+		prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, C::Index) -> Result<Bytes> + Send + 'static,
 	) -> Result<C::Hash> {
 		let _guard = self.submit_signed_extrinsic_lock.lock().await;
 		let transaction_nonce = self.next_account_index(extrinsic_signer).await?;
 		let best_header = self.best_header().await?;
-		let best_header_id = HeaderId(*best_header.number(), best_header.hash());
+
+		// By using parent of best block here, we are protecing again best-block reorganizations.
+		// E.g. transaction my have been submitted when the best block was `A[num=100]`. Then it has
+		// been changed to `B[num=100]`. Hash of `A` has been included into transaction signature
+		// payload. So when signature will be checked, the check will fail and transaction will be
+		// dropped from the pool.
+		let best_header_id = match best_header.number().checked_sub(&One::one()) {
+			Some(parent_block_number) => HeaderId(parent_block_number, *best_header.parent_hash()),
+			None => HeaderId(*best_header.number(), best_header.hash()),
+		};
+
 		self.jsonrpsee_execute(move |client| async move {
-			let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce);
-			let tx_hash = Substrate::<C>::author_submit_extrinsic(&*client, extrinsic).await?;
+			let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?;
+			let tx_hash = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::author_submit_extrinsic(&*client, extrinsic)
+			.await?;
 			log::trace!(target: "bridge", "Sent transaction to {} node: {:?}", C::NAME, tx_hash);
 			Ok(tx_hash)
 		})
@@ -364,7 +504,7 @@ impl<C: Chain> Client<C> {
 	pub async fn submit_and_watch_signed_extrinsic(
 		&self,
 		extrinsic_signer: C::AccountId,
-		prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, C::Index) -> Bytes + Send + 'static,
+		prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, C::Index) -> Result<Bytes> + Send + 'static,
 	) -> Result<Subscription<TransactionStatusOf<C>>> {
 		let _guard = self.submit_signed_extrinsic_lock.lock().await;
 		let transaction_nonce = self.next_account_index(extrinsic_signer).await?;
@@ -372,13 +512,13 @@ impl<C: Chain> Client<C> {
 		let best_header_id = HeaderId(*best_header.number(), best_header.hash());
 		let subscription = self
 			.jsonrpsee_execute(move |client| async move {
-				let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce);
+				let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?;
 				let tx_hash = C::Hasher::hash(&extrinsic.0);
 				let subscription = client
 					.subscribe(
 						"author_submitAndWatchExtrinsic",
-						JsonRpcParams::Array(vec![jsonrpsee_types::to_json_value(extrinsic)
-							.map_err(|e| Error::RpcError(e.into()))?]),
+						Some(ParamsSer::Array(vec![jsonrpsee::core::to_json_value(extrinsic)
+							.map_err(|e| Error::RpcError(e.into()))?])),
 						"author_unwatchExtrinsic",
 					)
 					.await?;
@@ -399,7 +539,15 @@ impl<C: Chain> Client<C> {
 	/// Returns pending extrinsics from transaction pool.
 	pub async fn pending_extrinsics(&self) -> Result<Vec<Bytes>> {
 		self.jsonrpsee_execute(move |client| async move {
-			Ok(Substrate::<C>::author_pending_extrinsics(&*client).await?)
+			Ok(SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::author_pending_extrinsics(&*client)
+			.await?)
 		})
 		.await
 	}
@@ -414,8 +562,15 @@ impl<C: Chain> Client<C> {
 			let call = SUB_API_TXPOOL_VALIDATE_TRANSACTION.to_string();
 			let data = Bytes((TransactionSource::External, transaction, at_block).encode());
 
-			let encoded_response =
-				Substrate::<C>::state_call(&*client, call, data, Some(at_block)).await?;
+			let encoded_response = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_call(&*client, call, data, Some(at_block))
+			.await?;
 			let validity = TransactionValidity::decode(&mut &encoded_response.0[..])
 				.map_err(Error::ResponseParseFailed)?;
 
@@ -430,8 +585,15 @@ impl<C: Chain> Client<C> {
 		transaction: Bytes,
 	) -> Result<InclusionFee<C::Balance>> {
 		self.jsonrpsee_execute(move |client| async move {
-			let fee_details =
-				Substrate::<C>::payment_query_fee_details(&*client, transaction, None).await?;
+			let fee_details = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::payment_query_fee_details(&*client, transaction, None)
+			.await?;
 			let inclusion_fee = fee_details
 				.inclusion_fee
 				.map(|inclusion_fee| InclusionFee {
@@ -463,8 +625,15 @@ impl<C: Chain> Client<C> {
 			let call = SUB_API_GRANDPA_AUTHORITIES.to_string();
 			let data = Bytes(Vec::new());
 
-			let encoded_response =
-				Substrate::<C>::state_call(&*client, call, data, Some(block)).await?;
+			let encoded_response = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_call(&*client, call, data, Some(block))
+			.await?;
 			let authority_list = encoded_response.0;
 
 			Ok(authority_list)
@@ -480,9 +649,16 @@ impl<C: Chain> Client<C> {
 		at_block: Option<C::Hash>,
 	) -> Result<Bytes> {
 		self.jsonrpsee_execute(move |client| async move {
-			Substrate::<C>::state_call(&*client, method, data, at_block)
-				.await
-				.map_err(Into::into)
+			SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_call(&*client, method, data, at_block)
+			.await
+			.map_err(Into::into)
 		})
 		.await
 	}
@@ -494,10 +670,36 @@ impl<C: Chain> Client<C> {
 		at_block: C::Hash,
 	) -> Result<StorageProof> {
 		self.jsonrpsee_execute(move |client| async move {
-			Substrate::<C>::state_prove_storage(&*client, keys, Some(at_block))
-				.await
-				.map(|proof| StorageProof::new(proof.proof.into_iter().map(|b| b.0)))
-				.map_err(Into::into)
+			SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::state_prove_storage(&*client, keys, Some(at_block))
+			.await
+			.map(|proof| {
+				StorageProof::new(proof.proof.into_iter().map(|b| b.0).collect::<Vec<_>>())
+			})
+			.map_err(Into::into)
+		})
+		.await
+	}
+
+	/// Return `tokenDecimals` property from the set of chain properties.
+	pub async fn token_decimals(&self) -> Result<Option<u64>> {
+		self.jsonrpsee_execute(move |client| async move {
+			let system_properties = SubstrateClient::<
+				AccountIdOf<C>,
+				BlockNumberOf<C>,
+				HashOf<C>,
+				HeaderOf<C>,
+				IndexOf<C>,
+				C::SignedBlock,
+			>::system_properties(&*client)
+			.await?;
+			Ok(system_properties.get("tokenDecimals").and_then(|v| v.as_u64()))
 		})
 		.await
 	}
@@ -509,7 +711,7 @@ impl<C: Chain> Client<C> {
 				Ok(client
 					.subscribe(
 						"grandpa_subscribeJustifications",
-						JsonRpcParams::NoParams,
+						None,
 						"grandpa_unsubscribeJustifications",
 					)
 					.await?)
@@ -549,32 +751,32 @@ impl<T: DeserializeOwned> Subscription<T> {
 	async fn background_worker(
 		chain_name: String,
 		item_type: String,
-		mut subscription: jsonrpsee_types::Subscription<T>,
+		mut subscription: jsonrpsee::core::client::Subscription<T>,
 		mut sender: futures::channel::mpsc::Sender<Option<T>>,
 	) {
 		loop {
 			match subscription.next().await {
-				Ok(Some(item)) =>
+				Some(Ok(item)) =>
 					if sender.send(Some(item)).await.is_err() {
 						break
 					},
-				Ok(None) => {
+				Some(Err(e)) => {
 					log::trace!(
 						target: "bridge",
-						"{} {} subscription stream has returned None. Stream needs to be restarted.",
+						"{} {} subscription stream has returned '{:?}'. Stream needs to be restarted.",
 						chain_name,
 						item_type,
+						e,
 					);
 					let _ = sender.send(None).await;
 					break
 				},
-				Err(e) => {
+				None => {
 					log::trace!(
 						target: "bridge",
-						"{} {} subscription stream has returned '{:?}'. Stream needs to be restarted.",
+						"{} {} subscription stream has returned None. Stream needs to be restarted.",
 						chain_name,
 						item_type,
-						e,
 					);
 					let _ = sender.send(None).await;
 					break
diff --git a/polkadot/bridges/relays/client-substrate/src/error.rs b/polkadot/bridges/relays/client-substrate/src/error.rs
index 33b9b22a03efe4d74f9375114cb4c5aa22663eb9..e698f2596c5fa400d400c6f600922cedf985cb22 100644
--- a/polkadot/bridges/relays/client-substrate/src/error.rs
+++ b/polkadot/bridges/relays/client-substrate/src/error.rs
@@ -16,7 +16,7 @@
 
 //! Substrate node RPC errors.
 
-use jsonrpsee_ws_client::types::Error as RpcError;
+use jsonrpsee::core::Error as RpcError;
 use relay_utils::MaybeConnectionError;
 use sc_rpc_api::system::Health;
 use sp_runtime::transaction_validity::TransactionValidityError;
@@ -51,6 +51,9 @@ pub enum Error {
 	/// The client we're connected to is not synced, so we can't rely on its state.
 	#[error("Substrate client is not synced {0}.")]
 	ClientNotSynced(Health),
+	/// The bridge pallet is halted and all transactions will be rejected.
+	#[error("Bridge pallet is halted.")]
+	BridgePalletIsHalted,
 	/// An error has happened when we have tried to parse storage proof.
 	#[error("Error when parsing storage proof: {0:?}.")]
 	StorageProofError(bp_runtime::StorageProofError),
diff --git a/polkadot/bridges/relays/client-substrate/src/guard.rs b/polkadot/bridges/relays/client-substrate/src/guard.rs
index a064e36234007785e58776f0b1da836d9f81370e..359a3f69d8a366c4f62267609187566d948ce096 100644
--- a/polkadot/bridges/relays/client-substrate/src/guard.rs
+++ b/polkadot/bridges/relays/client-substrate/src/guard.rs
@@ -64,6 +64,13 @@ pub fn abort_on_spec_version_change<C: ChainWithBalances>(
 	expected_spec_version: u32,
 ) {
 	async_std::task::spawn(async move {
+		log::info!(
+			target: "bridge-guard",
+			"Starting spec_version guard for {}. Expected spec_version: {}",
+			C::NAME,
+			expected_spec_version,
+		);
+
 		loop {
 			let actual_spec_version = env.runtime_version().await;
 			match actual_spec_version {
@@ -103,6 +110,14 @@ pub fn abort_when_account_balance_decreased<C: ChainWithBalances>(
 	const DAY: Duration = Duration::from_secs(60 * 60 * 24);
 
 	async_std::task::spawn(async move {
+		log::info!(
+			target: "bridge-guard",
+			"Starting balance guard for {}/{:?}. Maximal decrease: {:?}",
+			C::NAME,
+			account_id,
+			maximal_decrease,
+		);
+
 		let mut balances = VecDeque::new();
 
 		loop {
@@ -181,7 +196,7 @@ impl<C: ChainWithBalances> Environment<C> for Client<C> {
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use frame_support::weights::IdentityFee;
+	use frame_support::weights::{IdentityFee, Weight};
 	use futures::{
 		channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
 		future::FutureExt,
@@ -202,10 +217,19 @@ mod tests {
 		type Balance = u32;
 		type Index = u32;
 		type Signature = sp_runtime::testing::TestSignature;
+
+		fn max_extrinsic_size() -> u32 {
+			unreachable!()
+		}
+		fn max_extrinsic_weight() -> Weight {
+			unreachable!()
+		}
 	}
 
 	impl Chain for TestChain {
 		const NAME: &'static str = "Test";
+		const TOKEN_ID: Option<&'static str> = None;
+		const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "BestTestHeader";
 		const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(1);
 		const STORAGE_PROOF_OVERHEAD: u32 = 0;
 		const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = 0;
diff --git a/polkadot/bridges/relays/client-substrate/src/lib.rs b/polkadot/bridges/relays/client-substrate/src/lib.rs
index 51ddf852b9b6f3a8af19d98c317218b47a3e1b27..b3a7ec414190f7a89a35a4d727880d8aaab4d36a 100644
--- a/polkadot/bridges/relays/client-substrate/src/lib.rs
+++ b/polkadot/bridges/relays/client-substrate/src/lib.rs
@@ -24,7 +24,6 @@ mod error;
 mod rpc;
 mod sync_header;
 
-pub mod finality_source;
 pub mod guard;
 pub mod metrics;
 
@@ -32,10 +31,11 @@ use std::time::Duration;
 
 pub use crate::{
 	chain::{
-		BlockWithJustification, CallOf, Chain, ChainWithBalances, TransactionSignScheme,
-		TransactionStatusOf, UnsignedTransaction, WeightToFeeOf,
+		AccountKeyPairOf, BlockWithJustification, CallOf, Chain, ChainWithBalances,
+		ChainWithGrandpa, ChainWithMessages, SignParam, TransactionSignScheme, TransactionStatusOf,
+		UnsignedTransaction, WeightToFeeOf,
 	},
-	client::{Client, OpaqueGrandpaAuthoritiesSet, Subscription},
+	client::{ChainRuntimeVersion, Client, OpaqueGrandpaAuthoritiesSet, Subscription},
 	error::{Error, Result},
 	sync_header::SyncHeader,
 };
@@ -56,11 +56,18 @@ pub struct ConnectionParams {
 	pub port: u16,
 	/// Use secure websocket connection.
 	pub secure: bool,
+	/// Defined chain runtime version
+	pub chain_runtime_version: ChainRuntimeVersion,
 }
 
 impl Default for ConnectionParams {
 	fn default() -> Self {
-		ConnectionParams { host: "localhost".into(), port: 9944, secure: false }
+		ConnectionParams {
+			host: "localhost".into(),
+			port: 9944,
+			secure: false,
+			chain_runtime_version: ChainRuntimeVersion::Auto,
+		}
 	}
 }
 
diff --git a/polkadot/bridges/relays/client-substrate/src/metrics/float_storage_value.rs b/polkadot/bridges/relays/client-substrate/src/metrics/float_storage_value.rs
index 7dccf82b6f8e435a8402097e5b342292640eb3ab..7bb92693b38d27f42b623d323ba3e7ced8ebbda2 100644
--- a/polkadot/bridges/relays/client-substrate/src/metrics/float_storage_value.rs
+++ b/polkadot/bridges/relays/client-substrate/src/metrics/float_storage_value.rs
@@ -14,48 +14,84 @@
 // 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/>.
 
-use crate::{chain::Chain, client::Client};
+use crate::{chain::Chain, client::Client, Error as SubstrateError};
 
 use async_std::sync::{Arc, RwLock};
 use async_trait::async_trait;
 use codec::Decode;
+use num_traits::One;
 use relay_utils::metrics::{
 	metric_name, register, F64SharedRef, Gauge, Metric, PrometheusError, Registry,
 	StandaloneMetric, F64,
 };
-use sp_core::storage::StorageKey;
-use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber};
-use std::time::Duration;
+use sp_core::storage::{StorageData, StorageKey};
+use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber, FixedU128};
+use std::{marker::PhantomData, time::Duration};
 
 /// Storage value update interval (in blocks).
 const UPDATE_INTERVAL_IN_BLOCKS: u32 = 5;
 
+/// Fied-point storage value and the way it is decoded from the raw storage value.
+pub trait FloatStorageValue: 'static + Clone + Send + Sync {
+	/// Type of the value.
+	type Value: FixedPointNumber;
+	/// Try to decode value from the raw storage value.
+	fn decode(
+		&self,
+		maybe_raw_value: Option<StorageData>,
+	) -> Result<Option<Self::Value>, SubstrateError>;
+}
+
+/// Implementation of `FloatStorageValue` that expects encoded `FixedU128` value and returns `1` if
+/// value is missing from the storage.
+#[derive(Clone, Debug, Default)]
+pub struct FixedU128OrOne;
+
+impl FloatStorageValue for FixedU128OrOne {
+	type Value = FixedU128;
+
+	fn decode(
+		&self,
+		maybe_raw_value: Option<StorageData>,
+	) -> Result<Option<Self::Value>, SubstrateError> {
+		maybe_raw_value
+			.map(|raw_value| {
+				FixedU128::decode(&mut &raw_value.0[..])
+					.map_err(SubstrateError::ResponseParseFailed)
+					.map(Some)
+			})
+			.unwrap_or_else(|| Ok(Some(FixedU128::one())))
+	}
+}
+
 /// Metric that represents fixed-point runtime storage value as float gauge.
 #[derive(Clone, Debug)]
-pub struct FloatStorageValueMetric<C: Chain, T: Clone> {
+pub struct FloatStorageValueMetric<C: Chain, V: FloatStorageValue> {
+	value_converter: V,
 	client: Client<C>,
 	storage_key: StorageKey,
-	maybe_default_value: Option<T>,
 	metric: Gauge<F64>,
 	shared_value_ref: F64SharedRef,
+	_phantom: PhantomData<V>,
 }
 
-impl<C: Chain, T: Decode + FixedPointNumber> FloatStorageValueMetric<C, T> {
+impl<C: Chain, V: FloatStorageValue> FloatStorageValueMetric<C, V> {
 	/// Create new metric.
 	pub fn new(
+		value_converter: V,
 		client: Client<C>,
 		storage_key: StorageKey,
-		maybe_default_value: Option<T>,
 		name: String,
 		help: String,
 	) -> Result<Self, PrometheusError> {
 		let shared_value_ref = Arc::new(RwLock::new(None));
 		Ok(FloatStorageValueMetric {
+			value_converter,
 			client,
 			storage_key,
-			maybe_default_value,
 			metric: Gauge::new(metric_name(None, &name), help)?,
 			shared_value_ref,
+			_phantom: Default::default(),
 		})
 	}
 
@@ -65,20 +101,14 @@ impl<C: Chain, T: Decode + FixedPointNumber> FloatStorageValueMetric<C, T> {
 	}
 }
 
-impl<C: Chain, T> Metric for FloatStorageValueMetric<C, T>
-where
-	T: 'static + Decode + Send + Sync + FixedPointNumber,
-{
+impl<C: Chain, V: FloatStorageValue> Metric for FloatStorageValueMetric<C, V> {
 	fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
 		register(self.metric.clone(), registry).map(drop)
 	}
 }
 
 #[async_trait]
-impl<C: Chain, T> StandaloneMetric for FloatStorageValueMetric<C, T>
-where
-	T: 'static + Decode + Send + Sync + FixedPointNumber,
-{
+impl<C: Chain, V: FloatStorageValue> StandaloneMetric for FloatStorageValueMetric<C, V> {
 	fn update_interval(&self) -> Duration {
 		C::AVERAGE_BLOCK_INTERVAL * UPDATE_INTERVAL_IN_BLOCKS
 	}
@@ -86,16 +116,18 @@ where
 	async fn update(&self) {
 		let value = self
 			.client
-			.storage_value::<T>(self.storage_key.clone(), None)
+			.raw_storage_value(self.storage_key.clone(), None)
 			.await
-			.map(|maybe_storage_value| {
-				maybe_storage_value.or(self.maybe_default_value).map(|storage_value| {
-					storage_value.into_inner().unique_saturated_into() as f64 /
-						T::DIV.unique_saturated_into() as f64
+			.and_then(|maybe_storage_value| {
+				self.value_converter.decode(maybe_storage_value).map(|maybe_fixed_point_value| {
+					maybe_fixed_point_value.map(|fixed_point_value| {
+						fixed_point_value.into_inner().unique_saturated_into() as f64 /
+							V::Value::DIV.unique_saturated_into() as f64
+					})
 				})
 			})
-			.map_err(drop);
-		relay_utils::metrics::set_gauge_value(&self.metric, value);
+			.map_err(|e| e.to_string());
+		relay_utils::metrics::set_gauge_value(&self.metric, value.clone());
 		*self.shared_value_ref.write().await = value.ok().and_then(|x| x);
 	}
 }
diff --git a/polkadot/bridges/relays/client-substrate/src/metrics/mod.rs b/polkadot/bridges/relays/client-substrate/src/metrics/mod.rs
index 177e2a709cf2db24004a553ce4d192bbe08ec588..3b63099e0003662a98d7b1d6f5040a9bad826256 100644
--- a/polkadot/bridges/relays/client-substrate/src/metrics/mod.rs
+++ b/polkadot/bridges/relays/client-substrate/src/metrics/mod.rs
@@ -16,7 +16,7 @@
 
 //! Contains several Substrate-specific metrics that may be exposed by relay.
 
-pub use float_storage_value::FloatStorageValueMetric;
+pub use float_storage_value::{FixedU128OrOne, FloatStorageValue, FloatStorageValueMetric};
 pub use storage_proof_overhead::StorageProofOverheadMetric;
 
 mod float_storage_value;
diff --git a/polkadot/bridges/relays/client-substrate/src/rpc.rs b/polkadot/bridges/relays/client-substrate/src/rpc.rs
index efd45ebe43f36d102ee17f7f047f20a30e4c8c1a..a0172d1e550136639b8991d2f63a77131b5c3059 100644
--- a/polkadot/bridges/relays/client-substrate/src/rpc.rs
+++ b/polkadot/bridges/relays/client-substrate/src/rpc.rs
@@ -16,8 +16,7 @@
 
 //! The most generic Substrate node RPC interface.
 
-use crate::chain::Chain;
-
+use jsonrpsee::{core::RpcResult, proc_macros::rpc};
 use pallet_transaction_payment_rpc_runtime_api::FeeDetails;
 use sc_rpc_api::{state::ReadProof, system::Health};
 use sp_core::{
@@ -27,33 +26,51 @@ use sp_core::{
 use sp_rpc::number::NumberOrHex;
 use sp_version::RuntimeVersion;
 
-jsonrpsee_proc_macros::rpc_client_api! {
-	pub(crate) Substrate<C: Chain> {
-		#[rpc(method = "system_health", positional_params)]
-		fn system_health() -> Health;
-		#[rpc(method = "chain_getHeader", positional_params)]
-		fn chain_get_header(block_hash: Option<C::Hash>) -> C::Header;
-		#[rpc(method = "chain_getFinalizedHead", positional_params)]
-		fn chain_get_finalized_head() -> C::Hash;
-		#[rpc(method = "chain_getBlock", positional_params)]
-		fn chain_get_block(block_hash: Option<C::Hash>) -> C::SignedBlock;
-		#[rpc(method = "chain_getBlockHash", positional_params)]
-		fn chain_get_block_hash(block_number: Option<C::BlockNumber>) -> C::Hash;
-		#[rpc(method = "system_accountNextIndex", positional_params)]
-		fn system_account_next_index(account_id: C::AccountId) -> C::Index;
-		#[rpc(method = "author_submitExtrinsic", positional_params)]
-		fn author_submit_extrinsic(extrinsic: Bytes) -> C::Hash;
-		#[rpc(method = "author_pendingExtrinsics", positional_params)]
-		fn author_pending_extrinsics() -> Vec<Bytes>;
-		#[rpc(method = "state_call", positional_params)]
-		fn state_call(method: String, data: Bytes, at_block: Option<C::Hash>) -> Bytes;
-		#[rpc(method = "state_getStorage", positional_params)]
-		fn state_get_storage(key: StorageKey, at_block: Option<C::Hash>) -> Option<StorageData>;
-		#[rpc(method = "state_getReadProof", positional_params)]
-		fn state_prove_storage(keys: Vec<StorageKey>, hash: Option<C::Hash>) -> ReadProof<C::Hash>;
-		#[rpc(method = "state_getRuntimeVersion", positional_params)]
-		fn state_runtime_version() -> RuntimeVersion;
-		#[rpc(method = "payment_queryFeeDetails", positional_params)]
-		fn payment_query_fee_details(extrinsic: Bytes, at_block: Option<C::Hash>) -> FeeDetails<NumberOrHex>;
-	}
+#[rpc(client)]
+pub(crate) trait Substrate<AccountId, BlockNumber, Hash, Header, Index, SignedBlock> {
+	#[method(name = "system_health", param_kind = array)]
+	async fn system_health(&self) -> RpcResult<Health>;
+	#[method(name = "system_properties", param_kind = array)]
+	async fn system_properties(&self) -> RpcResult<sc_chain_spec::Properties>;
+	#[method(name = "chain_getHeader", param_kind = array)]
+	async fn chain_get_header(&self, block_hash: Option<Hash>) -> RpcResult<Header>;
+	#[method(name = "chain_getFinalizedHead", param_kind = array)]
+	async fn chain_get_finalized_head(&self) -> RpcResult<Hash>;
+	#[method(name = "chain_getBlock", param_kind = array)]
+	async fn chain_get_block(&self, block_hash: Option<Hash>) -> RpcResult<SignedBlock>;
+	#[method(name = "chain_getBlockHash", param_kind = array)]
+	async fn chain_get_block_hash(&self, block_number: Option<BlockNumber>) -> RpcResult<Hash>;
+	#[method(name = "system_accountNextIndex", param_kind = array)]
+	async fn system_account_next_index(&self, account_id: AccountId) -> RpcResult<Index>;
+	#[method(name = "author_submitExtrinsic", param_kind = array)]
+	async fn author_submit_extrinsic(&self, extrinsic: Bytes) -> RpcResult<Hash>;
+	#[method(name = "author_pendingExtrinsics", param_kind = array)]
+	async fn author_pending_extrinsics(&self) -> RpcResult<Vec<Bytes>>;
+	#[method(name = "state_call", param_kind = array)]
+	async fn state_call(
+		&self,
+		method: String,
+		data: Bytes,
+		at_block: Option<Hash>,
+	) -> RpcResult<Bytes>;
+	#[method(name = "state_getStorage", param_kind = array)]
+	async fn state_get_storage(
+		&self,
+		key: StorageKey,
+		at_block: Option<Hash>,
+	) -> RpcResult<Option<StorageData>>;
+	#[method(name = "state_getReadProof", param_kind = array)]
+	async fn state_prove_storage(
+		&self,
+		keys: Vec<StorageKey>,
+		hash: Option<Hash>,
+	) -> RpcResult<ReadProof<Hash>>;
+	#[method(name = "state_getRuntimeVersion", param_kind = array)]
+	async fn state_runtime_version(&self) -> RpcResult<RuntimeVersion>;
+	#[method(name = "payment_queryFeeDetails", param_kind = array)]
+	async fn payment_query_fee_details(
+		&self,
+		extrinsic: Bytes,
+		at_block: Option<Hash>,
+	) -> RpcResult<FeeDetails<NumberOrHex>>;
 }
diff --git a/polkadot/bridges/relays/client-substrate/src/sync_header.rs b/polkadot/bridges/relays/client-substrate/src/sync_header.rs
index ed3de6289ce01769265dff5d5493f0cf6c2fd987..e45e6b4197abbeaa0cb0d7fc8fffa4cf0af91863 100644
--- a/polkadot/bridges/relays/client-substrate/src/sync_header.rs
+++ b/polkadot/bridges/relays/client-substrate/src/sync_header.rs
@@ -44,7 +44,11 @@ impl<Header> From<Header> for SyncHeader<Header> {
 	}
 }
 
-impl<Header: HeaderT> FinalitySourceHeader<Header::Number> for SyncHeader<Header> {
+impl<Header: HeaderT> FinalitySourceHeader<Header::Hash, Header::Number> for SyncHeader<Header> {
+	fn hash(&self) -> Header::Hash {
+		self.0.hash()
+	}
+
 	fn number(&self) -> Header::Number {
 		*self.0.number()
 	}
diff --git a/polkadot/bridges/relays/client-westend/Cargo.toml b/polkadot/bridges/relays/client-westend/Cargo.toml
index 24b05c4f4836b2dab2fc1947273dfc081b98ec65..d38aa162994595a42ad06b7d8864400766003b31 100644
--- a/polkadot/bridges/relays/client-westend/Cargo.toml
+++ b/polkadot/bridges/relays/client-westend/Cargo.toml
@@ -2,11 +2,11 @@
 name = "relay-westend-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
 
@@ -16,5 +16,6 @@ bp-westend = { path = "../../primitives/chain-westend" }
 
 # Substrate Dependencies
 
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/relays/client-westend/src/lib.rs b/polkadot/bridges/relays/client-westend/src/lib.rs
index c719d6ea55364be8c09d00220d82ec6681f7419d..caf0c010c56ae98f4b3986676161db750faade9e 100644
--- a/polkadot/bridges/relays/client-westend/src/lib.rs
+++ b/polkadot/bridges/relays/client-westend/src/lib.rs
@@ -16,7 +16,8 @@
 
 //! Types used to connect to the Westend chain.
 
-use relay_substrate_client::{Chain, ChainBase, ChainWithBalances};
+use frame_support::weights::Weight;
+use relay_substrate_client::{Chain, ChainBase, ChainWithBalances, ChainWithGrandpa};
 use sp_core::storage::StorageKey;
 use std::time::Duration;
 
@@ -40,10 +41,21 @@ impl ChainBase for Westend {
 	type Balance = bp_westend::Balance;
 	type Index = bp_westend::Nonce;
 	type Signature = bp_westend::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_westend::Westend::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_westend::Westend::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Westend {
 	const NAME: &'static str = "Westend";
+	const TOKEN_ID: Option<&'static str> = None;
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_westend::BEST_FINALIZED_WESTEND_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_westend::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_westend::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -53,6 +65,11 @@ impl Chain for Westend {
 	type WeightToFee = bp_westend::WeightToFee;
 }
 
+impl ChainWithGrandpa for Westend {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str =
+		bp_westend::WITH_WESTEND_GRANDPA_PALLET_NAME;
+}
+
 impl ChainWithBalances for Westend {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		StorageKey(bp_westend::account_info_storage_key(account_id))
diff --git a/polkadot/bridges/relays/client-wococo/Cargo.toml b/polkadot/bridges/relays/client-wococo/Cargo.toml
index ea46c3c898bbbb3f22afd722242579a105d99105..6845ac34c84a079d4d2d7e4160cccc4ecd632f52 100644
--- a/polkadot/bridges/relays/client-wococo/Cargo.toml
+++ b/polkadot/bridges/relays/client-wococo/Cargo.toml
@@ -2,14 +2,14 @@
 name = "relay-wococo-client"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 relay-substrate-client = { path = "../client-substrate" }
 relay-utils = { path = "../utils" }
-scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
 
 # Bridge dependencies
 bridge-runtime-common = { path = "../../bin/runtime-common" }
diff --git a/polkadot/bridges/relays/client-wococo/src/lib.rs b/polkadot/bridges/relays/client-wococo/src/lib.rs
index d61915ec123708580ac117f4ffbabdeddddde0c8..485ca1bd62f4f853e6882b3abb85bd68d7deaa3d 100644
--- a/polkadot/bridges/relays/client-wococo/src/lib.rs
+++ b/polkadot/bridges/relays/client-wococo/src/lib.rs
@@ -16,10 +16,12 @@
 
 //! Types used to connect to the Wococo-Substrate chain.
 
+use bp_messages::MessageNonce;
 use codec::Encode;
+use frame_support::weights::Weight;
 use relay_substrate_client::{
-	Chain, ChainBase, ChainWithBalances, TransactionEraOf, TransactionSignScheme,
-	UnsignedTransaction,
+	Chain, ChainBase, ChainWithBalances, ChainWithGrandpa, ChainWithMessages,
+	Error as SubstrateError, SignParam, TransactionSignScheme, UnsignedTransaction,
 };
 use sp_core::{storage::StorageKey, Pair};
 use sp_runtime::{generic::SignedPayload, traits::IdentifyAccount};
@@ -47,10 +49,21 @@ impl ChainBase for Wococo {
 	type Balance = bp_wococo::Balance;
 	type Index = bp_wococo::Nonce;
 	type Signature = bp_wococo::Signature;
+
+	fn max_extrinsic_size() -> u32 {
+		bp_wococo::Wococo::max_extrinsic_size()
+	}
+
+	fn max_extrinsic_weight() -> Weight {
+		bp_wococo::Wococo::max_extrinsic_weight()
+	}
 }
 
 impl Chain for Wococo {
 	const NAME: &'static str = "Wococo";
+	const TOKEN_ID: Option<&'static str> = None;
+	const BEST_FINALIZED_HEADER_ID_METHOD: &'static str =
+		bp_wococo::BEST_FINALIZED_WOCOCO_HEADER_METHOD;
 	const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_secs(6);
 	const STORAGE_PROOF_OVERHEAD: u32 = bp_wococo::EXTRA_STORAGE_PROOF_SIZE;
 	const MAXIMAL_ENCODED_ACCOUNT_ID_SIZE: u32 = bp_wococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE;
@@ -60,6 +73,24 @@ impl Chain for Wococo {
 	type WeightToFee = bp_wococo::WeightToFee;
 }
 
+impl ChainWithGrandpa for Wococo {
+	const WITH_CHAIN_GRANDPA_PALLET_NAME: &'static str = bp_wococo::WITH_WOCOCO_GRANDPA_PALLET_NAME;
+}
+
+impl ChainWithMessages for Wococo {
+	const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str =
+		bp_wococo::WITH_WOCOCO_MESSAGES_PALLET_NAME;
+	const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str =
+		bp_wococo::TO_WOCOCO_MESSAGE_DETAILS_METHOD;
+	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN: Weight =
+		bp_wococo::PAY_INBOUND_DISPATCH_FEE_WEIGHT;
+	const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce =
+		bp_wococo::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
+	const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce =
+		bp_wococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
+	type WeightInfo = ();
+}
+
 impl ChainWithBalances for Wococo {
 	fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey {
 		StorageKey(bp_wococo::account_info_storage_key(account_id))
@@ -71,34 +102,30 @@ impl TransactionSignScheme for Wococo {
 	type AccountKeyPair = sp_core::sr25519::Pair;
 	type SignedTransaction = crate::runtime::UncheckedExtrinsic;
 
-	fn sign_transaction(
-		genesis_hash: <Self::Chain as ChainBase>::Hash,
-		signer: &Self::AccountKeyPair,
-		era: TransactionEraOf<Self::Chain>,
-		unsigned: UnsignedTransaction<Self::Chain>,
-	) -> Self::SignedTransaction {
+	fn sign_transaction(param: SignParam<Self>) -> Result<Self::SignedTransaction, SubstrateError> {
 		let raw_payload = SignedPayload::new(
-			unsigned.call,
+			param.unsigned.call.clone(),
 			bp_wococo::SignedExtensions::new(
-				bp_wococo::VERSION,
-				era,
-				genesis_hash,
-				unsigned.nonce,
-				unsigned.tip,
+				param.spec_version,
+				param.transaction_version,
+				param.era,
+				param.genesis_hash,
+				param.unsigned.nonce,
+				param.unsigned.tip,
 			),
 		)
 		.expect("SignedExtension never fails.");
 
-		let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-		let signer: sp_runtime::MultiSigner = signer.public().into();
+		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();
 
-		bp_wococo::UncheckedExtrinsic::new_signed(
+		Ok(bp_wococo::UncheckedExtrinsic::new_signed(
 			call,
 			sp_runtime::MultiAddress::Id(signer.into_account()),
 			signature.into(),
 			extra,
-		)
+		))
 	}
 
 	fn is_signed(tx: &Self::SignedTransaction) -> bool {
diff --git a/polkadot/bridges/relays/client-wococo/src/runtime.rs b/polkadot/bridges/relays/client-wococo/src/runtime.rs
index 91d32d1aa76f71a9dc7aefe51bbcdd662053e3f7..b28e053086b161ccbdc32adc4fff01634d2a7ea7 100644
--- a/polkadot/bridges/relays/client-wococo/src/runtime.rs
+++ b/polkadot/bridges/relays/client-wococo/src/runtime.rs
@@ -17,9 +17,9 @@
 //! Types that are specific to the Wococo runtime.
 
 use bp_messages::{LaneId, UnrewardedRelayersState};
-use bp_polkadot_core::PolkadotLike;
+use bp_polkadot_core::{AccountAddress, Balance, PolkadotLike};
 use bp_runtime::Chain;
-use codec::{Decode, Encode};
+use codec::{Compact, Decode, Encode};
 use frame_support::weights::Weight;
 use scale_info::TypeInfo;
 
@@ -66,12 +66,15 @@ pub enum Call {
 	/// System pallet.
 	#[codec(index = 0)]
 	System(SystemCall),
+	/// Balances pallet.
+	#[codec(index = 4)]
+	Balances(BalancesCall),
 	/// Rococo bridge pallet.
 	#[codec(index = 40)]
 	BridgeGrandpaRococo(BridgeGrandpaRococoCall),
 	/// Rococo messages pallet.
 	#[codec(index = 43)]
-	BridgeMessagesRococo(BridgeMessagesRococoCall),
+	BridgeRococoMessages(BridgeRococoMessagesCall),
 }
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
@@ -81,6 +84,13 @@ pub enum SystemCall {
 	remark(Vec<u8>),
 }
 
+#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
+#[allow(non_camel_case_types)]
+pub enum BalancesCall {
+	#[codec(index = 0)]
+	transfer(AccountAddress, Compact<Balance>),
+}
+
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 #[allow(non_camel_case_types)]
 pub enum BridgeGrandpaRococoCall {
@@ -95,7 +105,7 @@ pub enum BridgeGrandpaRococoCall {
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 #[allow(non_camel_case_types)]
-pub enum BridgeMessagesRococoCall {
+pub enum BridgeRococoMessagesCall {
 	#[codec(index = 3)]
 	send_message(
 		LaneId,
diff --git a/polkadot/bridges/relays/finality/Cargo.toml b/polkadot/bridges/relays/finality/Cargo.toml
index 645ac10775bafa04f2d7a86333f9fe16108217e4..cc5ae54be339c1c2f310b72998e1a8e7cf273ede 100644
--- a/polkadot/bridges/relays/finality/Cargo.toml
+++ b/polkadot/bridges/relays/finality/Cargo.toml
@@ -2,7 +2,7 @@
 name = "finality-relay"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 description = "Finality proofs relay"
 
diff --git a/polkadot/bridges/relays/finality/src/finality_loop.rs b/polkadot/bridges/relays/finality/src/finality_loop.rs
index 320b44d310f0bfdc672778acf9fd46376201c507..c29a5d5fec21f1bdbe9a17dbfbf3bfec08653d8b 100644
--- a/polkadot/bridges/relays/finality/src/finality_loop.rs
+++ b/polkadot/bridges/relays/finality/src/finality_loop.rs
@@ -29,7 +29,7 @@ use futures::{select, Future, FutureExt, Stream, StreamExt};
 use num_traits::{One, Saturating};
 use relay_utils::{
 	metrics::MetricsParams, relay_loop::Client as RelayClient, retry_backoff, FailedClient,
-	MaybeConnectionError,
+	HeaderId, MaybeConnectionError,
 };
 use std::{
 	pin::Pin,
@@ -87,7 +87,9 @@ pub trait SourceClient<P: FinalitySyncPipeline>: RelayClient {
 #[async_trait]
 pub trait TargetClient<P: FinalitySyncPipeline>: RelayClient {
 	/// Get best finalized source block number.
-	async fn best_finalized_source_block_number(&self) -> Result<P::Number, Self::Error>;
+	async fn best_finalized_source_block_id(
+		&self,
+	) -> Result<HeaderId<P::Hash, P::Number>, Self::Error>;
 
 	/// Submit header finality proof.
 	async fn submit_finality_proof(
@@ -114,7 +116,11 @@ pub async fn run<P: FinalitySyncPipeline>(
 	let exit_signal = exit_signal.shared();
 	relay_utils::relay_loop(source_client, target_client)
 		.with_metrics(metrics_params)
-		.loop_metric(SyncLoopMetrics::new(Some(&metrics_prefix::<P>()))?)?
+		.loop_metric(SyncLoopMetrics::new(
+			Some(&metrics_prefix::<P>()),
+			"source",
+			"source_at_target",
+		)?)?
 		.expose()
 		.await?
 		.run(metrics_prefix::<P>(), move |source_client, target_client, metrics| {
@@ -169,7 +175,7 @@ where
 
 /// Information about transaction that we have submitted.
 #[derive(Debug, Clone)]
-struct Transaction<Number> {
+pub(crate) struct Transaction<Number> {
 	/// Time when we have submitted this transaction.
 	pub time: Instant,
 	/// The number of the header we have submitted.
@@ -181,7 +187,7 @@ pub(crate) struct RestartableFinalityProofsStream<S> {
 	/// Flag that the stream needs to be restarted.
 	pub(crate) needs_restart: bool,
 	/// The stream itself.
-	stream: Pin<Box<S>>,
+	pub(crate) stream: Pin<Box<S>>,
 }
 
 #[cfg(test)]
@@ -192,15 +198,16 @@ impl<S> From<S> for RestartableFinalityProofsStream<S> {
 }
 
 /// Finality synchronization loop state.
-struct FinalityLoopState<'a, P: FinalitySyncPipeline, FinalityProofsStream> {
+pub(crate) struct FinalityLoopState<'a, P: FinalitySyncPipeline, FinalityProofsStream> {
 	/// Synchronization loop progress.
-	progress: &'a mut (Instant, Option<P::Number>),
+	pub(crate) progress: &'a mut (Instant, Option<P::Number>),
 	/// Finality proofs stream.
-	finality_proofs_stream: &'a mut RestartableFinalityProofsStream<FinalityProofsStream>,
+	pub(crate) finality_proofs_stream:
+		&'a mut RestartableFinalityProofsStream<FinalityProofsStream>,
 	/// Recent finality proofs that we have read from the stream.
-	recent_finality_proofs: &'a mut FinalityProofs<P>,
+	pub(crate) recent_finality_proofs: &'a mut FinalityProofs<P>,
 	/// Last transaction that we have submitted to the target node.
-	last_transaction: Option<Transaction<P::Number>>,
+	pub(crate) last_transaction: Option<Transaction<P::Number>>,
 }
 
 async fn run_until_connection_lost<P: FinalitySyncPipeline>(
@@ -280,7 +287,7 @@ async fn run_until_connection_lost<P: FinalitySyncPipeline>(
 	}
 }
 
-async fn run_loop_iteration<P, SC, TC>(
+pub(crate) async fn run_loop_iteration<P, SC, TC>(
 	source_client: &SC,
 	target_client: &TC,
 	state: FinalityLoopState<'_, P, SC::FinalityProofsStream>,
@@ -295,13 +302,31 @@ where
 	// read best source headers ids from source and target nodes
 	let best_number_at_source =
 		source_client.best_finalized_block_number().await.map_err(Error::Source)?;
-	let best_number_at_target = target_client
-		.best_finalized_source_block_number()
+	let best_id_at_target =
+		target_client.best_finalized_source_block_id().await.map_err(Error::Target)?;
+	let best_number_at_target = best_id_at_target.0;
+
+	let different_hash_at_source = ensure_same_fork::<P, _>(&best_id_at_target, source_client)
 		.await
-		.map_err(Error::Target)?;
+		.map_err(Error::Source)?;
+	let using_same_fork = different_hash_at_source.is_none();
+	if let Some(ref different_hash_at_source) = different_hash_at_source {
+		log::error!(
+			target: "bridge",
+			"Source node ({}) and pallet at target node ({}) have different headers at the same height {:?}: \
+			at-source {:?} vs at-target {:?}",
+			P::SOURCE_NAME,
+			P::TARGET_NAME,
+			best_number_at_target,
+			different_hash_at_source,
+			best_id_at_target.1,
+		);
+	}
+
 	if let Some(ref metrics_sync) = *metrics_sync {
 		metrics_sync.update_best_block_at_source(best_number_at_source);
 		metrics_sync.update_best_block_at_target(best_number_at_target);
+		metrics_sync.update_using_same_fork(using_same_fork);
 	}
 	*state.progress =
 		print_sync_progress::<P>(*state.progress, best_number_at_source, best_number_at_target);
@@ -427,6 +452,22 @@ where
 	Ok(selected_finality_proof)
 }
 
+/// Ensures that both clients are on the same fork.
+///
+/// Returns `Some(_)` with header has at the source client if headers are different.
+async fn ensure_same_fork<P: FinalitySyncPipeline, SC: SourceClient<P>>(
+	best_id_at_target: &HeaderId<P::Hash, P::Number>,
+	source_client: &SC,
+) -> Result<Option<P::Hash>, SC::Error> {
+	let header_at_source = source_client.header_and_finality_proof(best_id_at_target.0).await?.0;
+	let header_hash_at_source = header_at_source.hash();
+	Ok(if best_id_at_target.1 == header_hash_at_source {
+		None
+	} else {
+		Some(header_hash_at_source)
+	})
+}
+
 /// Finality proof that has been selected by the `read_missing_headers` function.
 pub(crate) enum SelectedFinalityProof<Header, FinalityProof> {
 	/// Mandatory header and its proof has been selected. We shall submit proof for this header.
diff --git a/polkadot/bridges/relays/finality/src/finality_loop_tests.rs b/polkadot/bridges/relays/finality/src/finality_loop_tests.rs
index e8f42593d1e36fcc76e5b851bf41f133c8f9118b..b8cb3bdb3549f0c3a7a97ddc4ee8db64e3f0052d 100644
--- a/polkadot/bridges/relays/finality/src/finality_loop_tests.rs
+++ b/polkadot/bridges/relays/finality/src/finality_loop_tests.rs
@@ -20,10 +20,12 @@
 
 use crate::{
 	finality_loop::{
-		prune_recent_finality_proofs, read_finality_proofs_from_stream, run,
-		select_better_recent_finality_proof, select_header_to_submit, FinalityProofs,
-		FinalitySyncParams, RestartableFinalityProofsStream, SourceClient, TargetClient,
+		prune_recent_finality_proofs, read_finality_proofs_from_stream, run, run_loop_iteration,
+		select_better_recent_finality_proof, select_header_to_submit, FinalityLoopState,
+		FinalityProofs, FinalitySyncParams, RestartableFinalityProofsStream, SourceClient,
+		TargetClient,
 	},
+	sync_loop_metrics::SyncLoopMetrics,
 	FinalityProof, FinalitySyncPipeline, SourceHeader,
 };
 
@@ -31,12 +33,18 @@ use async_trait::async_trait;
 use futures::{FutureExt, Stream, StreamExt};
 use parking_lot::Mutex;
 use relay_utils::{
-	metrics::MetricsParams, relay_loop::Client as RelayClient, MaybeConnectionError,
+	metrics::MetricsParams, relay_loop::Client as RelayClient, HeaderId, MaybeConnectionError,
+};
+use std::{
+	collections::HashMap,
+	pin::Pin,
+	sync::Arc,
+	time::{Duration, Instant},
 };
-use std::{collections::HashMap, pin::Pin, sync::Arc, time::Duration};
 
 type IsMandatory = bool;
 type TestNumber = u64;
+type TestHash = u64;
 
 #[derive(Debug, Clone)]
 enum TestError {
@@ -56,16 +64,20 @@ impl FinalitySyncPipeline for TestFinalitySyncPipeline {
 	const SOURCE_NAME: &'static str = "TestSource";
 	const TARGET_NAME: &'static str = "TestTarget";
 
-	type Hash = u64;
+	type Hash = TestHash;
 	type Number = TestNumber;
 	type Header = TestSourceHeader;
 	type FinalityProof = TestFinalityProof;
 }
 
 #[derive(Debug, Clone, PartialEq)]
-struct TestSourceHeader(IsMandatory, TestNumber);
+struct TestSourceHeader(IsMandatory, TestNumber, TestHash);
+
+impl SourceHeader<TestHash, TestNumber> for TestSourceHeader {
+	fn hash(&self) -> TestHash {
+		self.2
+	}
 
-impl SourceHeader<TestNumber> for TestSourceHeader {
 	fn number(&self) -> TestNumber {
 		self.1
 	}
@@ -90,7 +102,7 @@ struct ClientsData {
 	source_headers: HashMap<TestNumber, (TestSourceHeader, Option<TestFinalityProof>)>,
 	source_proofs: Vec<TestFinalityProof>,
 
-	target_best_block_number: TestNumber,
+	target_best_block_id: HeaderId<TestHash, TestNumber>,
 	target_headers: Vec<(TestSourceHeader, TestFinalityProof)>,
 }
 
@@ -152,10 +164,12 @@ impl RelayClient for TestTargetClient {
 
 #[async_trait]
 impl TargetClient<TestFinalitySyncPipeline> for TestTargetClient {
-	async fn best_finalized_source_block_number(&self) -> Result<TestNumber, TestError> {
+	async fn best_finalized_source_block_id(
+		&self,
+	) -> Result<HeaderId<TestHash, TestNumber>, TestError> {
 		let mut data = self.data.lock();
 		(self.on_method_call)(&mut *data);
-		Ok(data.target_best_block_number)
+		Ok(data.target_best_block_id)
 	}
 
 	async fn submit_finality_proof(
@@ -165,7 +179,7 @@ impl TargetClient<TestFinalitySyncPipeline> for TestTargetClient {
 	) -> Result<(), TestError> {
 		let mut data = self.data.lock();
 		(self.on_method_call)(&mut *data);
-		data.target_best_block_number = header.number();
+		data.target_best_block_id = HeaderId(header.number(), header.hash());
 		data.target_headers.push((header, proof));
 		Ok(())
 	}
@@ -187,7 +201,7 @@ fn prepare_test_clients(
 		source_headers,
 		source_proofs: vec![TestFinalityProof(12), TestFinalityProof(14)],
 
-		target_best_block_number: 5,
+		target_best_block_id: HeaderId(5, 5),
 		target_headers: vec![],
 	}));
 	(
@@ -199,6 +213,15 @@ fn prepare_test_clients(
 	)
 }
 
+fn test_sync_params() -> FinalitySyncParams {
+	FinalitySyncParams {
+		tick: Duration::from_secs(0),
+		recent_finality_proofs_limit: 1024,
+		stall_timeout: Duration::from_secs(1),
+		only_mandatory_headers: false,
+	}
+}
+
 fn run_sync_loop(
 	state_function: impl Fn(&mut ClientsData) -> bool + Send + Sync + 'static,
 ) -> ClientsData {
@@ -207,21 +230,17 @@ fn run_sync_loop(
 		exit_sender,
 		state_function,
 		vec![
-			(6, (TestSourceHeader(false, 6), None)),
-			(7, (TestSourceHeader(false, 7), Some(TestFinalityProof(7)))),
-			(8, (TestSourceHeader(true, 8), Some(TestFinalityProof(8)))),
-			(9, (TestSourceHeader(false, 9), Some(TestFinalityProof(9)))),
-			(10, (TestSourceHeader(false, 10), None)),
+			(5, (TestSourceHeader(false, 5, 5), None)),
+			(6, (TestSourceHeader(false, 6, 6), None)),
+			(7, (TestSourceHeader(false, 7, 7), Some(TestFinalityProof(7)))),
+			(8, (TestSourceHeader(true, 8, 8), Some(TestFinalityProof(8)))),
+			(9, (TestSourceHeader(false, 9, 9), Some(TestFinalityProof(9)))),
+			(10, (TestSourceHeader(false, 10, 10), None)),
 		]
 		.into_iter()
 		.collect(),
 	);
-	let sync_params = FinalitySyncParams {
-		tick: Duration::from_secs(0),
-		recent_finality_proofs_limit: 1024,
-		stall_timeout: Duration::from_secs(1),
-		only_mandatory_headers: false,
-	};
+	let sync_params = test_sync_params();
 
 	let clients_data = source_client.data.clone();
 	let _ = async_std::task::block_on(run(
@@ -246,38 +265,38 @@ fn finality_sync_loop_works() {
 		//
 		// once this ^^^ is done, we generate more blocks && read proof for blocks 12 and 14 from
 		// the stream
-		if data.target_best_block_number == 9 {
+		if data.target_best_block_id.0 == 9 {
 			data.source_best_block_number = 14;
-			data.source_headers.insert(11, (TestSourceHeader(false, 11), None));
+			data.source_headers.insert(11, (TestSourceHeader(false, 11, 11), None));
 			data.source_headers
-				.insert(12, (TestSourceHeader(false, 12), Some(TestFinalityProof(12))));
-			data.source_headers.insert(13, (TestSourceHeader(false, 13), None));
+				.insert(12, (TestSourceHeader(false, 12, 12), Some(TestFinalityProof(12))));
+			data.source_headers.insert(13, (TestSourceHeader(false, 13, 13), None));
 			data.source_headers
-				.insert(14, (TestSourceHeader(false, 14), Some(TestFinalityProof(14))));
+				.insert(14, (TestSourceHeader(false, 14, 14), Some(TestFinalityProof(14))));
 		}
 		// once this ^^^ is done, we generate more blocks && read persistent proof for block 16
-		if data.target_best_block_number == 14 {
+		if data.target_best_block_id.0 == 14 {
 			data.source_best_block_number = 17;
-			data.source_headers.insert(15, (TestSourceHeader(false, 15), None));
+			data.source_headers.insert(15, (TestSourceHeader(false, 15, 15), None));
 			data.source_headers
-				.insert(16, (TestSourceHeader(false, 16), Some(TestFinalityProof(16))));
-			data.source_headers.insert(17, (TestSourceHeader(false, 17), None));
+				.insert(16, (TestSourceHeader(false, 16, 16), Some(TestFinalityProof(16))));
+			data.source_headers.insert(17, (TestSourceHeader(false, 17, 17), None));
 		}
 
-		data.target_best_block_number == 16
+		data.target_best_block_id.0 == 16
 	});
 
 	assert_eq!(
 		client_data.target_headers,
 		vec![
 			// before adding 11..14: finality proof for mandatory header#8
-			(TestSourceHeader(true, 8), TestFinalityProof(8)),
+			(TestSourceHeader(true, 8, 8), TestFinalityProof(8)),
 			// before adding 11..14: persistent finality proof for non-mandatory header#9
-			(TestSourceHeader(false, 9), TestFinalityProof(9)),
+			(TestSourceHeader(false, 9, 9), TestFinalityProof(9)),
 			// after adding 11..14: ephemeral finality proof for non-mandatory header#14
-			(TestSourceHeader(false, 14), TestFinalityProof(14)),
+			(TestSourceHeader(false, 14, 14), TestFinalityProof(14)),
 			// after adding 15..17: persistent finality proof for non-mandatory header#16
-			(TestSourceHeader(false, 16), TestFinalityProof(16)),
+			(TestSourceHeader(false, 16, 16), TestFinalityProof(16)),
 		],
 	);
 }
@@ -291,11 +310,11 @@ fn run_only_mandatory_headers_mode_test(
 		exit_sender,
 		|_| false,
 		vec![
-			(6, (TestSourceHeader(false, 6), Some(TestFinalityProof(6)))),
-			(7, (TestSourceHeader(false, 7), Some(TestFinalityProof(7)))),
-			(8, (TestSourceHeader(has_mandatory_headers, 8), Some(TestFinalityProof(8)))),
-			(9, (TestSourceHeader(false, 9), Some(TestFinalityProof(9)))),
-			(10, (TestSourceHeader(false, 10), Some(TestFinalityProof(10)))),
+			(6, (TestSourceHeader(false, 6, 6), Some(TestFinalityProof(6)))),
+			(7, (TestSourceHeader(false, 7, 7), Some(TestFinalityProof(7)))),
+			(8, (TestSourceHeader(has_mandatory_headers, 8, 8), Some(TestFinalityProof(8)))),
+			(9, (TestSourceHeader(false, 9, 9), Some(TestFinalityProof(9)))),
+			(10, (TestSourceHeader(false, 10, 10), Some(TestFinalityProof(10)))),
 		]
 		.into_iter()
 		.collect(),
@@ -322,7 +341,7 @@ fn select_header_to_submit_skips_non_mandatory_headers_when_only_mandatory_heade
 	assert_eq!(run_only_mandatory_headers_mode_test(true, false), None);
 	assert_eq!(
 		run_only_mandatory_headers_mode_test(false, false),
-		Some((TestSourceHeader(false, 10), TestFinalityProof(10))),
+		Some((TestSourceHeader(false, 10, 10), TestFinalityProof(10))),
 	);
 }
 
@@ -330,11 +349,11 @@ fn select_header_to_submit_skips_non_mandatory_headers_when_only_mandatory_heade
 fn select_header_to_submit_selects_mandatory_headers_when_only_mandatory_headers_are_required() {
 	assert_eq!(
 		run_only_mandatory_headers_mode_test(true, true),
-		Some((TestSourceHeader(true, 8), TestFinalityProof(8))),
+		Some((TestSourceHeader(true, 8, 8), TestFinalityProof(8))),
 	);
 	assert_eq!(
 		run_only_mandatory_headers_mode_test(false, true),
-		Some((TestSourceHeader(true, 8), TestFinalityProof(8))),
+		Some((TestSourceHeader(true, 8, 8), TestFinalityProof(8))),
 	);
 }
 
@@ -345,63 +364,74 @@ fn select_better_recent_finality_proof_works() {
 		select_better_recent_finality_proof::<TestFinalitySyncPipeline>(
 			&[(5, TestFinalityProof(5))],
 			&mut vec![],
-			Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+			Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 		),
-		Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+		Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 	);
 
 	// if there are no recent finality proofs, nothing is changed
 	assert_eq!(
 		select_better_recent_finality_proof::<TestFinalitySyncPipeline>(
 			&[],
-			&mut vec![TestSourceHeader(false, 5)],
-			Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+			&mut vec![TestSourceHeader(false, 5, 5)],
+			Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 		),
-		Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+		Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 	);
 
 	// if there's no intersection between recent finality proofs and unjustified headers, nothing is
 	// changed
-	let mut unjustified_headers = vec![TestSourceHeader(false, 9), TestSourceHeader(false, 10)];
+	let mut unjustified_headers =
+		vec![TestSourceHeader(false, 9, 9), TestSourceHeader(false, 10, 10)];
 	assert_eq!(
 		select_better_recent_finality_proof::<TestFinalitySyncPipeline>(
 			&[(1, TestFinalityProof(1)), (4, TestFinalityProof(4))],
 			&mut unjustified_headers,
-			Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+			Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 		),
-		Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+		Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 	);
 
 	// if there's intersection between recent finality proofs and unjustified headers, but there are
 	// no proofs in this intersection, nothing is changed
-	let mut unjustified_headers =
-		vec![TestSourceHeader(false, 8), TestSourceHeader(false, 9), TestSourceHeader(false, 10)];
+	let mut unjustified_headers = vec![
+		TestSourceHeader(false, 8, 8),
+		TestSourceHeader(false, 9, 9),
+		TestSourceHeader(false, 10, 10),
+	];
 	assert_eq!(
 		select_better_recent_finality_proof::<TestFinalitySyncPipeline>(
 			&[(7, TestFinalityProof(7)), (11, TestFinalityProof(11))],
 			&mut unjustified_headers,
-			Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+			Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 		),
-		Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+		Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 	);
 	assert_eq!(
 		unjustified_headers,
-		vec![TestSourceHeader(false, 8), TestSourceHeader(false, 9), TestSourceHeader(false, 10)]
+		vec![
+			TestSourceHeader(false, 8, 8),
+			TestSourceHeader(false, 9, 9),
+			TestSourceHeader(false, 10, 10)
+		]
 	);
 
 	// if there's intersection between recent finality proofs and unjustified headers and there's
 	// a proof in this intersection:
 	// - this better (last from intersection) proof is selected;
 	// - 'obsolete' unjustified headers are pruned.
-	let mut unjustified_headers =
-		vec![TestSourceHeader(false, 8), TestSourceHeader(false, 9), TestSourceHeader(false, 10)];
+	let mut unjustified_headers = vec![
+		TestSourceHeader(false, 8, 8),
+		TestSourceHeader(false, 9, 9),
+		TestSourceHeader(false, 10, 10),
+	];
 	assert_eq!(
 		select_better_recent_finality_proof::<TestFinalitySyncPipeline>(
 			&[(7, TestFinalityProof(7)), (9, TestFinalityProof(9))],
 			&mut unjustified_headers,
-			Some((TestSourceHeader(false, 2), TestFinalityProof(2))),
+			Some((TestSourceHeader(false, 2, 2), TestFinalityProof(2))),
 		),
-		Some((TestSourceHeader(false, 9), TestFinalityProof(9))),
+		Some((TestSourceHeader(false, 9, 9), TestFinalityProof(9))),
 	);
 }
 
@@ -475,3 +505,45 @@ fn prune_recent_finality_proofs_works() {
 	prune_recent_finality_proofs::<TestFinalitySyncPipeline>(20, &mut recent_finality_proofs, 2);
 	assert_eq!(&original_recent_finality_proofs[5..], recent_finality_proofs);
 }
+
+#[test]
+fn different_forks_at_source_and_at_target_are_detected() {
+	let (exit_sender, _exit_receiver) = futures::channel::mpsc::unbounded();
+	let (source_client, target_client) = prepare_test_clients(
+		exit_sender,
+		|_| false,
+		vec![
+			(5, (TestSourceHeader(false, 5, 42), None)),
+			(6, (TestSourceHeader(false, 6, 6), None)),
+			(7, (TestSourceHeader(false, 7, 7), None)),
+			(8, (TestSourceHeader(false, 8, 8), None)),
+			(9, (TestSourceHeader(false, 9, 9), None)),
+			(10, (TestSourceHeader(false, 10, 10), None)),
+		]
+		.into_iter()
+		.collect(),
+	);
+
+	let mut progress = (Instant::now(), None);
+	let mut finality_proofs_stream = RestartableFinalityProofsStream {
+		needs_restart: false,
+		stream: Box::pin(futures::stream::iter(vec![]).boxed()),
+	};
+	let mut recent_finality_proofs = Vec::new();
+	let metrics_sync = SyncLoopMetrics::new(None, "source", "target").unwrap();
+	async_std::task::block_on(run_loop_iteration::<TestFinalitySyncPipeline, _, _>(
+		&source_client,
+		&target_client,
+		FinalityLoopState {
+			progress: &mut progress,
+			finality_proofs_stream: &mut finality_proofs_stream,
+			recent_finality_proofs: &mut recent_finality_proofs,
+			last_transaction: None,
+		},
+		&test_sync_params(),
+		&Some(metrics_sync.clone()),
+	))
+	.unwrap();
+
+	assert!(!metrics_sync.is_using_same_fork());
+}
diff --git a/polkadot/bridges/relays/finality/src/lib.rs b/polkadot/bridges/relays/finality/src/lib.rs
index 6421d13b787c8463c9a89e9cca3db8e15144cc81..49be64ff74db2a3254776f5e4170d2cbe0678561 100644
--- a/polkadot/bridges/relays/finality/src/lib.rs
+++ b/polkadot/bridges/relays/finality/src/lib.rs
@@ -19,8 +19,9 @@
 //! are still submitted to the target node, but are treated as auxiliary data as we are not trying
 //! to submit all source headers to the target node.
 
-pub use crate::finality_loop::{
-	metrics_prefix, run, FinalitySyncParams, SourceClient, TargetClient,
+pub use crate::{
+	finality_loop::{metrics_prefix, run, FinalitySyncParams, SourceClient, TargetClient},
+	sync_loop_metrics::SyncLoopMetrics,
 };
 
 use bp_header_chain::FinalityProof;
@@ -42,13 +43,15 @@ pub trait FinalitySyncPipeline: 'static + Clone + Debug + Send + Sync {
 	/// Headers we're syncing are identified by this number.
 	type Number: relay_utils::BlockNumberBase;
 	/// Type of header that we're syncing.
-	type Header: SourceHeader<Self::Number>;
+	type Header: SourceHeader<Self::Hash, Self::Number>;
 	/// Finality proof type.
 	type FinalityProof: FinalityProof<Self::Number>;
 }
 
 /// Header that we're receiving from source node.
-pub trait SourceHeader<Number>: Clone + Debug + PartialEq + Send + Sync {
+pub trait SourceHeader<Hash, Number>: Clone + Debug + PartialEq + Send + Sync {
+	/// Returns hash of header.
+	fn hash(&self) -> Hash;
 	/// Returns number of header.
 	fn number(&self) -> Number;
 	/// Returns true if this header needs to be submitted to target node.
diff --git a/polkadot/bridges/relays/finality/src/sync_loop_metrics.rs b/polkadot/bridges/relays/finality/src/sync_loop_metrics.rs
index 1f65dac17c05eae0d81f5934a4250b8beef3fbbb..a003a47d890960a28596837861d48d10de6e1a44 100644
--- a/polkadot/bridges/relays/finality/src/sync_loop_metrics.rs
+++ b/polkadot/bridges/relays/finality/src/sync_loop_metrics.rs
@@ -16,49 +16,71 @@
 
 //! Metrics for headers synchronization relay loop.
 
-use relay_utils::metrics::{
-	metric_name, register, GaugeVec, Metric, Opts, PrometheusError, Registry, U64,
-};
+use relay_utils::metrics::{metric_name, register, IntGauge, Metric, PrometheusError, Registry};
 
 /// Headers sync metrics.
 #[derive(Clone)]
 pub struct SyncLoopMetrics {
-	/// Best syncing headers at "source" and "target" nodes.
-	best_block_numbers: GaugeVec<U64>,
+	/// Best syncing header at the source.
+	best_source_block_number: IntGauge,
+	/// Best syncing header at the target.
+	best_target_block_number: IntGauge,
+	/// Flag that has `0` value when best source headers at the source node and at-target-chain
+	/// are matching and `1` otherwise.
+	using_different_forks: IntGauge,
 }
 
 impl SyncLoopMetrics {
 	/// Create and register headers loop metrics.
-	pub fn new(prefix: Option<&str>) -> Result<Self, PrometheusError> {
+	pub fn new(
+		prefix: Option<&str>,
+		at_source_chain_label: &str,
+		at_target_chain_label: &str,
+	) -> Result<Self, PrometheusError> {
 		Ok(SyncLoopMetrics {
-			best_block_numbers: GaugeVec::new(
-				Opts::new(
-					metric_name(prefix, "best_block_numbers"),
-					"Best block numbers on source and target nodes",
-				),
-				&["node"],
+			best_source_block_number: IntGauge::new(
+				metric_name(prefix, &format!("best_{}_block_number", at_source_chain_label)),
+				format!("Best block number at the {}", at_source_chain_label),
+			)?,
+			best_target_block_number: IntGauge::new(
+				metric_name(prefix, &format!("best_{}_block_number", at_target_chain_label)),
+				format!("Best block number at the {}", at_target_chain_label),
+			)?,
+			using_different_forks: IntGauge::new(
+				metric_name(prefix, &format!("is_{}_and_{}_using_different_forks", at_source_chain_label, at_target_chain_label)),
+				"Whether the best finalized source block at target node is different (value 1) from the \
+				corresponding block at the source node",
 			)?,
 		})
 	}
 
+	/// Returns current value of the using-same-fork flag.
+	#[cfg(test)]
+	pub(crate) fn is_using_same_fork(&self) -> bool {
+		self.using_different_forks.get() == 0
+	}
+
 	/// Update best block number at source.
 	pub fn update_best_block_at_source<Number: Into<u64>>(&self, source_best_number: Number) {
-		self.best_block_numbers
-			.with_label_values(&["source"])
-			.set(source_best_number.into());
+		self.best_source_block_number.set(source_best_number.into());
 	}
 
 	/// Update best block number at target.
 	pub fn update_best_block_at_target<Number: Into<u64>>(&self, target_best_number: Number) {
-		self.best_block_numbers
-			.with_label_values(&["target"])
-			.set(target_best_number.into());
+		self.best_target_block_number.set(target_best_number.into());
+	}
+
+	/// Update using-same-fork flag.
+	pub fn update_using_same_fork(&self, using_same_fork: bool) {
+		self.using_different_forks.set(if using_same_fork { 0 } else { 1 })
 	}
 }
 
 impl Metric for SyncLoopMetrics {
 	fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
-		register(self.best_block_numbers.clone(), registry)?;
+		register(self.best_source_block_number.clone(), registry)?;
+		register(self.best_target_block_number.clone(), registry)?;
+		register(self.using_different_forks.clone(), registry)?;
 		Ok(())
 	}
 }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/Cargo.toml b/polkadot/bridges/relays/lib-substrate-relay/Cargo.toml
index 5bee10856daa3c64adf32fef6cb62107bcd6ce3b..e2cabf52f449b397c1fbf385988a57bab45e2ad4 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/Cargo.toml
+++ b/polkadot/bridges/relays/lib-substrate-relay/Cargo.toml
@@ -2,7 +2,7 @@
 name = "substrate-relay-helper"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -10,23 +10,23 @@ anyhow = "1.0"
 thiserror = "1.0.26"
 async-std = "1.9.0"
 async-trait = "0.1.42"
-codec = { package = "parity-scale-codec", version = "2.2.0" }
+codec = { package = "parity-scale-codec", version = "3.0.0" }
 futures = "0.3.12"
 num-traits = "0.2"
 log = "0.4.14"
 
-
 # Bridge dependencies
 
 bp-header-chain = { path = "../../primitives/header-chain" }
 bridge-runtime-common = { path = "../../bin/runtime-common" }
 
-finality-grandpa = { version = "0.14.0" }
+finality-grandpa = { version = "0.15.0" }
 finality-relay = { path = "../finality" }
 relay-utils = { path = "../utils" }
 messages-relay = { path = "../messages" }
 relay-substrate-client = { path = "../client-substrate" }
 
+pallet-bridge-grandpa = { path = "../../modules/grandpa" }
 pallet-bridge-messages = { path = "../../modules/messages" }
 
 bp-runtime = { path = "../../primitives/runtime" }
@@ -35,14 +35,18 @@ bp-messages = { path = "../../primitives/messages" }
 # Substrate Dependencies
 
 frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" }
+frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" }
+pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-finality-grandpa = { git = "https://github.com/paritytech/substrate", branch = "master" }
 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
 
 [dev-dependencies]
 bp-millau = { path = "../../primitives/chain-millau" }
+bp-rialto = { path = "../../primitives/chain-rialto" }
 bp-rococo = { path = "../../primitives/chain-rococo" }
 bp-wococo = { path = "../../primitives/chain-wococo" }
+pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master" }
 relay-rococo-client = { path = "../client-rococo" }
 relay-wococo-client = { path = "../client-wococo" }
 rialto-runtime = { path = "../../bin/rialto/runtime" }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs b/polkadot/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs
index 93458457d34c9dc4213aa71817d8cb6f73ef6a76..469bc5589932b54ba76a80984f5bc857e4da4a52 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/conversion_rate_update.rs
@@ -16,39 +16,143 @@
 
 //! Tools for updating conversion rate that is stored in the runtime storage.
 
+use crate::{messages_lane::SubstrateMessageLane, TransactionParams};
+
+use codec::Encode;
+use relay_substrate_client::{
+	transaction_stall_timeout, AccountIdOf, AccountKeyPairOf, CallOf, Chain, Client, SignParam,
+	TransactionEra, TransactionSignScheme, UnsignedTransaction,
+};
 use relay_utils::metrics::F64SharedRef;
-use std::{future::Future, time::Duration};
+use sp_core::{Bytes, Pair};
+use std::time::{Duration, Instant};
 
 /// Duration between updater iterations.
 const SLEEP_DURATION: Duration = Duration::from_secs(60);
 
+/// Duration which will almost never expire. Since changing conversion rate may require manual
+/// intervention (e.g. if call is made through `multisig` pallet), we don't want relayer to
+/// resubmit transaction often.
+const ALMOST_NEVER_DURATION: Duration = Duration::from_secs(60 * 60 * 24 * 30);
+
 /// Update-conversion-rate transaction status.
 #[derive(Debug, Clone, Copy, PartialEq)]
 enum TransactionStatus {
 	/// We have not submitted any transaction recently.
 	Idle,
 	/// We have recently submitted transaction that should update conversion rate.
-	Submitted(f64),
+	Submitted(Instant, f64),
+}
+
+/// Different ways of building 'update conversion rate' calls.
+pub trait UpdateConversionRateCallBuilder<C: Chain> {
+	/// Given conversion rate, build call that updates conversion rate in given chain runtime
+	/// storage.
+	fn build_update_conversion_rate_call(conversion_rate: f64) -> anyhow::Result<CallOf<C>>;
+}
+
+impl<C: Chain> UpdateConversionRateCallBuilder<C> for () {
+	fn build_update_conversion_rate_call(_conversion_rate: f64) -> anyhow::Result<CallOf<C>> {
+		Err(anyhow::format_err!("Conversion rate update is not supported at {}", C::NAME))
+	}
+}
+
+/// Macro that generates `UpdateConversionRateCallBuilder` implementation for the case when
+/// you have a direct access to the source chain runtime.
+#[rustfmt::skip]
+#[macro_export]
+macro_rules! generate_direct_update_conversion_rate_call_builder {
+	(
+		$source_chain:ident,
+		$mocked_builder:ident,
+		$runtime:ty,
+		$instance:ty,
+		$parameter:path
+	) => {
+		pub struct $mocked_builder;
+
+		impl $crate::conversion_rate_update::UpdateConversionRateCallBuilder<$source_chain>
+			for $mocked_builder
+		{
+			fn build_update_conversion_rate_call(
+				conversion_rate: f64,
+			) -> anyhow::Result<relay_substrate_client::CallOf<$source_chain>> {
+				Ok(pallet_bridge_messages::Call::update_pallet_parameter::<$runtime, $instance> {
+					parameter: $parameter(sp_runtime::FixedU128::from_float(conversion_rate)),
+				}.into())
+			}
+		}
+	};
+}
+
+/// Macro that generates `UpdateConversionRateCallBuilder` implementation for the case when
+/// you only have an access to the mocked version of source chain runtime. In this case you
+/// should provide "name" of the call variant for the bridge messages calls, the "name" of
+/// the variant for the `update_pallet_parameter` call within that first option and the name
+/// of the conversion rate parameter itself.
+#[rustfmt::skip]
+#[macro_export]
+macro_rules! generate_mocked_update_conversion_rate_call_builder {
+	(
+		$source_chain:ident,
+		$mocked_builder:ident,
+		$bridge_messages:path,
+		$update_pallet_parameter:path,
+		$parameter:path
+	) => {
+		pub struct $mocked_builder;
+
+		impl $crate::conversion_rate_update::UpdateConversionRateCallBuilder<$source_chain>
+			for $mocked_builder
+		{
+			fn build_update_conversion_rate_call(
+				conversion_rate: f64,
+			) -> anyhow::Result<relay_substrate_client::CallOf<$source_chain>> {
+				Ok($bridge_messages($update_pallet_parameter($parameter(
+					sp_runtime::FixedU128::from_float(conversion_rate),
+				))))
+			}
+		}
+	};
 }
 
 /// Run infinite conversion rate updater loop.
 ///
 /// The loop is maintaining the Left -> Right conversion rate, used as `RightTokens = LeftTokens *
 /// Rate`.
-pub fn run_conversion_rate_update_loop<
-	SubmitConversionRateFuture: Future<Output = anyhow::Result<()>> + Send + 'static,
->(
+pub fn run_conversion_rate_update_loop<Lane, Sign>(
+	client: Client<Lane::SourceChain>,
+	transaction_params: TransactionParams<AccountKeyPairOf<Sign>>,
 	left_to_right_stored_conversion_rate: F64SharedRef,
 	left_to_base_conversion_rate: F64SharedRef,
 	right_to_base_conversion_rate: F64SharedRef,
 	max_difference_ratio: f64,
-	submit_conversion_rate: impl Fn(f64) -> SubmitConversionRateFuture + Send + 'static,
-) {
+) where
+	Lane: SubstrateMessageLane,
+	Sign: TransactionSignScheme<Chain = Lane::SourceChain>,
+	AccountIdOf<Lane::SourceChain>: From<<AccountKeyPairOf<Sign> as Pair>::Public>,
+{
+	let stall_timeout = transaction_stall_timeout(
+		transaction_params.mortality,
+		Lane::SourceChain::AVERAGE_BLOCK_INTERVAL,
+		ALMOST_NEVER_DURATION,
+	);
+
+	log::info!(
+		target: "bridge",
+		"Starting {} -> {} conversion rate  (on {}) update loop. Stall timeout: {}s",
+		Lane::TargetChain::NAME,
+		Lane::SourceChain::NAME,
+		Lane::SourceChain::NAME,
+		stall_timeout.as_secs(),
+	);
+
 	async_std::task::spawn(async move {
 		let mut transaction_status = TransactionStatus::Idle;
 		loop {
 			async_std::task::sleep(SLEEP_DURATION).await;
 			let maybe_new_conversion_rate = maybe_select_new_conversion_rate(
+				stall_timeout,
 				&mut transaction_status,
 				&left_to_right_stored_conversion_rate,
 				&left_to_base_conversion_rate,
@@ -57,13 +161,32 @@ pub fn run_conversion_rate_update_loop<
 			)
 			.await;
 			if let Some((prev_conversion_rate, new_conversion_rate)) = maybe_new_conversion_rate {
-				let submit_conversion_rate_future = submit_conversion_rate(new_conversion_rate);
-				match submit_conversion_rate_future.await {
+				log::info!(
+					target: "bridge",
+					"Going to update {} -> {} (on {}) conversion rate to {}.",
+					Lane::TargetChain::NAME,
+					Lane::SourceChain::NAME,
+					Lane::SourceChain::NAME,
+					new_conversion_rate,
+				);
+
+				let result = update_target_to_source_conversion_rate::<Lane, Sign>(
+					client.clone(),
+					transaction_params.clone(),
+					new_conversion_rate,
+				)
+				.await;
+				match result {
 					Ok(()) => {
-						transaction_status = TransactionStatus::Submitted(prev_conversion_rate);
+						transaction_status =
+							TransactionStatus::Submitted(Instant::now(), prev_conversion_rate);
 					},
 					Err(error) => {
-						log::trace!(target: "bridge", "Failed to submit conversion rate update transaction: {:?}", error);
+						log::error!(
+							target: "bridge",
+							"Failed to submit conversion rate update transaction: {:?}",
+							error,
+						);
 					},
 				}
 			}
@@ -73,6 +196,7 @@ pub fn run_conversion_rate_update_loop<
 
 /// Select new conversion rate to submit to the node.
 async fn maybe_select_new_conversion_rate(
+	stall_timeout: Duration,
 	transaction_status: &mut TransactionStatus,
 	left_to_right_stored_conversion_rate: &F64SharedRef,
 	left_to_base_conversion_rate: &F64SharedRef,
@@ -83,7 +207,18 @@ async fn maybe_select_new_conversion_rate(
 		(*left_to_right_stored_conversion_rate.read().await)?;
 	match *transaction_status {
 		TransactionStatus::Idle => (),
-		TransactionStatus::Submitted(previous_left_to_right_stored_conversion_rate) => {
+		TransactionStatus::Submitted(submitted_at, _)
+			if Instant::now() - submitted_at > stall_timeout =>
+		{
+			log::error!(
+				target: "bridge",
+				"Conversion rate update transaction has been lost and loop stalled. Restarting",
+			);
+
+			// we assume that our transaction has been lost
+			*transaction_status = TransactionStatus::Idle;
+		},
+		TransactionStatus::Submitted(_, previous_left_to_right_stored_conversion_rate) => {
 			// we can't compare float values from different sources directly, so we only care
 			// whether the stored rate has been changed or not. If it has been changed, then we
 			// assume that our proposal has been accepted.
@@ -106,7 +241,7 @@ async fn maybe_select_new_conversion_rate(
 	let left_to_base_conversion_rate = (*left_to_base_conversion_rate.read().await)?;
 	let right_to_base_conversion_rate = (*right_to_base_conversion_rate.read().await)?;
 	let actual_left_to_right_conversion_rate =
-		right_to_base_conversion_rate / left_to_base_conversion_rate;
+		left_to_base_conversion_rate / right_to_base_conversion_rate;
 
 	let rate_difference =
 		(actual_left_to_right_conversion_rate - left_to_right_stored_conversion_rate).abs();
@@ -118,11 +253,50 @@ async fn maybe_select_new_conversion_rate(
 	Some((left_to_right_stored_conversion_rate, actual_left_to_right_conversion_rate))
 }
 
+/// Update Target -> Source tokens conversion rate, stored in the Source runtime storage.
+pub async fn update_target_to_source_conversion_rate<Lane, Sign>(
+	client: Client<Lane::SourceChain>,
+	transaction_params: TransactionParams<AccountKeyPairOf<Sign>>,
+	updated_rate: f64,
+) -> anyhow::Result<()>
+where
+	Lane: SubstrateMessageLane,
+	Sign: TransactionSignScheme<Chain = Lane::SourceChain>,
+	AccountIdOf<Lane::SourceChain>: From<<AccountKeyPairOf<Sign> as Pair>::Public>,
+{
+	let genesis_hash = *client.genesis_hash();
+	let signer_id = transaction_params.signer.public().into();
+	let (spec_version, transaction_version) = client.simple_runtime_version().await?;
+	let call =
+		Lane::TargetToSourceChainConversionRateUpdateBuilder::build_update_conversion_rate_call(
+			updated_rate,
+		)?;
+	client
+		.submit_signed_extrinsic(signer_id, move |best_block_id, transaction_nonce| {
+			Ok(Bytes(
+				Sign::sign_transaction(SignParam {
+					spec_version,
+					transaction_version,
+					genesis_hash,
+					signer: transaction_params.signer,
+					era: TransactionEra::new(best_block_id, transaction_params.mortality),
+					unsigned: UnsignedTransaction::new(call.into(), transaction_nonce).into(),
+				})?
+				.encode(),
+			))
+		})
+		.await
+		.map(drop)
+		.map_err(|err| anyhow::format_err!("{:?}", err))
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
 	use async_std::sync::{Arc, RwLock};
 
+	const TEST_STALL_TIMEOUT: Duration = Duration::from_secs(60);
+
 	fn test_maybe_select_new_conversion_rate(
 		mut transaction_status: TransactionStatus,
 		stored_conversion_rate: Option<f64>,
@@ -134,6 +308,7 @@ mod tests {
 		let left_to_base_conversion_rate = Arc::new(RwLock::new(left_to_base_conversion_rate));
 		let right_to_base_conversion_rate = Arc::new(RwLock::new(right_to_base_conversion_rate));
 		let result = async_std::task::block_on(maybe_select_new_conversion_rate(
+			TEST_STALL_TIMEOUT,
 			&mut transaction_status,
 			&stored_conversion_rate,
 			&left_to_base_conversion_rate,
@@ -145,15 +320,10 @@ mod tests {
 
 	#[test]
 	fn rate_is_not_updated_when_transaction_is_submitted() {
+		let status = TransactionStatus::Submitted(Instant::now(), 10.0);
 		assert_eq!(
-			test_maybe_select_new_conversion_rate(
-				TransactionStatus::Submitted(10.0),
-				Some(10.0),
-				Some(1.0),
-				Some(1.0),
-				0.0
-			),
-			(None, TransactionStatus::Submitted(10.0)),
+			test_maybe_select_new_conversion_rate(status, Some(10.0), Some(1.0), Some(1.0), 0.0),
+			(None, status),
 		);
 	}
 
@@ -161,7 +331,7 @@ mod tests {
 	fn transaction_state_is_changed_to_idle_when_stored_rate_shanges() {
 		assert_eq!(
 			test_maybe_select_new_conversion_rate(
-				TransactionStatus::Submitted(1.0),
+				TransactionStatus::Submitted(Instant::now(), 1.0),
 				Some(10.0),
 				Some(1.0),
 				Some(1.0),
@@ -229,15 +399,42 @@ mod tests {
 
 	#[test]
 	fn transaction_is_submitted_when_difference_is_above_threshold() {
+		let left_to_right_stored_conversion_rate = 1.0;
+		let left_to_base_conversion_rate = 18f64;
+		let right_to_base_conversion_rate = 180f64;
+
+		assert!(left_to_base_conversion_rate < right_to_base_conversion_rate);
+
 		assert_eq!(
 			test_maybe_select_new_conversion_rate(
 				TransactionStatus::Idle,
-				Some(1.0),
-				Some(1.0),
-				Some(1.03),
+				Some(left_to_right_stored_conversion_rate),
+				Some(left_to_base_conversion_rate),
+				Some(right_to_base_conversion_rate),
 				0.02
 			),
-			(Some((1.0, 1.03)), TransactionStatus::Idle),
+			(
+				Some((
+					left_to_right_stored_conversion_rate,
+					left_to_base_conversion_rate / right_to_base_conversion_rate,
+				)),
+				TransactionStatus::Idle
+			),
+		);
+	}
+
+	#[test]
+	fn transaction_expires() {
+		let status = TransactionStatus::Submitted(Instant::now() - TEST_STALL_TIMEOUT / 2, 10.0);
+		assert_eq!(
+			test_maybe_select_new_conversion_rate(status, Some(10.0), Some(1.0), Some(1.0), 0.0),
+			(None, status),
+		);
+
+		let status = TransactionStatus::Submitted(Instant::now() - TEST_STALL_TIMEOUT * 2, 10.0);
+		assert_eq!(
+			test_maybe_select_new_conversion_rate(status, Some(10.0), Some(1.0), Some(1.0), 0.0),
+			(Some((10.0, 1.0)), TransactionStatus::Idle),
 		);
 	}
 }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/error.rs b/polkadot/bridges/relays/lib-substrate-relay/src/error.rs
index 802499503563dcbee09a6fa058520880bdc8d918..9402d55e379815467d2f1586d9c09df806ee72cb 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/error.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/error.rs
@@ -55,4 +55,7 @@ pub enum Error<Hash: Debug + MaybeDisplay, HeaderNumber: Debug + MaybeDisplay> {
 	/// Failed to retrieve header by the hash from the source chain.
 	#[error("Failed to retrieve {0} header with hash {1}: {:?}")]
 	RetrieveHeader(&'static str, Hash, client::Error),
+	/// Failed to retrieve best finalized source header hash from the target chain.
+	#[error("Failed to retrieve best finalized {0} header from the target chain: {1}")]
+	RetrieveBestFinalizedHeaderHash(&'static str, client::Error),
 }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/finality_guards.rs b/polkadot/bridges/relays/lib-substrate-relay/src/finality_guards.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a3e69afe1b1d908fbd559b88393e7b706380177c
--- /dev/null
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/finality_guards.rs
@@ -0,0 +1,48 @@
+// 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/>.
+
+//! Tools for starting guards of finality relays.
+
+use crate::TransactionParams;
+
+use relay_substrate_client::{
+	AccountIdOf, AccountKeyPairOf, ChainWithBalances, TransactionSignScheme,
+};
+use sp_core::Pair;
+
+/// Start finality relay guards.
+pub async fn start<C: ChainWithBalances, S: TransactionSignScheme<Chain = C>>(
+	target_client: &relay_substrate_client::Client<C>,
+	transaction_params: &TransactionParams<S::AccountKeyPair>,
+	enable_version_guard: bool,
+	maximal_balance_decrease_per_day: C::Balance,
+) -> relay_substrate_client::Result<()>
+where
+	AccountIdOf<C>: From<<AccountKeyPairOf<S> as Pair>::Public>,
+{
+	if enable_version_guard {
+		relay_substrate_client::guard::abort_on_spec_version_change(
+			target_client.clone(),
+			target_client.simple_runtime_version().await?.0,
+		);
+	}
+	relay_substrate_client::guard::abort_when_account_balance_decreased(
+		target_client.clone(),
+		transaction_params.signer.public().into(),
+		maximal_balance_decrease_per_day,
+	);
+	Ok(())
+}
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/finality_pipeline.rs b/polkadot/bridges/relays/lib-substrate-relay/src/finality_pipeline.rs
index cdfbb3354d27412853992e9fd0a9e7c3d66cc88d..3daf8d11440eb8283951fdecdf2d6fa05cd52f4c 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/finality_pipeline.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/finality_pipeline.rs
@@ -14,18 +14,24 @@
 // 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/>.
 
-//! Substrate-to-Substrate headers sync entrypoint.
+//! Types and functions intended to ease adding of new Substrate -> Substrate
+//! finality proofs synchronization pipelines.
 
-use crate::{finality_target::SubstrateFinalityTarget, STALL_TIMEOUT};
+use crate::{
+	finality_source::SubstrateFinalitySource, finality_target::SubstrateFinalityTarget,
+	TransactionParams,
+};
 
+use async_trait::async_trait;
 use bp_header_chain::justification::GrandpaJustification;
-use bp_runtime::AccountIdOf;
-use finality_relay::{FinalitySyncParams, FinalitySyncPipeline};
+use finality_relay::FinalitySyncPipeline;
+use pallet_bridge_grandpa::{Call as BridgeGrandpaCall, Config as BridgeGrandpaConfig};
 use relay_substrate_client::{
-	finality_source::FinalitySource, BlockNumberOf, Chain, Client, HashOf, SyncHeader,
+	transaction_stall_timeout, AccountIdOf, AccountKeyPairOf, BlockNumberOf, CallOf, Chain,
+	ChainWithGrandpa, Client, HashOf, HeaderOf, SyncHeader, TransactionSignScheme,
 };
-use relay_utils::{metrics::MetricsParams, BlockNumberBase};
-use sp_core::Bytes;
+use relay_utils::metrics::MetricsParams;
+use sp_core::Pair;
 use std::{fmt::Debug, marker::PhantomData};
 
 /// Default limit of recent finality proofs.
@@ -34,130 +40,146 @@ use std::{fmt::Debug, marker::PhantomData};
 /// Substrate+GRANDPA based chains (good to know).
 pub(crate) const RECENT_FINALITY_PROOFS_LIMIT: usize = 4096;
 
-/// Headers sync pipeline for Substrate <-> Substrate relays.
+/// Substrate -> Substrate finality proofs synchronization pipeline.
+#[async_trait]
 pub trait SubstrateFinalitySyncPipeline: 'static + Clone + Debug + Send + Sync {
-	/// Pipeline for syncing finalized Source chain headers to Target chain.
-	type FinalitySyncPipeline: FinalitySyncPipeline;
-
-	/// Name of the runtime method that returns id of best finalized source header at target chain.
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str;
-
-	/// Chain with GRANDPA bridge pallet.
+	/// Headers of this chain are submitted to the `TargetChain`.
+	type SourceChain: ChainWithGrandpa;
+	/// Headers of the `SourceChain` are submitted to this chain.
 	type TargetChain: Chain;
 
-	/// Customize metrics exposed by headers sync loop.
-	fn customize_metrics(params: MetricsParams) -> anyhow::Result<MetricsParams> {
-		Ok(params)
+	/// How submit finality proof call is built?
+	type SubmitFinalityProofCallBuilder: SubmitFinalityProofCallBuilder<Self>;
+	/// Scheme used to sign target chain transactions.
+	type TransactionSignScheme: TransactionSignScheme;
+
+	/// Add relay guards if required.
+	async fn start_relay_guards(
+		_target_client: &Client<Self::TargetChain>,
+		_transaction_params: &TransactionParams<AccountKeyPairOf<Self::TransactionSignScheme>>,
+		_enable_version_guard: bool,
+	) -> relay_substrate_client::Result<()> {
+		Ok(())
 	}
+}
 
-	/// Start finality relay guards.
-	///
-	/// Different finality bridges may have different set of guards - e.g. on ephemeral chains we
-	/// don't need a version guards, on test chains we don't care that much about relayer account
-	/// balance, ... So the implementation is left to the specific bridges.
-	fn start_relay_guards(&self) {}
-
-	/// Returns id of account that we're using to sign transactions at target chain.
-	fn transactions_author(&self) -> AccountIdOf<Self::TargetChain>;
-
-	/// Make submit header transaction.
-	fn make_submit_finality_proof_transaction(
-		&self,
-		era: bp_runtime::TransactionEraOf<Self::TargetChain>,
-		transaction_nonce: bp_runtime::IndexOf<Self::TargetChain>,
-		header: <Self::FinalitySyncPipeline as FinalitySyncPipeline>::Header,
-		proof: <Self::FinalitySyncPipeline as FinalitySyncPipeline>::FinalityProof,
-	) -> Bytes;
+/// Adapter that allows all `SubstrateFinalitySyncPipeline` to act as `FinalitySyncPipeline`.
+#[derive(Clone, Debug)]
+pub struct FinalitySyncPipelineAdapter<P: SubstrateFinalitySyncPipeline> {
+	_phantom: PhantomData<P>,
 }
 
-/// Substrate-to-Substrate finality proof pipeline.
-#[derive(Clone)]
-pub struct SubstrateFinalityToSubstrate<SourceChain, TargetChain: Chain, TargetSign> {
-	/// Client for the target chain.
-	pub target_client: Client<TargetChain>,
-	/// Data required to sign target chain transactions.
-	pub target_sign: TargetSign,
-	/// Unused generic arguments dump.
-	_marker: PhantomData<SourceChain>,
+impl<P: SubstrateFinalitySyncPipeline> FinalitySyncPipeline for FinalitySyncPipelineAdapter<P> {
+	const SOURCE_NAME: &'static str = P::SourceChain::NAME;
+	const TARGET_NAME: &'static str = P::TargetChain::NAME;
+
+	type Hash = HashOf<P::SourceChain>;
+	type Number = BlockNumberOf<P::SourceChain>;
+	type Header = relay_substrate_client::SyncHeader<HeaderOf<P::SourceChain>>;
+	type FinalityProof = GrandpaJustification<HeaderOf<P::SourceChain>>;
 }
 
-impl<SourceChain, TargetChain: Chain, TargetSign> Debug
-	for SubstrateFinalityToSubstrate<SourceChain, TargetChain, TargetSign>
-{
-	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-		f.debug_struct("SubstrateFinalityToSubstrate")
-			.field("target_client", &self.target_client)
-			.finish()
-	}
+/// Different ways of building `submit_finality_proof` calls.
+pub trait SubmitFinalityProofCallBuilder<P: SubstrateFinalitySyncPipeline> {
+	/// Given source chain header and its finality proofs, build call of `submit_finality_proof`
+	/// function of bridge GRANDPA module at the target chain.
+	fn build_submit_finality_proof_call(
+		header: SyncHeader<HeaderOf<P::SourceChain>>,
+		proof: GrandpaJustification<HeaderOf<P::SourceChain>>,
+	) -> CallOf<P::TargetChain>;
 }
 
-impl<SourceChain, TargetChain: Chain, TargetSign>
-	SubstrateFinalityToSubstrate<SourceChain, TargetChain, TargetSign>
-{
-	/// Create new Substrate-to-Substrate headers pipeline.
-	pub fn new(target_client: Client<TargetChain>, target_sign: TargetSign) -> Self {
-		SubstrateFinalityToSubstrate { target_client, target_sign, _marker: Default::default() }
-	}
+/// Building `submit_finality_proof` call when you have direct access to the target
+/// chain runtime.
+pub struct DirectSubmitFinalityProofCallBuilder<P, R, I> {
+	_phantom: PhantomData<(P, R, I)>,
 }
 
-impl<SourceChain, TargetChain, TargetSign> FinalitySyncPipeline
-	for SubstrateFinalityToSubstrate<SourceChain, TargetChain, TargetSign>
+impl<P, R, I> SubmitFinalityProofCallBuilder<P> for DirectSubmitFinalityProofCallBuilder<P, R, I>
 where
-	SourceChain: Clone + Chain + Debug,
-	BlockNumberOf<SourceChain>: BlockNumberBase,
-	TargetChain: Clone + Chain + Debug,
-	TargetSign: 'static + Clone + Send + Sync,
+	P: SubstrateFinalitySyncPipeline,
+	R: BridgeGrandpaConfig<I>,
+	I: 'static,
+	R::BridgedChain: bp_runtime::Chain<Header = HeaderOf<P::SourceChain>>,
+	CallOf<P::TargetChain>: From<BridgeGrandpaCall<R, I>>,
 {
-	const SOURCE_NAME: &'static str = SourceChain::NAME;
-	const TARGET_NAME: &'static str = TargetChain::NAME;
+	fn build_submit_finality_proof_call(
+		header: SyncHeader<HeaderOf<P::SourceChain>>,
+		proof: GrandpaJustification<HeaderOf<P::SourceChain>>,
+	) -> CallOf<P::TargetChain> {
+		BridgeGrandpaCall::<R, I>::submit_finality_proof {
+			finality_target: Box::new(header.into_inner()),
+			justification: proof,
+		}
+		.into()
+	}
+}
 
-	type Hash = HashOf<SourceChain>;
-	type Number = BlockNumberOf<SourceChain>;
-	type Header = SyncHeader<SourceChain::Header>;
-	type FinalityProof = GrandpaJustification<SourceChain::Header>;
+/// Macro that generates `SubmitFinalityProofCallBuilder` implementation for the case when
+/// you only have an access to the mocked version of target chain runtime. In this case you
+/// should provide "name" of the call variant for the bridge GRANDPA calls and the "name" of
+/// the variant for the `submit_finality_proof` call within that first option.
+#[rustfmt::skip]
+#[macro_export]
+macro_rules! generate_mocked_submit_finality_proof_call_builder {
+	($pipeline:ident, $mocked_builder:ident, $bridge_grandpa:path, $submit_finality_proof:path) => {
+		pub struct $mocked_builder;
+
+		impl $crate::finality_pipeline::SubmitFinalityProofCallBuilder<$pipeline>
+			for $mocked_builder
+		{
+			fn build_submit_finality_proof_call(
+				header: relay_substrate_client::SyncHeader<
+					relay_substrate_client::HeaderOf<
+						<$pipeline as $crate::finality_pipeline::SubstrateFinalitySyncPipeline>::SourceChain
+					>
+				>,
+				proof: bp_header_chain::justification::GrandpaJustification<
+					relay_substrate_client::HeaderOf<
+						<$pipeline as $crate::finality_pipeline::SubstrateFinalitySyncPipeline>::SourceChain
+					>
+				>,
+			) -> relay_substrate_client::CallOf<
+				<$pipeline as $crate::finality_pipeline::SubstrateFinalitySyncPipeline>::TargetChain
+			> {
+				$bridge_grandpa($submit_finality_proof(Box::new(header.into_inner()), proof))
+			}
+		}
+	};
 }
 
-/// Run Substrate-to-Substrate finality sync.
-pub async fn run<SourceChain, TargetChain, P>(
-	pipeline: P,
-	source_client: Client<SourceChain>,
-	target_client: Client<TargetChain>,
+/// Run Substrate-to-Substrate finality sync loop.
+pub async fn run<P: SubstrateFinalitySyncPipeline>(
+	source_client: Client<P::SourceChain>,
+	target_client: Client<P::TargetChain>,
 	only_mandatory_headers: bool,
-	transactions_mortality: Option<u32>,
+	transaction_params: TransactionParams<AccountKeyPairOf<P::TransactionSignScheme>>,
 	metrics_params: MetricsParams,
 ) -> anyhow::Result<()>
 where
-	P: SubstrateFinalitySyncPipeline<TargetChain = TargetChain>,
-	P::FinalitySyncPipeline: FinalitySyncPipeline<
-		Hash = HashOf<SourceChain>,
-		Number = BlockNumberOf<SourceChain>,
-		Header = SyncHeader<SourceChain::Header>,
-		FinalityProof = GrandpaJustification<SourceChain::Header>,
-	>,
-	SourceChain: Clone + Chain,
-	BlockNumberOf<SourceChain>: BlockNumberBase,
-	TargetChain: Clone + Chain,
+	AccountIdOf<P::TargetChain>: From<<AccountKeyPairOf<P::TransactionSignScheme> as Pair>::Public>,
+	P::TransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
 {
 	log::info!(
 		target: "bridge",
 		"Starting {} -> {} finality proof relay",
-		SourceChain::NAME,
-		TargetChain::NAME,
+		P::SourceChain::NAME,
+		P::TargetChain::NAME,
 	);
 
 	finality_relay::run(
-		FinalitySource::new(source_client, None),
-		SubstrateFinalityTarget::new(target_client, pipeline, transactions_mortality),
-		FinalitySyncParams {
+		SubstrateFinalitySource::<P>::new(source_client, None),
+		SubstrateFinalityTarget::<P>::new(target_client, transaction_params.clone()),
+		finality_relay::FinalitySyncParams {
 			tick: std::cmp::max(
-				SourceChain::AVERAGE_BLOCK_INTERVAL,
-				TargetChain::AVERAGE_BLOCK_INTERVAL,
+				P::SourceChain::AVERAGE_BLOCK_INTERVAL,
+				P::TargetChain::AVERAGE_BLOCK_INTERVAL,
 			),
 			recent_finality_proofs_limit: RECENT_FINALITY_PROOFS_LIMIT,
-			stall_timeout: relay_substrate_client::transaction_stall_timeout(
-				transactions_mortality,
-				TargetChain::AVERAGE_BLOCK_INTERVAL,
-				STALL_TIMEOUT,
+			stall_timeout: transaction_stall_timeout(
+				transaction_params.mortality,
+				P::TargetChain::AVERAGE_BLOCK_INTERVAL,
+				crate::STALL_TIMEOUT,
 			),
 			only_mandatory_headers,
 		},
diff --git a/polkadot/bridges/relays/client-substrate/src/finality_source.rs b/polkadot/bridges/relays/lib-substrate-relay/src/finality_source.rs
similarity index 66%
rename from polkadot/bridges/relays/client-substrate/src/finality_source.rs
rename to polkadot/bridges/relays/lib-substrate-relay/src/finality_source.rs
index 98526de178cb3de5654d61da1b0a1f0737a495a6..804d3212930d441260a6973eebe4bdc45af1b342 100644
--- a/polkadot/bridges/relays/client-substrate/src/finality_source.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/finality_source.rs
@@ -16,49 +16,59 @@
 
 //! Default generic implementation of finality source for basic Substrate client.
 
-use crate::{
-	chain::{BlockWithJustification, Chain},
-	client::Client,
-	error::Error,
-	sync_header::SyncHeader,
-};
+use crate::finality_pipeline::{FinalitySyncPipelineAdapter, SubstrateFinalitySyncPipeline};
 
 use async_std::sync::{Arc, Mutex};
 use async_trait::async_trait;
 use bp_header_chain::justification::GrandpaJustification;
 use codec::Decode;
-use finality_relay::{FinalitySyncPipeline, SourceClient, SourceHeader};
+use finality_relay::SourceClient;
 use futures::stream::{unfold, Stream, StreamExt};
+use relay_substrate_client::{
+	BlockNumberOf, BlockWithJustification, Chain, Client, Error, HeaderOf,
+};
 use relay_utils::relay_loop::Client as RelayClient;
 use sp_runtime::traits::Header as HeaderT;
-use std::{marker::PhantomData, pin::Pin};
+use std::pin::Pin;
 
 /// Shared updatable reference to the maximal header number that we want to sync from the source.
 pub type RequiredHeaderNumberRef<C> = Arc<Mutex<<C as bp_runtime::Chain>::BlockNumber>>;
 
+/// Substrate finality proofs stream.
+pub type SubstrateFinalityProofsStream<P> = Pin<
+	Box<
+		dyn Stream<
+				Item = GrandpaJustification<
+					HeaderOf<<P as SubstrateFinalitySyncPipeline>::SourceChain>,
+				>,
+			> + Send,
+	>,
+>;
+
 /// Substrate node as finality source.
-pub struct FinalitySource<C: Chain, P> {
-	client: Client<C>,
-	maximal_header_number: Option<RequiredHeaderNumberRef<C>>,
-	_phantom: PhantomData<P>,
+pub struct SubstrateFinalitySource<P: SubstrateFinalitySyncPipeline> {
+	client: Client<P::SourceChain>,
+	maximal_header_number: Option<RequiredHeaderNumberRef<P::SourceChain>>,
 }
 
-impl<C: Chain, P> FinalitySource<C, P> {
+impl<P: SubstrateFinalitySyncPipeline> SubstrateFinalitySource<P> {
 	/// Create new headers source using given client.
 	pub fn new(
-		client: Client<C>,
-		maximal_header_number: Option<RequiredHeaderNumberRef<C>>,
+		client: Client<P::SourceChain>,
+		maximal_header_number: Option<RequiredHeaderNumberRef<P::SourceChain>>,
 	) -> Self {
-		FinalitySource { client, maximal_header_number, _phantom: Default::default() }
+		SubstrateFinalitySource { client, maximal_header_number }
 	}
 
 	/// Returns reference to the underlying RPC client.
-	pub fn client(&self) -> &Client<C> {
+	pub fn client(&self) -> &Client<P::SourceChain> {
 		&self.client
 	}
 
 	/// Returns best finalized block number.
-	pub async fn on_chain_best_finalized_block_number(&self) -> Result<C::BlockNumber, Error> {
+	pub async fn on_chain_best_finalized_block_number(
+		&self,
+	) -> Result<BlockNumberOf<P::SourceChain>, Error> {
 		// we **CAN** continue to relay finality proofs if source node is out of sync, because
 		// target node may be missing proofs that are already available at the source
 		let finalized_header_hash = self.client.best_finalized_header_hash().await?;
@@ -67,18 +77,17 @@ impl<C: Chain, P> FinalitySource<C, P> {
 	}
 }
 
-impl<C: Chain, P> Clone for FinalitySource<C, P> {
+impl<P: SubstrateFinalitySyncPipeline> Clone for SubstrateFinalitySource<P> {
 	fn clone(&self) -> Self {
-		FinalitySource {
+		SubstrateFinalitySource {
 			client: self.client.clone(),
 			maximal_header_number: self.maximal_header_number.clone(),
-			_phantom: Default::default(),
 		}
 	}
 }
 
 #[async_trait]
-impl<C: Chain, P: FinalitySyncPipeline> RelayClient for FinalitySource<C, P> {
+impl<P: SubstrateFinalitySyncPipeline> RelayClient for SubstrateFinalitySource<P> {
 	type Error = Error;
 
 	async fn reconnect(&mut self) -> Result<(), Error> {
@@ -87,21 +96,12 @@ impl<C: Chain, P: FinalitySyncPipeline> RelayClient for FinalitySource<C, P> {
 }
 
 #[async_trait]
-impl<C, P> SourceClient<P> for FinalitySource<C, P>
-where
-	C: Chain,
-	C::BlockNumber: relay_utils::BlockNumberBase,
-	P: FinalitySyncPipeline<
-		Hash = C::Hash,
-		Number = C::BlockNumber,
-		Header = SyncHeader<C::Header>,
-		FinalityProof = GrandpaJustification<C::Header>,
-	>,
-	P::Header: SourceHeader<C::BlockNumber>,
+impl<P: SubstrateFinalitySyncPipeline> SourceClient<FinalitySyncPipelineAdapter<P>>
+	for SubstrateFinalitySource<P>
 {
-	type FinalityProofsStream = Pin<Box<dyn Stream<Item = GrandpaJustification<C::Header>> + Send>>;
+	type FinalityProofsStream = SubstrateFinalityProofsStream<P>;
 
-	async fn best_finalized_block_number(&self) -> Result<P::Number, Error> {
+	async fn best_finalized_block_number(&self) -> Result<BlockNumberOf<P::SourceChain>, Error> {
 		let mut finalized_header_number = self.on_chain_best_finalized_block_number().await?;
 		// never return block number larger than requested. This way we'll never sync headers
 		// past `maximal_header_number`
@@ -116,15 +116,23 @@ where
 
 	async fn header_and_finality_proof(
 		&self,
-		number: P::Number,
-	) -> Result<(P::Header, Option<P::FinalityProof>), Error> {
+		number: BlockNumberOf<P::SourceChain>,
+	) -> Result<
+		(
+			relay_substrate_client::SyncHeader<HeaderOf<P::SourceChain>>,
+			Option<GrandpaJustification<HeaderOf<P::SourceChain>>>,
+		),
+		Error,
+	> {
 		let header_hash = self.client.block_hash_by_number(number).await?;
 		let signed_block = self.client.get_block(Some(header_hash)).await?;
 
 		let justification = signed_block
 			.justification()
 			.map(|raw_justification| {
-				GrandpaJustification::<C::Header>::decode(&mut raw_justification.as_slice())
+				GrandpaJustification::<HeaderOf<P::SourceChain>>::decode(
+					&mut raw_justification.as_slice(),
+				)
 			})
 			.transpose()
 			.map_err(Error::ResponseParseFailed)?;
@@ -141,7 +149,7 @@ where
 						log::error!(
 							target: "bridge",
 							"Failed to read justification target from the {} justifications stream: {:?}",
-							P::SOURCE_NAME,
+							P::SourceChain::NAME,
 							err,
 						);
 					};
@@ -153,7 +161,9 @@ where
 						.ok()??;
 
 					let decoded_justification =
-						GrandpaJustification::<C::Header>::decode(&mut &next_justification[..]);
+						GrandpaJustification::<HeaderOf<P::SourceChain>>::decode(
+							&mut &next_justification[..],
+						);
 
 					let justification = match decoded_justification {
 						Ok(j) => j,
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/finality_target.rs b/polkadot/bridges/relays/lib-substrate-relay/src/finality_target.rs
index f50bd103f4300c0e78024b6cd50c81f5f83233cf..4c5814171049aedc7e8b1575c623ed8580e188df 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/finality_target.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/finality_target.rs
@@ -15,96 +15,122 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 //! Substrate client as Substrate finality proof target. The chain we connect to should have
-//! runtime that implements `<BridgedChainName>FinalityApi` to allow bridging with
-//! <BridgedName> chain.
+//! bridge GRANDPA pallet deployed and provide `<BridgedChainName>FinalityApi` to allow bridging
+//! with <BridgedName> chain.
 
-use crate::finality_pipeline::SubstrateFinalitySyncPipeline;
+use crate::{
+	finality_pipeline::{
+		FinalitySyncPipelineAdapter, SubmitFinalityProofCallBuilder, SubstrateFinalitySyncPipeline,
+	},
+	TransactionParams,
+};
 
 use async_trait::async_trait;
-use codec::Decode;
-use finality_relay::{FinalitySyncPipeline, TargetClient};
-use relay_substrate_client::{Chain, Client, Error as SubstrateError};
+use bp_header_chain::{justification::GrandpaJustification, storage_keys::is_halted_key};
+use codec::Encode;
+use finality_relay::TargetClient;
+use relay_substrate_client::{
+	AccountIdOf, AccountKeyPairOf, Chain, ChainWithGrandpa, Client, Error, HeaderIdOf, HeaderOf,
+	SignParam, SyncHeader, TransactionEra, TransactionSignScheme, UnsignedTransaction,
+};
 use relay_utils::relay_loop::Client as RelayClient;
+use sp_core::{Bytes, Pair};
 
 /// Substrate client as Substrate finality target.
-pub struct SubstrateFinalityTarget<C: Chain, P> {
-	client: Client<C>,
-	pipeline: P,
-	transactions_mortality: Option<u32>,
+pub struct SubstrateFinalityTarget<P: SubstrateFinalitySyncPipeline> {
+	client: Client<P::TargetChain>,
+	transaction_params: TransactionParams<AccountKeyPairOf<P::TransactionSignScheme>>,
 }
 
-impl<C: Chain, P> SubstrateFinalityTarget<C, P> {
+impl<P: SubstrateFinalitySyncPipeline> SubstrateFinalityTarget<P> {
 	/// Create new Substrate headers target.
-	pub fn new(client: Client<C>, pipeline: P, transactions_mortality: Option<u32>) -> Self {
-		SubstrateFinalityTarget { client, pipeline, transactions_mortality }
+	pub fn new(
+		client: Client<P::TargetChain>,
+		transaction_params: TransactionParams<AccountKeyPairOf<P::TransactionSignScheme>>,
+	) -> Self {
+		SubstrateFinalityTarget { client, transaction_params }
+	}
+
+	/// Ensure that the GRANDPA pallet at target chain is active.
+	pub async fn ensure_pallet_active(&self) -> Result<(), Error> {
+		let is_halted = self
+			.client
+			.storage_value(is_halted_key(P::SourceChain::WITH_CHAIN_GRANDPA_PALLET_NAME), None)
+			.await?;
+		if is_halted.unwrap_or(false) {
+			Err(Error::BridgePalletIsHalted)
+		} else {
+			Ok(())
+		}
 	}
 }
 
-impl<C: Chain, P: SubstrateFinalitySyncPipeline> Clone for SubstrateFinalityTarget<C, P> {
+impl<P: SubstrateFinalitySyncPipeline> Clone for SubstrateFinalityTarget<P> {
 	fn clone(&self) -> Self {
 		SubstrateFinalityTarget {
 			client: self.client.clone(),
-			pipeline: self.pipeline.clone(),
-			transactions_mortality: self.transactions_mortality,
+			transaction_params: self.transaction_params.clone(),
 		}
 	}
 }
 
 #[async_trait]
-impl<C: Chain, P: SubstrateFinalitySyncPipeline> RelayClient for SubstrateFinalityTarget<C, P> {
-	type Error = SubstrateError;
+impl<P: SubstrateFinalitySyncPipeline> RelayClient for SubstrateFinalityTarget<P> {
+	type Error = Error;
 
-	async fn reconnect(&mut self) -> Result<(), SubstrateError> {
+	async fn reconnect(&mut self) -> Result<(), Error> {
 		self.client.reconnect().await
 	}
 }
 
 #[async_trait]
-impl<C, P> TargetClient<P::FinalitySyncPipeline> for SubstrateFinalityTarget<C, P>
+impl<P: SubstrateFinalitySyncPipeline> TargetClient<FinalitySyncPipelineAdapter<P>>
+	for SubstrateFinalityTarget<P>
 where
-	C: Chain,
-	P: SubstrateFinalitySyncPipeline<TargetChain = C>,
-	<P::FinalitySyncPipeline as FinalitySyncPipeline>::Number: Decode,
-	<P::FinalitySyncPipeline as FinalitySyncPipeline>::Hash: Decode,
+	AccountIdOf<P::TargetChain>: From<<AccountKeyPairOf<P::TransactionSignScheme> as Pair>::Public>,
+	P::TransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
 {
-	async fn best_finalized_source_block_number(
-		&self,
-	) -> Result<<P::FinalitySyncPipeline as FinalitySyncPipeline>::Number, SubstrateError> {
+	async fn best_finalized_source_block_id(&self) -> Result<HeaderIdOf<P::SourceChain>, Error> {
 		// we can't continue to relay finality if target node is out of sync, because
 		// it may have already received (some of) headers that we're going to relay
 		self.client.ensure_synced().await?;
+		// we can't relay finality if GRANDPA pallet at target chain is halted
+		self.ensure_pallet_active().await?;
 
-		Ok(crate::messages_source::read_client_state::<
-			C,
-			<P::FinalitySyncPipeline as FinalitySyncPipeline>::Hash,
-			<P::FinalitySyncPipeline as FinalitySyncPipeline>::Number,
-		>(&self.client, P::BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET)
+		Ok(crate::messages_source::read_client_state::<P::TargetChain, P::SourceChain>(
+			&self.client,
+			None,
+			P::SourceChain::BEST_FINALIZED_HEADER_ID_METHOD,
+		)
 		.await?
-		.best_finalized_peer_at_best_self
-		.0)
+		.best_finalized_peer_at_best_self)
 	}
 
 	async fn submit_finality_proof(
 		&self,
-		header: <P::FinalitySyncPipeline as FinalitySyncPipeline>::Header,
-		proof: <P::FinalitySyncPipeline as FinalitySyncPipeline>::FinalityProof,
-	) -> Result<(), SubstrateError> {
-		let transactions_author = self.pipeline.transactions_author();
-		let pipeline = self.pipeline.clone();
-		let transactions_mortality = self.transactions_mortality;
+		header: SyncHeader<HeaderOf<P::SourceChain>>,
+		proof: GrandpaJustification<HeaderOf<P::SourceChain>>,
+	) -> Result<(), Error> {
+		let genesis_hash = *self.client.genesis_hash();
+		let transaction_params = self.transaction_params.clone();
+		let call =
+			P::SubmitFinalityProofCallBuilder::build_submit_finality_proof_call(header, proof);
+		let (spec_version, transaction_version) = self.client.simple_runtime_version().await?;
 		self.client
 			.submit_signed_extrinsic(
-				transactions_author,
+				self.transaction_params.signer.public().into(),
 				move |best_block_id, transaction_nonce| {
-					pipeline.make_submit_finality_proof_transaction(
-						relay_substrate_client::TransactionEra::new(
-							best_block_id,
-							transactions_mortality,
-						),
-						transaction_nonce,
-						header,
-						proof,
-					)
+					Ok(Bytes(
+						P::TransactionSignScheme::sign_transaction(SignParam {
+							spec_version,
+							transaction_version,
+							genesis_hash,
+							signer: transaction_params.signer.clone(),
+							era: TransactionEra::new(best_block_id, transaction_params.mortality),
+							unsigned: UnsignedTransaction::new(call.into(), transaction_nonce),
+						})?
+						.encode(),
+					))
 				},
 			)
 			.await
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/headers_initialize.rs b/polkadot/bridges/relays/lib-substrate-relay/src/headers_initialize.rs
index 2e802c4cb215078cb121449d18093c90d7580d2b..0e1371c53c815d10c4eac6965bf69b09ea69aca6 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/headers_initialize.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/headers_initialize.rs
@@ -31,17 +31,22 @@ use bp_header_chain::{
 use codec::Decode;
 use finality_grandpa::voter_set::VoterSet;
 use num_traits::{One, Zero};
-use relay_substrate_client::{Chain, Client};
+use relay_substrate_client::{
+	BlockNumberOf, Chain, ChainWithGrandpa, Client, Error as SubstrateError, HashOf,
+};
 use sp_core::Bytes;
 use sp_finality_grandpa::AuthorityList as GrandpaAuthoritiesSet;
 use sp_runtime::traits::Header as HeaderT;
 
 /// Submit headers-bridge initialization transaction.
-pub async fn initialize<SourceChain: Chain, TargetChain: Chain>(
+pub async fn initialize<SourceChain: ChainWithGrandpa, TargetChain: Chain>(
 	source_client: Client<SourceChain>,
 	target_client: Client<TargetChain>,
 	target_transactions_signer: TargetChain::AccountId,
-	prepare_initialize_transaction: impl FnOnce(TargetChain::Index, InitializationData<SourceChain::Header>) -> Bytes
+	prepare_initialize_transaction: impl FnOnce(
+			TargetChain::Index,
+			InitializationData<SourceChain::Header>,
+		) -> Result<Bytes, SubstrateError>
 		+ Send
 		+ 'static,
 ) {
@@ -54,13 +59,14 @@ pub async fn initialize<SourceChain: Chain, TargetChain: Chain>(
 	.await;
 
 	match result {
-		Ok(tx_hash) => log::info!(
+		Ok(Some(tx_hash)) => log::info!(
 			target: "bridge",
 			"Successfully submitted {}-headers bridge initialization transaction to {}: {:?}",
 			SourceChain::NAME,
 			TargetChain::NAME,
 			tx_hash,
 		),
+		Ok(None) => (),
 		Err(err) => log::error!(
 			target: "bridge",
 			"Failed to submit {}-headers bridge initialization transaction to {}: {:?}",
@@ -72,14 +78,31 @@ pub async fn initialize<SourceChain: Chain, TargetChain: Chain>(
 }
 
 /// Craft and submit initialization transaction, returning any error that may occur.
-async fn do_initialize<SourceChain: Chain, TargetChain: Chain>(
+async fn do_initialize<SourceChain: ChainWithGrandpa, TargetChain: Chain>(
 	source_client: Client<SourceChain>,
 	target_client: Client<TargetChain>,
 	target_transactions_signer: TargetChain::AccountId,
-	prepare_initialize_transaction: impl FnOnce(TargetChain::Index, InitializationData<SourceChain::Header>) -> Bytes
+	prepare_initialize_transaction: impl FnOnce(
+			TargetChain::Index,
+			InitializationData<SourceChain::Header>,
+		) -> Result<Bytes, SubstrateError>
 		+ Send
 		+ 'static,
-) -> Result<TargetChain::Hash, Error<SourceChain::Hash, <SourceChain::Header as HeaderT>::Number>> {
+) -> Result<
+	Option<TargetChain::Hash>,
+	Error<SourceChain::Hash, <SourceChain::Header as HeaderT>::Number>,
+> {
+	let is_initialized = is_initialized::<SourceChain, TargetChain>(&target_client).await?;
+	if is_initialized {
+		log::info!(
+			target: "bridge",
+			"{}-headers bridge at {} is already initialized. Skipping",
+			SourceChain::NAME,
+			TargetChain::NAME,
+		);
+		return Ok(None)
+	}
+
 	let initialization_data = prepare_initialization_data(source_client).await?;
 	log::info!(
 		target: "bridge",
@@ -95,7 +118,23 @@ async fn do_initialize<SourceChain: Chain, TargetChain: Chain>(
 		})
 		.await
 		.map_err(|err| Error::SubmitTransaction(TargetChain::NAME, err))?;
-	Ok(initialization_tx_hash)
+	Ok(Some(initialization_tx_hash))
+}
+
+/// Returns `Ok(true)` if bridge has already been initialized.
+async fn is_initialized<SourceChain: ChainWithGrandpa, TargetChain: Chain>(
+	target_client: &Client<TargetChain>,
+) -> Result<bool, Error<HashOf<SourceChain>, BlockNumberOf<SourceChain>>> {
+	Ok(target_client
+		.raw_storage_value(
+			bp_header_chain::storage_keys::best_finalized_hash_key(
+				SourceChain::WITH_CHAIN_GRANDPA_PALLET_NAME,
+			),
+			None,
+		)
+		.await
+		.map_err(|err| Error::RetrieveBestFinalizedHeaderHash(SourceChain::NAME, err))?
+		.is_some())
 }
 
 /// Prepare initialization data for the GRANDPA verifier pallet.
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/helpers.rs b/polkadot/bridges/relays/lib-substrate-relay/src/helpers.rs
index f95a8e0aba3ab43f011c2d431f3fb8027609e5f2..80359b1c2a9802b105f2cdacf48fd93850b62ea1 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/helpers.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/helpers.rs
@@ -16,14 +16,91 @@
 
 //! Substrate relay helpers
 
-use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError};
+use relay_utils::metrics::{FloatJsonValueMetric, PrometheusError, StandaloneMetric};
 
 /// Creates standalone token price metric.
 pub fn token_price_metric(token_id: &str) -> Result<FloatJsonValueMetric, PrometheusError> {
 	FloatJsonValueMetric::new(
 		format!("https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies=btc", token_id),
 		format!("$.{}.btc", token_id),
-		format!("{}_to_base_conversion_rate", token_id.replace("-", "_")),
+		format!("{}_to_base_conversion_rate", token_id.replace('-', "_")),
 		format!("Rate used to convert from {} to some BASE tokens", token_id.to_uppercase()),
 	)
 }
+
+/// Compute conversion rate between two tokens immediately, without spawning any metrics.
+///
+/// Returned rate may be used in expression: `from_tokens * rate -> to_tokens`.
+pub async fn tokens_conversion_rate_from_metrics(
+	from_token_id: &str,
+	to_token_id: &str,
+) -> anyhow::Result<f64> {
+	let from_token_metric = token_price_metric(from_token_id)?;
+	from_token_metric.update().await;
+	let to_token_metric = token_price_metric(to_token_id)?;
+	to_token_metric.update().await;
+
+	let from_token_value = *from_token_metric.shared_value_ref().read().await;
+	let to_token_value = *to_token_metric.shared_value_ref().read().await;
+	// `FloatJsonValueMetric` guarantees that the value is positive && normal, so no additional
+	// checks required here
+	match (from_token_value, to_token_value) {
+		(Some(from_token_value), Some(to_token_value)) =>
+			Ok(tokens_conversion_rate(from_token_value, to_token_value)),
+		_ => Err(anyhow::format_err!(
+			"Failed to compute conversion rate from {} to {}",
+			from_token_id,
+			to_token_id,
+		)),
+	}
+}
+
+/// Compute conversion rate between two tokens, given token prices.
+///
+/// Returned rate may be used in expression: `from_tokens * rate -> to_tokens`.
+///
+/// Both prices are assumed to be normal and non-negative.
+pub fn tokens_conversion_rate(from_token_value: f64, to_token_value: f64) -> f64 {
+	from_token_value / to_token_value
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn rialto_to_millau_conversion_rate_is_correct() {
+		let rialto_price = 18.18;
+		let millau_price = 136.35;
+		assert!(rialto_price < millau_price);
+
+		let conversion_rate = tokens_conversion_rate(rialto_price, millau_price);
+		let rialto_amount = 100.0;
+		let millau_amount = rialto_amount * conversion_rate;
+		assert!(
+			rialto_amount > millau_amount,
+			"{} RLT * {} = {} MLU",
+			rialto_amount,
+			conversion_rate,
+			millau_amount,
+		);
+	}
+
+	#[test]
+	fn millau_to_rialto_conversion_rate_is_correct() {
+		let rialto_price = 18.18;
+		let millau_price = 136.35;
+		assert!(rialto_price < millau_price);
+
+		let conversion_rate = tokens_conversion_rate(millau_price, rialto_price);
+		let millau_amount = 100.0;
+		let rialto_amount = millau_amount * conversion_rate;
+		assert!(
+			rialto_amount > millau_amount,
+			"{} MLU * {} = {} RLT",
+			millau_amount,
+			conversion_rate,
+			rialto_amount,
+		);
+	}
+}
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/lib.rs b/polkadot/bridges/relays/lib-substrate-relay/src/lib.rs
index cc066bf501ac6048730809b5c795e077d65dcd3b..27d91147c2ddcb68bd0fb25fcee243c849250aa3 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/lib.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/lib.rs
@@ -22,11 +22,14 @@ use std::time::Duration;
 
 pub mod conversion_rate_update;
 pub mod error;
+pub mod finality_guards;
 pub mod finality_pipeline;
+pub mod finality_source;
 pub mod finality_target;
 pub mod headers_initialize;
 pub mod helpers;
 pub mod messages_lane;
+pub mod messages_metrics;
 pub mod messages_source;
 pub mod messages_target;
 pub mod on_demand_headers;
@@ -39,3 +42,12 @@ pub mod on_demand_headers;
 /// blockchains) blocks. So 1 hour seems to be a good guess for (even congested) chains to mine
 /// transaction, or remove it from the pool.
 pub const STALL_TIMEOUT: Duration = Duration::from_secs(60 * 60);
+
+/// Transaction creation parameters.
+#[derive(Clone, Debug)]
+pub struct TransactionParams<TS> {
+	/// Transactions author.
+	pub signer: TS,
+	/// Transactions mortality.
+	pub mortality: Option<u32>,
+}
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/messages_lane.rs b/polkadot/bridges/relays/lib-substrate-relay/src/messages_lane.rs
index 6cadb64754a511661627f9eae79a1295a0249e07..fadf5e62245adf7ab63952bc4d74492554d7abab 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/messages_lane.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/messages_lane.rs
@@ -17,194 +17,434 @@
 //! Tools for supporting message lanes between two Substrate-based chains.
 
 use crate::{
-	messages_source::SubstrateMessagesProof, messages_target::SubstrateMessagesReceivingProof,
+	conversion_rate_update::UpdateConversionRateCallBuilder,
+	messages_metrics::StandaloneMessagesMetrics,
+	messages_source::{SubstrateMessagesProof, SubstrateMessagesSource},
+	messages_target::{SubstrateMessagesDeliveryProof, SubstrateMessagesTarget},
 	on_demand_headers::OnDemandHeadersRelay,
+	TransactionParams, STALL_TIMEOUT,
 };
 
-use async_trait::async_trait;
 use bp_messages::{LaneId, MessageNonce};
-use bp_runtime::{AccountIdOf, IndexOf};
-use frame_support::weights::Weight;
-use messages_relay::{
-	message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf},
-	relay_strategy::RelayStrategy,
+use bp_runtime::{AccountIdOf, Chain as _};
+use bridge_runtime_common::messages::{
+	source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof,
 };
+use codec::Encode;
+use frame_support::weights::{GetDispatchInfo, Weight};
+use messages_relay::{message_lane::MessageLane, relay_strategy::RelayStrategy};
+use pallet_bridge_messages::{Call as BridgeMessagesCall, Config as BridgeMessagesConfig};
 use relay_substrate_client::{
-	metrics::{FloatStorageValueMetric, StorageProofOverheadMetric},
-	BlockNumberOf, Chain, Client, HashOf,
+	transaction_stall_timeout, AccountKeyPairOf, BalanceOf, BlockNumberOf, CallOf, Chain,
+	ChainWithMessages, Client, HashOf, TransactionSignScheme,
 };
-use relay_utils::{
-	metrics::{
-		FloatJsonValueMetric, GlobalMetrics, MetricsParams, PrometheusError, StandaloneMetric,
-	},
-	BlockNumberBase,
-};
-use sp_core::{storage::StorageKey, Bytes};
-use sp_runtime::FixedU128;
-use std::ops::RangeInclusive;
+use relay_utils::metrics::MetricsParams;
+use sp_core::Pair;
+use std::{convert::TryFrom, fmt::Debug, marker::PhantomData};
+
+/// Substrate -> Substrate messages synchronization pipeline.
+pub trait SubstrateMessageLane: 'static + Clone + Debug + Send + Sync {
+	/// Name of the source -> target tokens conversion rate parameter.
+	///
+	/// The parameter is stored at the target chain and the storage key is computed using
+	/// `bp_runtime::storage_parameter_key` function. If value is unknown, it is assumed
+	/// to be 1.
+	const SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str>;
+	/// Name of the target -> source tokens conversion rate parameter.
+	///
+	/// The parameter is stored at the source chain and the storage key is computed using
+	/// `bp_runtime::storage_parameter_key` function. If value is unknown, it is assumed
+	/// to be 1.
+	const TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME: Option<&'static str>;
+
+	/// Name of the source chain fee multiplier parameter.
+	///
+	/// The parameter is stored at the target chain and the storage key is computed using
+	/// `bp_runtime::storage_parameter_key` function. If value is unknown, it is assumed
+	/// to be 1.
+	const SOURCE_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str>;
+	/// Name of the target chain fee multiplier parameter.
+	///
+	/// The parameter is stored at the source chain and the storage key is computed using
+	/// `bp_runtime::storage_parameter_key` function. If value is unknown, it is assumed
+	/// to be 1.
+	const TARGET_FEE_MULTIPLIER_PARAMETER_NAME: Option<&'static str>;
+
+	/// Name of the transaction payment pallet, deployed at the source chain.
+	const AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str>;
+	/// Name of the transaction payment pallet, deployed at the target chain.
+	const AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME: Option<&'static str>;
+
+	/// Messages of this chain are relayed to the `TargetChain`.
+	type SourceChain: ChainWithMessages;
+	/// Messages from the `SourceChain` are dispatched on this chain.
+	type TargetChain: ChainWithMessages;
+
+	/// Scheme used to sign source chain transactions.
+	type SourceTransactionSignScheme: TransactionSignScheme;
+	/// Scheme used to sign target chain transactions.
+	type TargetTransactionSignScheme: TransactionSignScheme;
+
+	/// How receive messages proof call is built?
+	type ReceiveMessagesProofCallBuilder: ReceiveMessagesProofCallBuilder<Self>;
+	/// How receive messages delivery proof call is built?
+	type ReceiveMessagesDeliveryProofCallBuilder: ReceiveMessagesDeliveryProofCallBuilder<Self>;
+
+	/// `TargetChain` tokens to `SourceChain` tokens conversion rate update builder.
+	///
+	/// If not applicable to this bridge, you may use `()` here.
+	type TargetToSourceChainConversionRateUpdateBuilder: UpdateConversionRateCallBuilder<
+		Self::SourceChain,
+	>;
+
+	/// Message relay strategy.
+	type RelayStrategy: RelayStrategy;
+}
+
+/// Adapter that allows all `SubstrateMessageLane` to act as `MessageLane`.
+#[derive(Clone, Debug)]
+pub(crate) struct MessageLaneAdapter<P: SubstrateMessageLane> {
+	_phantom: PhantomData<P>,
+}
+
+impl<P: SubstrateMessageLane> MessageLane for MessageLaneAdapter<P> {
+	const SOURCE_NAME: &'static str = P::SourceChain::NAME;
+	const TARGET_NAME: &'static str = P::TargetChain::NAME;
+
+	type MessagesProof = SubstrateMessagesProof<P::SourceChain>;
+	type MessagesReceivingProof = SubstrateMessagesDeliveryProof<P::TargetChain>;
+
+	type SourceChainBalance = BalanceOf<P::SourceChain>;
+	type SourceHeaderNumber = BlockNumberOf<P::SourceChain>;
+	type SourceHeaderHash = HashOf<P::SourceChain>;
+
+	type TargetHeaderNumber = BlockNumberOf<P::TargetChain>;
+	type TargetHeaderHash = HashOf<P::TargetChain>;
+}
 
 /// Substrate <-> Substrate messages relay parameters.
-pub struct MessagesRelayParams<SC: Chain, SS, TC: Chain, TS, Strategy: RelayStrategy> {
+pub struct MessagesRelayParams<P: SubstrateMessageLane> {
 	/// Messages source client.
-	pub source_client: Client<SC>,
-	/// Sign parameters for messages source chain.
-	pub source_sign: SS,
-	/// Mortality of source transactions.
-	pub source_transactions_mortality: Option<u32>,
+	pub source_client: Client<P::SourceChain>,
+	/// Source transaction params.
+	pub source_transaction_params:
+		TransactionParams<AccountKeyPairOf<P::SourceTransactionSignScheme>>,
 	/// Messages target client.
-	pub target_client: Client<TC>,
-	/// Sign parameters for messages target chain.
-	pub target_sign: TS,
-	/// Mortality of target transactions.
-	pub target_transactions_mortality: Option<u32>,
+	pub target_client: Client<P::TargetChain>,
+	/// Target transaction params.
+	pub target_transaction_params:
+		TransactionParams<AccountKeyPairOf<P::TargetTransactionSignScheme>>,
 	/// Optional on-demand source to target headers relay.
-	pub source_to_target_headers_relay: Option<OnDemandHeadersRelay<SC>>,
+	pub source_to_target_headers_relay: Option<OnDemandHeadersRelay<P::SourceChain>>,
 	/// Optional on-demand target to source headers relay.
-	pub target_to_source_headers_relay: Option<OnDemandHeadersRelay<TC>>,
+	pub target_to_source_headers_relay: Option<OnDemandHeadersRelay<P::TargetChain>>,
 	/// Identifier of lane that needs to be served.
 	pub lane_id: LaneId,
 	/// Metrics parameters.
 	pub metrics_params: MetricsParams,
 	/// Pre-registered standalone metrics.
-	pub standalone_metrics: Option<StandaloneMessagesMetrics<SC, TC>>,
-	/// Relay strategy
-	pub relay_strategy: Strategy,
+	pub standalone_metrics: Option<StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>>,
+	/// Relay strategy.
+	pub relay_strategy: P::RelayStrategy,
 }
 
-/// Message sync pipeline for Substrate <-> Substrate relays.
-#[async_trait]
-pub trait SubstrateMessageLane: 'static + Clone + Send + Sync {
-	/// Underlying generic message lane.
-	type MessageLane: MessageLane;
-
-	/// Name of the runtime method that returns dispatch weight of outbound messages at the source
-	/// chain.
-	const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str;
-	/// Name of the runtime method that returns latest generated nonce at the source chain.
-	const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str;
-	/// Name of the runtime method that returns latest received (confirmed) nonce at the the source
-	/// chain.
-	const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str;
-
-	/// Name of the runtime method that returns latest received nonce at the target chain.
-	const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str;
-	/// Name of the runtime method that returns the latest confirmed (reward-paid) nonce at the
-	/// target chain.
-	const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str;
-	/// Number of the runtime method that returns state of "unrewarded relayers" set at the target
-	/// chain.
-	const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str;
-
-	/// Name of the runtime method that returns id of best finalized source header at target chain.
-	const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str;
-	/// Name of the runtime method that returns id of best finalized target header at source chain.
-	const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str;
-
-	/// Name of the messages pallet as it is declared in the `construct_runtime!()` at source chain.
-	const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str;
-	/// Name of the messages pallet as it is declared in the `construct_runtime!()` at target chain.
-	const MESSAGE_PALLET_NAME_AT_TARGET: &'static str;
-
-	/// Extra weight of the delivery transaction at the target chain, that is paid to cover
-	/// dispatch fee payment.
-	///
-	/// If dispatch fee is paid at the source chain, then this weight is refunded by the
-	/// delivery transaction.
-	const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight;
-
-	/// Source chain.
-	type SourceChain: Chain;
-	/// Target chain.
-	type TargetChain: Chain;
-
-	/// Returns id of account that we're using to sign transactions at target chain (messages
-	/// proof).
-	fn target_transactions_author(&self) -> AccountIdOf<Self::TargetChain>;
-
-	/// Make messages delivery transaction.
-	fn make_messages_delivery_transaction(
-		&self,
-		best_block_id: TargetHeaderIdOf<Self::MessageLane>,
-		transaction_nonce: IndexOf<Self::TargetChain>,
-		generated_at_header: SourceHeaderIdOf<Self::MessageLane>,
-		nonces: RangeInclusive<MessageNonce>,
-		proof: <Self::MessageLane as MessageLane>::MessagesProof,
-	) -> Bytes;
-
-	/// Returns id of account that we're using to sign transactions at source chain (delivery
-	/// proof).
-	fn source_transactions_author(&self) -> AccountIdOf<Self::SourceChain>;
-
-	/// Make messages receiving proof transaction.
-	fn make_messages_receiving_proof_transaction(
-		&self,
-		best_block_id: SourceHeaderIdOf<Self::MessageLane>,
-		transaction_nonce: IndexOf<Self::SourceChain>,
-		generated_at_header: TargetHeaderIdOf<Self::MessageLane>,
-		proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-	) -> Bytes;
+/// Run Substrate-to-Substrate messages sync loop.
+pub async fn run<P: SubstrateMessageLane>(params: MessagesRelayParams<P>) -> anyhow::Result<()>
+where
+	AccountIdOf<P::SourceChain>:
+		From<<AccountKeyPairOf<P::SourceTransactionSignScheme> as Pair>::Public>,
+	AccountIdOf<P::TargetChain>:
+		From<<AccountKeyPairOf<P::TargetTransactionSignScheme> as Pair>::Public>,
+	BalanceOf<P::SourceChain>: TryFrom<BalanceOf<P::TargetChain>>,
+	P::SourceTransactionSignScheme: TransactionSignScheme<Chain = P::SourceChain>,
+	P::TargetTransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
+{
+	let source_client = params.source_client;
+	let target_client = params.target_client;
+	let stall_timeout = relay_substrate_client::bidirectional_transaction_stall_timeout(
+		params.source_transaction_params.mortality,
+		params.target_transaction_params.mortality,
+		P::SourceChain::AVERAGE_BLOCK_INTERVAL,
+		P::TargetChain::AVERAGE_BLOCK_INTERVAL,
+		STALL_TIMEOUT,
+	);
+	let relayer_id_at_source: AccountIdOf<P::SourceChain> =
+		params.source_transaction_params.signer.public().into();
+
+	// 2/3 is reserved for proofs and tx overhead
+	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 (max_messages_in_single_batch, max_messages_weight_in_single_batch) =
+		crate::messages_lane::select_delivery_transaction_limits::<
+			<P::TargetChain as ChainWithMessages>::WeightInfo,
+		>(
+			P::TargetChain::max_extrinsic_weight(),
+			P::SourceChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+		);
+	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);
+
+	let standalone_metrics = params.standalone_metrics.map(Ok).unwrap_or_else(|| {
+		crate::messages_metrics::standalone_metrics::<P>(
+			source_client.clone(),
+			target_client.clone(),
+		)
+	})?;
+
+	log::info!(
+		target: "bridge",
+		"Starting {} -> {} messages relay.\n\t\
+			{} relayer account id: {:?}\n\t\
+			Max messages in single transaction: {}\n\t\
+			Max messages size in single transaction: {}\n\t\
+			Max messages weight in single transaction: {}\n\t\
+			Tx mortality: {:?} (~{}m)/{:?} (~{}m)\n\t\
+			Stall timeout: {:?}",
+		P::SourceChain::NAME,
+		P::TargetChain::NAME,
+		P::SourceChain::NAME,
+		relayer_id_at_source,
+		max_messages_in_single_batch,
+		max_messages_size_in_single_batch,
+		max_messages_weight_in_single_batch,
+		params.source_transaction_params.mortality,
+		transaction_stall_timeout(
+			params.source_transaction_params.mortality,
+			P::SourceChain::AVERAGE_BLOCK_INTERVAL,
+			STALL_TIMEOUT,
+		).as_secs_f64() / 60.0f64,
+		params.target_transaction_params.mortality,
+		transaction_stall_timeout(
+			params.target_transaction_params.mortality,
+			P::TargetChain::AVERAGE_BLOCK_INTERVAL,
+			STALL_TIMEOUT,
+		).as_secs_f64() / 60.0f64,
+		stall_timeout,
+	);
+
+	messages_relay::message_lane_loop::run(
+		messages_relay::message_lane_loop::Params {
+			lane: params.lane_id,
+			source_tick: P::SourceChain::AVERAGE_BLOCK_INTERVAL,
+			target_tick: P::TargetChain::AVERAGE_BLOCK_INTERVAL,
+			reconnect_delay: relay_utils::relay_loop::RECONNECT_DELAY,
+			stall_timeout,
+			delivery_params: messages_relay::message_lane_loop::MessageDeliveryParams {
+				max_unrewarded_relayer_entries_at_target:
+					P::SourceChain::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+				max_unconfirmed_nonces_at_target:
+					P::SourceChain::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
+				max_messages_in_single_batch,
+				max_messages_weight_in_single_batch,
+				max_messages_size_in_single_batch,
+				relay_strategy: params.relay_strategy,
+			},
+		},
+		SubstrateMessagesSource::<P>::new(
+			source_client.clone(),
+			target_client.clone(),
+			params.lane_id,
+			params.source_transaction_params,
+			params.target_to_source_headers_relay,
+		),
+		SubstrateMessagesTarget::<P>::new(
+			target_client,
+			source_client,
+			params.lane_id,
+			relayer_id_at_source,
+			params.target_transaction_params,
+			standalone_metrics.clone(),
+			params.source_to_target_headers_relay,
+		),
+		standalone_metrics.register_and_spawn(params.metrics_params)?,
+		futures::future::pending(),
+	)
+	.await
+	.map_err(Into::into)
 }
 
-/// Substrate-to-Substrate message lane.
-#[derive(Debug)]
-pub struct SubstrateMessageLaneToSubstrate<
-	Source: Chain,
-	SourceSignParams,
-	Target: Chain,
-	TargetSignParams,
-> {
-	/// Client for the source Substrate chain.
-	pub source_client: Client<Source>,
-	/// Parameters required to sign transactions for source chain.
-	pub source_sign: SourceSignParams,
-	/// Source transactions mortality.
-	pub source_transactions_mortality: Option<u32>,
-	/// Client for the target Substrate chain.
-	pub target_client: Client<Target>,
-	/// Parameters required to sign transactions for target chain.
-	pub target_sign: TargetSignParams,
-	/// Target transactions mortality.
-	pub target_transactions_mortality: Option<u32>,
-	/// Account id of relayer at the source chain.
-	pub relayer_id_at_source: Source::AccountId,
+/// Different ways of building `receive_messages_proof` calls.
+pub trait ReceiveMessagesProofCallBuilder<P: SubstrateMessageLane> {
+	/// Given messages proof, build call of `receive_messages_proof` function of bridge
+	/// messages module at the target chain.
+	fn build_receive_messages_proof_call(
+		relayer_id_at_source: AccountIdOf<P::SourceChain>,
+		proof: SubstrateMessagesProof<P::SourceChain>,
+		messages_count: u32,
+		dispatch_weight: Weight,
+		trace_call: bool,
+	) -> CallOf<P::TargetChain>;
 }
 
-impl<Source: Chain, SourceSignParams: Clone, Target: Chain, TargetSignParams: Clone> Clone
-	for SubstrateMessageLaneToSubstrate<Source, SourceSignParams, Target, TargetSignParams>
+/// Building `receive_messages_proof` call when you have direct access to the target
+/// chain runtime.
+pub struct DirectReceiveMessagesProofCallBuilder<P, R, I> {
+	_phantom: PhantomData<(P, R, I)>,
+}
+
+impl<P, R, I> ReceiveMessagesProofCallBuilder<P> for DirectReceiveMessagesProofCallBuilder<P, R, I>
+where
+	P: SubstrateMessageLane,
+	R: BridgeMessagesConfig<I, InboundRelayer = AccountIdOf<P::SourceChain>>,
+	I: 'static,
+	R::SourceHeaderChain: bp_messages::target_chain::SourceHeaderChain<
+		R::InboundMessageFee,
+		MessagesProof = FromBridgedChainMessagesProof<HashOf<P::SourceChain>>,
+	>,
+	CallOf<P::TargetChain>: From<BridgeMessagesCall<R, I>> + GetDispatchInfo,
 {
-	fn clone(&self) -> Self {
-		Self {
-			source_client: self.source_client.clone(),
-			source_sign: self.source_sign.clone(),
-			source_transactions_mortality: self.source_transactions_mortality,
-			target_client: self.target_client.clone(),
-			target_sign: self.target_sign.clone(),
-			target_transactions_mortality: self.target_transactions_mortality,
-			relayer_id_at_source: self.relayer_id_at_source.clone(),
+	fn build_receive_messages_proof_call(
+		relayer_id_at_source: AccountIdOf<P::SourceChain>,
+		proof: SubstrateMessagesProof<P::SourceChain>,
+		messages_count: u32,
+		dispatch_weight: Weight,
+		trace_call: bool,
+	) -> CallOf<P::TargetChain> {
+		let call: CallOf<P::TargetChain> = BridgeMessagesCall::<R, I>::receive_messages_proof {
+			relayer_id_at_bridged_chain: relayer_id_at_source,
+			proof: proof.1,
+			messages_count,
+			dispatch_weight,
 		}
+		.into();
+		if trace_call {
+			// this trace isn't super-accurate, because limits are for transactions and we
+			// have a call here, but it provides required information
+			log::trace!(
+				target: "bridge",
+				"Prepared {} -> {} messages delivery call. Weight: {}/{}, size: {}/{}",
+				P::SourceChain::NAME,
+				P::TargetChain::NAME,
+				call.get_dispatch_info().weight,
+				P::TargetChain::max_extrinsic_weight(),
+				call.encode().len(),
+				P::TargetChain::max_extrinsic_size(),
+			);
+		}
+		call
 	}
 }
 
-impl<Source: Chain, SourceSignParams, Target: Chain, TargetSignParams> MessageLane
-	for SubstrateMessageLaneToSubstrate<Source, SourceSignParams, Target, TargetSignParams>
-where
-	SourceSignParams: Clone + Send + Sync + 'static,
-	TargetSignParams: Clone + Send + Sync + 'static,
-	BlockNumberOf<Source>: BlockNumberBase,
-	BlockNumberOf<Target>: BlockNumberBase,
-{
-	const SOURCE_NAME: &'static str = Source::NAME;
-	const TARGET_NAME: &'static str = Target::NAME;
+/// Macro that generates `ReceiveMessagesProofCallBuilder` implementation for the case when
+/// you only have an access to the mocked version of target chain runtime. In this case you
+/// should provide "name" of the call variant for the bridge messages calls and the "name" of
+/// the variant for the `receive_messages_proof` call within that first option.
+#[rustfmt::skip]
+#[macro_export]
+macro_rules! generate_mocked_receive_message_proof_call_builder {
+	($pipeline:ident, $mocked_builder:ident, $bridge_messages:path, $receive_messages_proof:path) => {
+		pub struct $mocked_builder;
+
+		impl $crate::messages_lane::ReceiveMessagesProofCallBuilder<$pipeline>
+			for $mocked_builder
+		{
+			fn build_receive_messages_proof_call(
+				relayer_id_at_source: relay_substrate_client::AccountIdOf<
+					<$pipeline as $crate::messages_lane::SubstrateMessageLane>::SourceChain
+				>,
+				proof: $crate::messages_source::SubstrateMessagesProof<
+					<$pipeline as $crate::messages_lane::SubstrateMessageLane>::SourceChain
+				>,
+				messages_count: u32,
+				dispatch_weight: Weight,
+				_trace_call: bool,
+			) -> relay_substrate_client::CallOf<
+				<$pipeline as $crate::messages_lane::SubstrateMessageLane>::TargetChain
+			> {
+				$bridge_messages($receive_messages_proof(
+					relayer_id_at_source,
+					proof.1,
+					messages_count,
+					dispatch_weight,
+				))
+			}
+		}
+	};
+}
 
-	type MessagesProof = SubstrateMessagesProof<Source>;
-	type MessagesReceivingProof = SubstrateMessagesReceivingProof<Target>;
+/// Different ways of building `receive_messages_delivery_proof` calls.
+pub trait ReceiveMessagesDeliveryProofCallBuilder<P: SubstrateMessageLane> {
+	/// Given messages delivery proof, build call of `receive_messages_delivery_proof` function of
+	/// bridge messages module at the source chain.
+	fn build_receive_messages_delivery_proof_call(
+		proof: SubstrateMessagesDeliveryProof<P::TargetChain>,
+		trace_call: bool,
+	) -> CallOf<P::SourceChain>;
+}
+
+/// Building `receive_messages_delivery_proof` call when you have direct access to the source
+/// chain runtime.
+pub struct DirectReceiveMessagesDeliveryProofCallBuilder<P, R, I> {
+	_phantom: PhantomData<(P, R, I)>,
+}
 
-	type SourceChainBalance = Source::Balance;
-	type SourceHeaderNumber = BlockNumberOf<Source>;
-	type SourceHeaderHash = HashOf<Source>;
+impl<P, R, I> ReceiveMessagesDeliveryProofCallBuilder<P>
+	for DirectReceiveMessagesDeliveryProofCallBuilder<P, R, I>
+where
+	P: SubstrateMessageLane,
+	R: BridgeMessagesConfig<I>,
+	I: 'static,
+	R::TargetHeaderChain: bp_messages::source_chain::TargetHeaderChain<
+		R::OutboundPayload,
+		R::AccountId,
+		MessagesDeliveryProof = FromBridgedChainMessagesDeliveryProof<HashOf<P::TargetChain>>,
+	>,
+	CallOf<P::SourceChain>: From<BridgeMessagesCall<R, I>> + GetDispatchInfo,
+{
+	fn build_receive_messages_delivery_proof_call(
+		proof: SubstrateMessagesDeliveryProof<P::TargetChain>,
+		trace_call: bool,
+	) -> CallOf<P::SourceChain> {
+		let call: CallOf<P::SourceChain> =
+			BridgeMessagesCall::<R, I>::receive_messages_delivery_proof {
+				proof: proof.1,
+				relayers_state: proof.0,
+			}
+			.into();
+		if trace_call {
+			// this trace isn't super-accurate, because limits are for transactions and we
+			// have a call here, but it provides required information
+			log::trace!(
+				target: "bridge",
+				"Prepared {} -> {} delivery confirmation transaction. Weight: {}/{}, size: {}/{}",
+				P::TargetChain::NAME,
+				P::SourceChain::NAME,
+				call.get_dispatch_info().weight,
+				P::SourceChain::max_extrinsic_weight(),
+				call.encode().len(),
+				P::SourceChain::max_extrinsic_size(),
+			);
+		}
+		call
+	}
+}
 
-	type TargetHeaderNumber = BlockNumberOf<Target>;
-	type TargetHeaderHash = HashOf<Target>;
+/// Macro that generates `ReceiveMessagesDeliveryProofCallBuilder` implementation for the case when
+/// you only have an access to the mocked version of source chain runtime. In this case you
+/// should provide "name" of the call variant for the bridge messages calls and the "name" of
+/// the variant for the `receive_messages_delivery_proof` call within that first option.
+#[rustfmt::skip]
+#[macro_export]
+macro_rules! generate_mocked_receive_message_delivery_proof_call_builder {
+	($pipeline:ident, $mocked_builder:ident, $bridge_messages:path, $receive_messages_delivery_proof:path) => {
+		pub struct $mocked_builder;
+
+		impl $crate::messages_lane::ReceiveMessagesDeliveryProofCallBuilder<$pipeline>
+			for $mocked_builder
+		{
+			fn build_receive_messages_delivery_proof_call(
+				proof: $crate::messages_target::SubstrateMessagesDeliveryProof<
+					<$pipeline as $crate::messages_lane::SubstrateMessageLane>::TargetChain
+				>,
+				_trace_call: bool,
+			) -> relay_substrate_client::CallOf<
+				<$pipeline as $crate::messages_lane::SubstrateMessageLane>::SourceChain
+			> {
+				$bridge_messages($receive_messages_delivery_proof(proof.1, proof.0))
+			}
+		}
+	};
 }
 
 /// Returns maximal number of messages and their maximal cumulative dispatch weight, based
@@ -245,165 +485,20 @@ pub fn select_delivery_transaction_limits<W: pallet_bridge_messages::WeightInfoE
 	(max_number_of_messages, weight_for_messages_dispatch)
 }
 
-/// Shared references to the standalone metrics of the message lane relay loop.
-#[derive(Debug, Clone)]
-pub struct StandaloneMessagesMetrics<SC: Chain, TC: Chain> {
-	/// Global metrics.
-	pub global: GlobalMetrics,
-	/// Storage chain proof overhead metric.
-	pub source_storage_proof_overhead: StorageProofOverheadMetric<SC>,
-	/// Target chain proof overhead metric.
-	pub target_storage_proof_overhead: StorageProofOverheadMetric<TC>,
-	/// Source tokens to base conversion rate metric.
-	pub source_to_base_conversion_rate: Option<FloatJsonValueMetric>,
-	/// Target tokens to base conversion rate metric.
-	pub target_to_base_conversion_rate: Option<FloatJsonValueMetric>,
-	/// Source tokens to target tokens conversion rate metric. This rate is stored by the target
-	/// chain.
-	pub source_to_target_conversion_rate:
-		Option<FloatStorageValueMetric<TC, sp_runtime::FixedU128>>,
-	/// Target tokens to source tokens conversion rate metric. This rate is stored by the source
-	/// chain.
-	pub target_to_source_conversion_rate:
-		Option<FloatStorageValueMetric<SC, sp_runtime::FixedU128>>,
-}
-
-impl<SC: Chain, TC: Chain> StandaloneMessagesMetrics<SC, TC> {
-	/// Swap source and target sides.
-	pub fn reverse(self) -> StandaloneMessagesMetrics<TC, SC> {
-		StandaloneMessagesMetrics {
-			global: self.global,
-			source_storage_proof_overhead: self.target_storage_proof_overhead,
-			target_storage_proof_overhead: self.source_storage_proof_overhead,
-			source_to_base_conversion_rate: self.target_to_base_conversion_rate,
-			target_to_base_conversion_rate: self.source_to_base_conversion_rate,
-			source_to_target_conversion_rate: self.target_to_source_conversion_rate,
-			target_to_source_conversion_rate: self.source_to_target_conversion_rate,
-		}
-	}
-
-	/// Register all metrics in the registry.
-	pub fn register_and_spawn(
-		self,
-		metrics: MetricsParams,
-	) -> Result<MetricsParams, PrometheusError> {
-		self.global.register_and_spawn(&metrics.registry)?;
-		self.source_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
-		self.target_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
-		if let Some(m) = self.source_to_base_conversion_rate {
-			m.register_and_spawn(&metrics.registry)?;
-		}
-		if let Some(m) = self.target_to_base_conversion_rate {
-			m.register_and_spawn(&metrics.registry)?;
-		}
-		if let Some(m) = self.target_to_source_conversion_rate {
-			m.register_and_spawn(&metrics.registry)?;
-		}
-		Ok(metrics)
-	}
-
-	/// Return conversion rate from target to source tokens.
-	pub async fn target_to_source_conversion_rate(&self) -> Option<f64> {
-		Self::compute_target_to_source_conversion_rate(
-			*self.target_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await,
-			*self.source_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await,
-		)
-	}
-
-	/// Return conversion rate from target to source tokens, given conversion rates from
-	/// target/source tokens to some base token.
-	fn compute_target_to_source_conversion_rate(
-		target_to_base_conversion_rate: Option<f64>,
-		source_to_base_conversion_rate: Option<f64>,
-	) -> Option<f64> {
-		Some(source_to_base_conversion_rate? / target_to_base_conversion_rate?)
-	}
-}
-
-/// Create standalone metrics for the message lane relay loop.
-///
-/// All metrics returned by this function are exposed by loops that are serving given lane (`P`)
-/// and by loops that are serving reverse lane (`P` with swapped `TargetChain` and `SourceChain`).
-pub fn standalone_metrics<SC: Chain, TC: Chain>(
-	source_client: Client<SC>,
-	target_client: Client<TC>,
-	source_chain_token_id: Option<&str>,
-	target_chain_token_id: Option<&str>,
-	source_to_target_conversion_rate_params: Option<(StorageKey, FixedU128)>,
-	target_to_source_conversion_rate_params: Option<(StorageKey, FixedU128)>,
-) -> anyhow::Result<StandaloneMessagesMetrics<SC, TC>> {
-	Ok(StandaloneMessagesMetrics {
-		global: GlobalMetrics::new()?,
-		source_storage_proof_overhead: StorageProofOverheadMetric::new(
-			source_client.clone(),
-			format!("{}_storage_proof_overhead", SC::NAME.to_lowercase()),
-			format!("{} storage proof overhead", SC::NAME),
-		)?,
-		target_storage_proof_overhead: StorageProofOverheadMetric::new(
-			target_client.clone(),
-			format!("{}_storage_proof_overhead", TC::NAME.to_lowercase()),
-			format!("{} storage proof overhead", TC::NAME),
-		)?,
-		source_to_base_conversion_rate: source_chain_token_id
-			.map(|source_chain_token_id| {
-				crate::helpers::token_price_metric(source_chain_token_id).map(Some)
-			})
-			.unwrap_or(Ok(None))?,
-		target_to_base_conversion_rate: target_chain_token_id
-			.map(|target_chain_token_id| {
-				crate::helpers::token_price_metric(target_chain_token_id).map(Some)
-			})
-			.unwrap_or(Ok(None))?,
-		source_to_target_conversion_rate: source_to_target_conversion_rate_params
-			.map(|(key, rate)| {
-				FloatStorageValueMetric::<_, sp_runtime::FixedU128>::new(
-					target_client,
-					key,
-					Some(rate),
-					format!("{}_{}_to_{}_conversion_rate", TC::NAME, SC::NAME, TC::NAME),
-					format!(
-						"{} to {} tokens conversion rate (used by {})",
-						SC::NAME,
-						TC::NAME,
-						TC::NAME
-					),
-				)
-				.map(Some)
-			})
-			.unwrap_or(Ok(None))?,
-		target_to_source_conversion_rate: target_to_source_conversion_rate_params
-			.map(|(key, rate)| {
-				FloatStorageValueMetric::<_, sp_runtime::FixedU128>::new(
-					source_client,
-					key,
-					Some(rate),
-					format!("{}_{}_to_{}_conversion_rate", SC::NAME, TC::NAME, SC::NAME),
-					format!(
-						"{} to {} tokens conversion rate (used by {})",
-						TC::NAME,
-						SC::NAME,
-						SC::NAME
-					),
-				)
-				.map(Some)
-			})
-			.unwrap_or(Ok(None))?,
-	})
-}
-
 #[cfg(test)]
 mod tests {
 	use super::*;
+	use bp_runtime::Chain;
 
 	type RialtoToMillauMessagesWeights =
-		pallet_bridge_messages::weights::RialtoWeight<rialto_runtime::Runtime>;
+		pallet_bridge_messages::weights::MillauWeight<rialto_runtime::Runtime>;
 
 	#[test]
 	fn select_delivery_transaction_limits_works() {
 		let (max_count, max_weight) =
 			select_delivery_transaction_limits::<RialtoToMillauMessagesWeights>(
-				bp_millau::max_extrinsic_weight(),
-				bp_millau::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
+				bp_millau::Millau::max_extrinsic_weight(),
+				bp_rialto::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
 			);
 		assert_eq!(
 			(max_count, max_weight),
@@ -412,15 +507,7 @@ mod tests {
 			// i.e. weight reserved for messages dispatch allows dispatch of non-trivial messages.
 			//
 			// Any significant change in this values should attract additional attention.
-			(782, 216_583_333_334),
-		);
-	}
-
-	#[async_std::test]
-	async fn target_to_source_conversion_rate_works() {
-		assert_eq!(
-			StandaloneMessagesMetrics::<relay_rococo_client::Rococo, relay_wococo_client::Wococo>::compute_target_to_source_conversion_rate(Some(183.15), Some(12.32)),
-			Some(12.32 / 183.15),
+			(958, 216_583_333_334),
 		);
 	}
 }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/messages_metrics.rs b/polkadot/bridges/relays/lib-substrate-relay/src/messages_metrics.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b165892fda1dc62484c171e2aa5878143e279eff
--- /dev/null
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/messages_metrics.rs
@@ -0,0 +1,389 @@
+// 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/>.
+
+//! Tools for supporting message lanes between two Substrate-based chains.
+
+use crate::{helpers::tokens_conversion_rate, messages_lane::SubstrateMessageLane};
+
+use codec::Decode;
+use frame_system::AccountInfo;
+use pallet_balances::AccountData;
+use relay_substrate_client::{
+	metrics::{
+		FixedU128OrOne, FloatStorageValue, FloatStorageValueMetric, StorageProofOverheadMetric,
+	},
+	AccountIdOf, BalanceOf, Chain, ChainWithBalances, Client, Error as SubstrateError, IndexOf,
+};
+use relay_utils::metrics::{
+	FloatJsonValueMetric, GlobalMetrics, MetricsParams, PrometheusError, StandaloneMetric,
+};
+use sp_core::storage::StorageData;
+use sp_runtime::{FixedPointNumber, FixedU128};
+use std::{convert::TryFrom, fmt::Debug, marker::PhantomData};
+
+/// Name of the `NextFeeMultiplier` storage value within the transaction payment pallet.
+const NEXT_FEE_MULTIPLIER_VALUE_NAME: &str = "NextFeeMultiplier";
+
+/// Shared references to the standalone metrics of the message lane relay loop.
+#[derive(Debug, Clone)]
+pub struct StandaloneMessagesMetrics<SC: Chain, TC: Chain> {
+	/// Global metrics.
+	pub global: GlobalMetrics,
+	/// Storage chain proof overhead metric.
+	pub source_storage_proof_overhead: StorageProofOverheadMetric<SC>,
+	/// Target chain proof overhead metric.
+	pub target_storage_proof_overhead: StorageProofOverheadMetric<TC>,
+	/// Source tokens to base conversion rate metric.
+	pub source_to_base_conversion_rate: Option<FloatJsonValueMetric>,
+	/// Target tokens to base conversion rate metric.
+	pub target_to_base_conversion_rate: Option<FloatJsonValueMetric>,
+	/// Source tokens to target tokens conversion rate metric. This rate is stored by the target
+	/// chain.
+	pub source_to_target_conversion_rate: Option<FloatStorageValueMetric<TC, FixedU128OrOne>>,
+	/// Target tokens to source tokens conversion rate metric. This rate is stored by the source
+	/// chain.
+	pub target_to_source_conversion_rate: Option<FloatStorageValueMetric<SC, FixedU128OrOne>>,
+
+	/// Actual source chain fee multiplier.
+	pub source_fee_multiplier: Option<FloatStorageValueMetric<SC, FixedU128OrOne>>,
+	/// Source chain fee multiplier, stored at the target chain.
+	pub source_fee_multiplier_at_target: Option<FloatStorageValueMetric<TC, FixedU128OrOne>>,
+	/// Actual target chain fee multiplier.
+	pub target_fee_multiplier: Option<FloatStorageValueMetric<TC, FixedU128OrOne>>,
+	/// Target chain fee multiplier, stored at the target chain.
+	pub target_fee_multiplier_at_source: Option<FloatStorageValueMetric<SC, FixedU128OrOne>>,
+}
+
+impl<SC: Chain, TC: Chain> StandaloneMessagesMetrics<SC, TC> {
+	/// Swap source and target sides.
+	pub fn reverse(self) -> StandaloneMessagesMetrics<TC, SC> {
+		StandaloneMessagesMetrics {
+			global: self.global,
+			source_storage_proof_overhead: self.target_storage_proof_overhead,
+			target_storage_proof_overhead: self.source_storage_proof_overhead,
+			source_to_base_conversion_rate: self.target_to_base_conversion_rate,
+			target_to_base_conversion_rate: self.source_to_base_conversion_rate,
+			source_to_target_conversion_rate: self.target_to_source_conversion_rate,
+			target_to_source_conversion_rate: self.source_to_target_conversion_rate,
+			source_fee_multiplier: self.target_fee_multiplier,
+			source_fee_multiplier_at_target: self.target_fee_multiplier_at_source,
+			target_fee_multiplier: self.source_fee_multiplier,
+			target_fee_multiplier_at_source: self.source_fee_multiplier_at_target,
+		}
+	}
+
+	/// Register all metrics in the registry.
+	pub fn register_and_spawn(
+		self,
+		metrics: MetricsParams,
+	) -> Result<MetricsParams, PrometheusError> {
+		self.global.register_and_spawn(&metrics.registry)?;
+		self.source_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
+		self.target_storage_proof_overhead.register_and_spawn(&metrics.registry)?;
+		if let Some(m) = self.source_to_base_conversion_rate {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.target_to_base_conversion_rate {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.target_to_source_conversion_rate {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.source_fee_multiplier {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.source_fee_multiplier_at_target {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.target_fee_multiplier {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		if let Some(m) = self.target_fee_multiplier_at_source {
+			m.register_and_spawn(&metrics.registry)?;
+		}
+		Ok(metrics)
+	}
+
+	/// Return conversion rate from target to source tokens.
+	pub async fn target_to_source_conversion_rate(&self) -> Option<f64> {
+		let from_token_value =
+			(*self.target_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await)?;
+		let to_token_value =
+			(*self.source_to_base_conversion_rate.as_ref()?.shared_value_ref().read().await)?;
+		Some(tokens_conversion_rate(from_token_value, to_token_value))
+	}
+}
+
+/// Create symmetric standalone metrics for the message lane relay loop.
+///
+/// All metrics returned by this function are exposed by loops that are serving given lane (`P`)
+/// and by loops that are serving reverse lane (`P` with swapped `TargetChain` and `SourceChain`).
+/// We assume that either conversion rate parameters have values in the storage, or they are
+/// initialized with 1:1.
+pub fn standalone_metrics<P: SubstrateMessageLane>(
+	source_client: Client<P::SourceChain>,
+	target_client: Client<P::TargetChain>,
+) -> anyhow::Result<StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>> {
+	Ok(StandaloneMessagesMetrics {
+		global: GlobalMetrics::new()?,
+		source_storage_proof_overhead: StorageProofOverheadMetric::new(
+			source_client.clone(),
+			format!("{}_storage_proof_overhead", P::SourceChain::NAME.to_lowercase()),
+			format!("{} storage proof overhead", P::SourceChain::NAME),
+		)?,
+		target_storage_proof_overhead: StorageProofOverheadMetric::new(
+			target_client.clone(),
+			format!("{}_storage_proof_overhead", P::TargetChain::NAME.to_lowercase()),
+			format!("{} storage proof overhead", P::TargetChain::NAME),
+		)?,
+		source_to_base_conversion_rate: P::SourceChain::TOKEN_ID
+			.map(|source_chain_token_id| {
+				crate::helpers::token_price_metric(source_chain_token_id).map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		target_to_base_conversion_rate: P::TargetChain::TOKEN_ID
+			.map(|target_chain_token_id| {
+				crate::helpers::token_price_metric(target_chain_token_id).map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		source_to_target_conversion_rate: P::SOURCE_TO_TARGET_CONVERSION_RATE_PARAMETER_NAME
+			.map(bp_runtime::storage_parameter_key)
+			.map(|key| {
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					target_client.clone(),
+					key,
+					format!(
+						"{}_{}_to_{}_conversion_rate",
+						P::TargetChain::NAME,
+						P::SourceChain::NAME,
+						P::TargetChain::NAME
+					),
+					format!(
+						"{} to {} tokens conversion rate (used by {})",
+						P::SourceChain::NAME,
+						P::TargetChain::NAME,
+						P::TargetChain::NAME
+					),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		target_to_source_conversion_rate: P::TARGET_TO_SOURCE_CONVERSION_RATE_PARAMETER_NAME
+			.map(bp_runtime::storage_parameter_key)
+			.map(|key| {
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					source_client.clone(),
+					key,
+					format!(
+						"{}_{}_to_{}_conversion_rate",
+						P::SourceChain::NAME,
+						P::TargetChain::NAME,
+						P::SourceChain::NAME
+					),
+					format!(
+						"{} to {} tokens conversion rate (used by {})",
+						P::TargetChain::NAME,
+						P::SourceChain::NAME,
+						P::SourceChain::NAME
+					),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		source_fee_multiplier: P::AT_SOURCE_TRANSACTION_PAYMENT_PALLET_NAME
+			.map(|pallet| bp_runtime::storage_value_key(pallet, NEXT_FEE_MULTIPLIER_VALUE_NAME))
+			.map(|key| {
+				log::trace!(target: "bridge", "{}_fee_multiplier", P::SourceChain::NAME);
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					source_client.clone(),
+					key,
+					format!("{}_fee_multiplier", P::SourceChain::NAME,),
+					format!("{} fee multiplier", P::SourceChain::NAME,),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		source_fee_multiplier_at_target: P::SOURCE_FEE_MULTIPLIER_PARAMETER_NAME
+			.map(bp_runtime::storage_parameter_key)
+			.map(|key| {
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					target_client.clone(),
+					key,
+					format!("{}_{}_fee_multiplier", P::TargetChain::NAME, P::SourceChain::NAME,),
+					format!(
+						"{} fee multiplier stored at {}",
+						P::SourceChain::NAME,
+						P::TargetChain::NAME,
+					),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		target_fee_multiplier: P::AT_TARGET_TRANSACTION_PAYMENT_PALLET_NAME
+			.map(|pallet| bp_runtime::storage_value_key(pallet, NEXT_FEE_MULTIPLIER_VALUE_NAME))
+			.map(|key| {
+				log::trace!(target: "bridge", "{}_fee_multiplier", P::TargetChain::NAME);
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					target_client,
+					key,
+					format!("{}_fee_multiplier", P::TargetChain::NAME,),
+					format!("{} fee multiplier", P::TargetChain::NAME,),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+		target_fee_multiplier_at_source: P::TARGET_FEE_MULTIPLIER_PARAMETER_NAME
+			.map(bp_runtime::storage_parameter_key)
+			.map(|key| {
+				FloatStorageValueMetric::new(
+					FixedU128OrOne::default(),
+					source_client,
+					key,
+					format!("{}_{}_fee_multiplier", P::SourceChain::NAME, P::TargetChain::NAME,),
+					format!(
+						"{} fee multiplier stored at {}",
+						P::TargetChain::NAME,
+						P::SourceChain::NAME,
+					),
+				)
+				.map(Some)
+			})
+			.unwrap_or(Ok(None))?,
+	})
+}
+
+/// Add relay accounts balance metrics.
+pub async fn add_relay_balances_metrics<C: ChainWithBalances>(
+	client: Client<C>,
+	metrics: MetricsParams,
+	relay_account_id: Option<AccountIdOf<C>>,
+	messages_pallet_owner_account_id: Option<AccountIdOf<C>>,
+) -> anyhow::Result<MetricsParams>
+where
+	BalanceOf<C>: Into<u128> + std::fmt::Debug,
+{
+	if relay_account_id.is_none() && messages_pallet_owner_account_id.is_none() {
+		return Ok(metrics)
+	}
+
+	// if `tokenDecimals` is missing from system properties, we'll be using
+	let token_decimals = client
+		.token_decimals()
+		.await?
+		.map(|token_decimals| {
+			log::info!(target: "bridge", "Read `tokenDecimals` for {}: {}", C::NAME, token_decimals);
+			token_decimals
+		})
+		.unwrap_or_else(|| {
+			// turns out it is normal not to have this property - e.g. when polkadot binary is
+			// started using `polkadot-local` chain. Let's use minimal nominal here
+			log::info!(target: "bridge", "Using default (zero) `tokenDecimals` value for {}", C::NAME);
+			0
+		});
+	let token_decimals = u32::try_from(token_decimals).map_err(|e| {
+		anyhow::format_err!(
+			"Token decimals value ({}) of {} doesn't fit into u32: {:?}",
+			token_decimals,
+			C::NAME,
+			e,
+		)
+	})?;
+	if let Some(relay_account_id) = relay_account_id {
+		let relay_account_balance_metric = FloatStorageValueMetric::new(
+			FreeAccountBalance::<C> { token_decimals, _phantom: Default::default() },
+			client.clone(),
+			C::account_info_storage_key(&relay_account_id),
+			format!("at_{}_relay_balance", C::NAME),
+			format!("Balance of the relay account at the {}", C::NAME),
+		)?;
+		relay_account_balance_metric.register_and_spawn(&metrics.registry)?;
+	}
+	if let Some(messages_pallet_owner_account_id) = messages_pallet_owner_account_id {
+		let pallet_owner_account_balance_metric = FloatStorageValueMetric::new(
+			FreeAccountBalance::<C> { token_decimals, _phantom: Default::default() },
+			client.clone(),
+			C::account_info_storage_key(&messages_pallet_owner_account_id),
+			format!("at_{}_messages_pallet_owner_balance", C::NAME),
+			format!("Balance of the messages pallet owner at the {}", C::NAME),
+		)?;
+		pallet_owner_account_balance_metric.register_and_spawn(&metrics.registry)?;
+	}
+	Ok(metrics)
+}
+
+/// Adapter for `FloatStorageValueMetric` to decode account free balance.
+#[derive(Clone, Debug)]
+struct FreeAccountBalance<C> {
+	token_decimals: u32,
+	_phantom: PhantomData<C>,
+}
+
+impl<C> FloatStorageValue for FreeAccountBalance<C>
+where
+	C: Chain,
+	BalanceOf<C>: Into<u128>,
+{
+	type Value = FixedU128;
+
+	fn decode(
+		&self,
+		maybe_raw_value: Option<StorageData>,
+	) -> Result<Option<Self::Value>, SubstrateError> {
+		maybe_raw_value
+			.map(|raw_value| {
+				AccountInfo::<IndexOf<C>, AccountData<BalanceOf<C>>>::decode(&mut &raw_value.0[..])
+					.map_err(SubstrateError::ResponseParseFailed)
+					.map(|account_data| {
+						convert_to_token_balance(account_data.data.free.into(), self.token_decimals)
+					})
+			})
+			.transpose()
+	}
+}
+
+/// Convert from raw `u128` balance (nominated in smallest chain token units) to the float regular
+/// tokens value.
+fn convert_to_token_balance(balance: u128, token_decimals: u32) -> FixedU128 {
+	FixedU128::from_inner(balance.saturating_mul(FixedU128::DIV / 10u128.pow(token_decimals)))
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use frame_support::storage::generator::StorageValue;
+	use sp_core::storage::StorageKey;
+
+	#[test]
+	fn token_decimals_used_properly() {
+		let plancks = 425_000_000_000;
+		let token_decimals = 10;
+		let dots = convert_to_token_balance(plancks, token_decimals);
+		assert_eq!(dots, FixedU128::saturating_from_rational(425, 10));
+	}
+
+	#[test]
+	fn next_fee_multiplier_storage_key_is_correct() {
+		assert_eq!(
+			bp_runtime::storage_value_key("TransactionPayment", NEXT_FEE_MULTIPLIER_VALUE_NAME),
+			StorageKey(pallet_transaction_payment::NextFeeMultiplier::<rialto_runtime::Runtime>::storage_value_final_key().to_vec()),
+		);
+	}
+}
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/messages_source.rs b/polkadot/bridges/relays/lib-substrate-relay/src/messages_source.rs
index 5f066296e7e71517838f1513e63511dba3eda635..77dd2aed05bcd81749d5ab27f68de56991d70fe2 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/messages_source.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/messages_source.rs
@@ -19,12 +19,19 @@
 //! <BridgedName> chain.
 
 use crate::{
-	messages_lane::SubstrateMessageLane, messages_target::SubstrateMessagesReceivingProof,
+	messages_lane::{
+		MessageLaneAdapter, ReceiveMessagesDeliveryProofCallBuilder, SubstrateMessageLane,
+	},
+	messages_target::SubstrateMessagesDeliveryProof,
 	on_demand_headers::OnDemandHeadersRelay,
+	TransactionParams,
 };
 
 use async_trait::async_trait;
-use bp_messages::{LaneId, MessageNonce, UnrewardedRelayersState};
+use bp_messages::{
+	storage_keys::{operating_mode_key, outbound_lane_data_key},
+	LaneId, MessageNonce, OperatingMode, OutboundLaneData, UnrewardedRelayersState,
+};
 use bridge_runtime_common::messages::{
 	source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof,
 };
@@ -39,15 +46,13 @@ use messages_relay::{
 };
 use num_traits::{Bounded, Zero};
 use relay_substrate_client::{
-	BalanceOf, BlockNumberOf, Chain, Client, Error as SubstrateError, HashOf, HeaderIdOf, HeaderOf,
-	IndexOf,
-};
-use relay_utils::{relay_loop::Client as RelayClient, BlockNumberBase, HeaderId};
-use sp_core::Bytes;
-use sp_runtime::{
-	traits::{AtLeast32BitUnsigned, Header as HeaderT},
-	DeserializeOwned,
+	AccountIdOf, AccountKeyPairOf, BalanceOf, BlockNumberOf, Chain, ChainWithMessages, Client,
+	Error as SubstrateError, HashOf, HeaderIdOf, IndexOf, SignParam, TransactionEra,
+	TransactionSignScheme, UnsignedTransaction,
 };
+use relay_utils::{relay_loop::Client as RelayClient, HeaderId};
+use sp_core::{Bytes, Pair};
+use sp_runtime::{traits::Header as HeaderT, DeserializeOwned};
 use std::ops::RangeInclusive;
 
 /// Intermediate message proof returned by the source Substrate node. Includes everything
@@ -57,30 +62,60 @@ pub type SubstrateMessagesProof<C> = (Weight, FromBridgedChainMessagesProof<Hash
 
 /// Substrate client as Substrate messages source.
 pub struct SubstrateMessagesSource<P: SubstrateMessageLane> {
-	client: Client<P::SourceChain>,
-	lane: P,
+	source_client: Client<P::SourceChain>,
+	target_client: Client<P::TargetChain>,
 	lane_id: LaneId,
+	transaction_params: TransactionParams<AccountKeyPairOf<P::SourceTransactionSignScheme>>,
 	target_to_source_headers_relay: Option<OnDemandHeadersRelay<P::TargetChain>>,
 }
 
 impl<P: SubstrateMessageLane> SubstrateMessagesSource<P> {
 	/// Create new Substrate headers source.
 	pub fn new(
-		client: Client<P::SourceChain>,
-		lane: P,
+		source_client: Client<P::SourceChain>,
+		target_client: Client<P::TargetChain>,
 		lane_id: LaneId,
+		transaction_params: TransactionParams<AccountKeyPairOf<P::SourceTransactionSignScheme>>,
 		target_to_source_headers_relay: Option<OnDemandHeadersRelay<P::TargetChain>>,
 	) -> Self {
-		SubstrateMessagesSource { client, lane, lane_id, target_to_source_headers_relay }
+		SubstrateMessagesSource {
+			source_client,
+			target_client,
+			lane_id,
+			transaction_params,
+			target_to_source_headers_relay,
+		}
+	}
+
+	/// Read outbound lane state from the on-chain storage at given block.
+	async fn outbound_lane_data(
+		&self,
+		id: SourceHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<Option<OutboundLaneData>, SubstrateError> {
+		self.source_client
+			.storage_value(
+				outbound_lane_data_key(
+					P::TargetChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
+					&self.lane_id,
+				),
+				Some(id.1),
+			)
+			.await
+	}
+
+	/// Ensure that the messages pallet at source chain is active.
+	async fn ensure_pallet_active(&self) -> Result<(), SubstrateError> {
+		ensure_messages_pallet_active::<P::SourceChain, P::TargetChain>(&self.source_client).await
 	}
 }
 
 impl<P: SubstrateMessageLane> Clone for SubstrateMessagesSource<P> {
 	fn clone(&self) -> Self {
 		Self {
-			client: self.client.clone(),
-			lane: self.lane.clone(),
+			source_client: self.source_client.clone(),
+			target_client: self.target_client.clone(),
 			lane_id: self.lane_id,
+			transaction_params: self.transaction_params.clone(),
 			target_to_source_headers_relay: self.target_to_source_headers_relay.clone(),
 		}
 	}
@@ -91,96 +126,71 @@ impl<P: SubstrateMessageLane> RelayClient for SubstrateMessagesSource<P> {
 	type Error = SubstrateError;
 
 	async fn reconnect(&mut self) -> Result<(), SubstrateError> {
-		self.client.reconnect().await
+		self.source_client.reconnect().await?;
+		self.target_client.reconnect().await
 	}
 }
 
 #[async_trait]
-impl<P> SourceClient<P::MessageLane> for SubstrateMessagesSource<P>
+impl<P: SubstrateMessageLane> SourceClient<MessageLaneAdapter<P>> for SubstrateMessagesSource<P>
 where
-	P: SubstrateMessageLane,
-	P::SourceChain: Chain<
-		Hash = <P::MessageLane as MessageLane>::SourceHeaderHash,
-		BlockNumber = <P::MessageLane as MessageLane>::SourceHeaderNumber,
-		Balance = <P::MessageLane as MessageLane>::SourceChainBalance,
-	>,
-	BalanceOf<P::SourceChain>: Decode + Bounded,
-	IndexOf<P::SourceChain>: DeserializeOwned,
-	HashOf<P::SourceChain>: Copy,
-	BlockNumberOf<P::SourceChain>: BlockNumberBase + Copy,
-	HeaderOf<P::SourceChain>: DeserializeOwned,
-	P::TargetChain: Chain<
-		Hash = <P::MessageLane as MessageLane>::TargetHeaderHash,
-		BlockNumber = <P::MessageLane as MessageLane>::TargetHeaderNumber,
-	>,
-
-	P::MessageLane: MessageLane<
-		MessagesProof = SubstrateMessagesProof<P::SourceChain>,
-		MessagesReceivingProof = SubstrateMessagesReceivingProof<P::TargetChain>,
-	>,
-	<P::MessageLane as MessageLane>::TargetHeaderNumber: Decode,
-	<P::MessageLane as MessageLane>::TargetHeaderHash: Decode,
-	<P::MessageLane as MessageLane>::SourceChainBalance: AtLeast32BitUnsigned,
+	AccountIdOf<P::SourceChain>:
+		From<<AccountKeyPairOf<P::SourceTransactionSignScheme> as Pair>::Public>,
+	P::SourceTransactionSignScheme: TransactionSignScheme<Chain = P::SourceChain>,
 {
-	async fn state(&self) -> Result<SourceClientState<P::MessageLane>, SubstrateError> {
+	async fn state(&self) -> Result<SourceClientState<MessageLaneAdapter<P>>, SubstrateError> {
 		// we can't continue to deliver confirmations if source node is out of sync, because
 		// it may have already received confirmations that we're going to deliver
-		self.client.ensure_synced().await?;
-
-		read_client_state::<
-			_,
-			<P::MessageLane as MessageLane>::TargetHeaderHash,
-			<P::MessageLane as MessageLane>::TargetHeaderNumber,
-		>(&self.client, P::BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE)
+		self.source_client.ensure_synced().await?;
+		// we can't relay confirmations if messages pallet at source chain is halted
+		self.ensure_pallet_active().await?;
+
+		read_client_state(
+			&self.source_client,
+			Some(&self.target_client),
+			P::TargetChain::BEST_FINALIZED_HEADER_ID_METHOD,
+		)
 		.await
 	}
 
 	async fn latest_generated_nonce(
 		&self,
-		id: SourceHeaderIdOf<P::MessageLane>,
-	) -> Result<(SourceHeaderIdOf<P::MessageLane>, MessageNonce), SubstrateError> {
-		let encoded_response = self
-			.client
-			.state_call(
-				P::OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD.into(),
-				Bytes(self.lane_id.encode()),
-				Some(id.1),
-			)
-			.await?;
-		let latest_generated_nonce: MessageNonce = Decode::decode(&mut &encoded_response.0[..])
-			.map_err(SubstrateError::ResponseParseFailed)?;
+		id: SourceHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<(SourceHeaderIdOf<MessageLaneAdapter<P>>, MessageNonce), SubstrateError> {
+		// lane data missing from the storage is fine until first message is sent
+		let latest_generated_nonce = self
+			.outbound_lane_data(id)
+			.await?
+			.map(|data| data.latest_generated_nonce)
+			.unwrap_or(0);
 		Ok((id, latest_generated_nonce))
 	}
 
 	async fn latest_confirmed_received_nonce(
 		&self,
-		id: SourceHeaderIdOf<P::MessageLane>,
-	) -> Result<(SourceHeaderIdOf<P::MessageLane>, MessageNonce), SubstrateError> {
-		let encoded_response = self
-			.client
-			.state_call(
-				P::OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD.into(),
-				Bytes(self.lane_id.encode()),
-				Some(id.1),
-			)
-			.await?;
-		let latest_received_nonce: MessageNonce = Decode::decode(&mut &encoded_response.0[..])
-			.map_err(SubstrateError::ResponseParseFailed)?;
+		id: SourceHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<(SourceHeaderIdOf<MessageLaneAdapter<P>>, MessageNonce), SubstrateError> {
+		// lane data missing from the storage is fine until first message is sent
+		let latest_received_nonce = self
+			.outbound_lane_data(id)
+			.await?
+			.map(|data| data.latest_received_nonce)
+			.unwrap_or(0);
 		Ok((id, latest_received_nonce))
 	}
 
 	async fn generated_message_details(
 		&self,
-		id: SourceHeaderIdOf<P::MessageLane>,
+		id: SourceHeaderIdOf<MessageLaneAdapter<P>>,
 		nonces: RangeInclusive<MessageNonce>,
 	) -> Result<
-		MessageDetailsMap<<P::MessageLane as MessageLane>::SourceChainBalance>,
+		MessageDetailsMap<<MessageLaneAdapter<P> as MessageLane>::SourceChainBalance>,
 		SubstrateError,
 	> {
 		let encoded_response = self
-			.client
+			.source_client
 			.state_call(
-				P::OUTBOUND_LANE_MESSAGE_DETAILS_METHOD.into(),
+				P::TargetChain::TO_CHAIN_MESSAGE_DETAILS_METHOD.into(),
 				Bytes((self.lane_id, nonces.start(), nonces.end()).encode()),
 				Some(id.1),
 			)
@@ -195,14 +205,14 @@ where
 
 	async fn prove_messages(
 		&self,
-		id: SourceHeaderIdOf<P::MessageLane>,
+		id: SourceHeaderIdOf<MessageLaneAdapter<P>>,
 		nonces: RangeInclusive<MessageNonce>,
 		proof_parameters: MessageProofParameters,
 	) -> Result<
 		(
-			SourceHeaderIdOf<P::MessageLane>,
+			SourceHeaderIdOf<MessageLaneAdapter<P>>,
 			RangeInclusive<MessageNonce>,
-			<P::MessageLane as MessageLane>::MessagesProof,
+			<MessageLaneAdapter<P> as MessageLane>::MessagesProof,
 		),
 		SubstrateError,
 	> {
@@ -210,8 +220,8 @@ where
 			Vec::with_capacity(nonces.end().saturating_sub(*nonces.start()) as usize + 1);
 		let mut message_nonce = *nonces.start();
 		while message_nonce <= *nonces.end() {
-			let message_key = pallet_bridge_messages::storage_keys::message_key(
-				P::MESSAGE_PALLET_NAME_AT_SOURCE,
+			let message_key = bp_messages::storage_keys::message_key(
+				P::TargetChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
 				&self.lane_id,
 				message_nonce,
 			);
@@ -219,13 +229,18 @@ where
 			message_nonce += 1;
 		}
 		if proof_parameters.outbound_state_proof_required {
-			storage_keys.push(pallet_bridge_messages::storage_keys::outbound_lane_data_key(
-				P::MESSAGE_PALLET_NAME_AT_SOURCE,
+			storage_keys.push(bp_messages::storage_keys::outbound_lane_data_key(
+				P::TargetChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
 				&self.lane_id,
 			));
 		}
 
-		let proof = self.client.prove_storage(storage_keys, id.1).await?.iter_nodes().collect();
+		let proof = self
+			.source_client
+			.prove_storage(storage_keys, id.1)
+			.await?
+			.iter_nodes()
+			.collect();
 		let proof = FromBridgedChainMessagesProof {
 			bridged_header_hash: id.1,
 			storage_proof: proof,
@@ -238,19 +253,26 @@ where
 
 	async fn submit_messages_receiving_proof(
 		&self,
-		generated_at_block: TargetHeaderIdOf<P::MessageLane>,
-		proof: <P::MessageLane as MessageLane>::MessagesReceivingProof,
+		_generated_at_block: TargetHeaderIdOf<MessageLaneAdapter<P>>,
+		proof: <MessageLaneAdapter<P> as MessageLane>::MessagesReceivingProof,
 	) -> Result<(), SubstrateError> {
-		let lane = self.lane.clone();
-		self.client
+		let genesis_hash = *self.source_client.genesis_hash();
+		let transaction_params = self.transaction_params.clone();
+		let (spec_version, transaction_version) =
+			self.source_client.simple_runtime_version().await?;
+		self.source_client
 			.submit_signed_extrinsic(
-				self.lane.source_transactions_author(),
+				self.transaction_params.signer.public().into(),
 				move |best_block_id, transaction_nonce| {
-					lane.make_messages_receiving_proof_transaction(
+					make_messages_delivery_proof_transaction::<P>(
+						spec_version,
+						transaction_version,
+						&genesis_hash,
+						&transaction_params,
 						best_block_id,
 						transaction_nonce,
-						generated_at_block,
 						proof,
+						true,
 					)
 				},
 			)
@@ -258,7 +280,7 @@ where
 		Ok(())
 	}
 
-	async fn require_target_header_on_source(&self, id: TargetHeaderIdOf<P::MessageLane>) {
+	async fn require_target_header_on_source(&self, id: TargetHeaderIdOf<MessageLaneAdapter<P>>) {
 		if let Some(ref target_to_source_headers_relay) = self.target_to_source_headers_relay {
 			target_to_source_headers_relay.require_finalized_header(id).await;
 		}
@@ -266,26 +288,89 @@ where
 
 	async fn estimate_confirmation_transaction(
 		&self,
-	) -> <P::MessageLane as MessageLane>::SourceChainBalance {
-		self.client
-			.estimate_extrinsic_fee(self.lane.make_messages_receiving_proof_transaction(
+	) -> <MessageLaneAdapter<P> as MessageLane>::SourceChainBalance {
+		let runtime_version = match self.source_client.runtime_version().await {
+			Ok(v) => v,
+			Err(_) => return BalanceOf::<P::SourceChain>::max_value(),
+		};
+		async {
+			let dummy_tx = make_messages_delivery_proof_transaction::<P>(
+				runtime_version.spec_version,
+				runtime_version.transaction_version,
+				self.source_client.genesis_hash(),
+				&self.transaction_params,
 				HeaderId(Default::default(), Default::default()),
 				Zero::zero(),
-				HeaderId(Default::default(), Default::default()),
 				prepare_dummy_messages_delivery_proof::<P::SourceChain, P::TargetChain>(),
-			))
-			.await
-			.map(|fee| fee.inclusion_fee())
-			.unwrap_or_else(|_| BalanceOf::<P::SourceChain>::max_value())
+				false,
+			)?;
+			self.source_client
+				.estimate_extrinsic_fee(dummy_tx)
+				.await
+				.map(|fee| fee.inclusion_fee())
+		}
+		.await
+		.unwrap_or_else(|_| BalanceOf::<P::SourceChain>::max_value())
+	}
+}
+
+/// Ensure that the messages pallet at source chain is active.
+pub(crate) async fn ensure_messages_pallet_active<AtChain, WithChain>(
+	client: &Client<AtChain>,
+) -> Result<(), SubstrateError>
+where
+	AtChain: ChainWithMessages,
+	WithChain: ChainWithMessages,
+{
+	let operating_mode = client
+		.storage_value(operating_mode_key(WithChain::WITH_CHAIN_MESSAGES_PALLET_NAME), None)
+		.await?;
+	let is_halted = operating_mode == Some(OperatingMode::Halted);
+	if is_halted {
+		Err(SubstrateError::BridgePalletIsHalted)
+	} else {
+		Ok(())
 	}
 }
 
+/// Make messages delivery proof transaction from given proof.
+#[allow(clippy::too_many_arguments)]
+fn make_messages_delivery_proof_transaction<P: SubstrateMessageLane>(
+	spec_version: u32,
+	transaction_version: u32,
+	source_genesis_hash: &HashOf<P::SourceChain>,
+	source_transaction_params: &TransactionParams<AccountKeyPairOf<P::SourceTransactionSignScheme>>,
+	source_best_block_id: HeaderIdOf<P::SourceChain>,
+	transaction_nonce: IndexOf<P::SourceChain>,
+	proof: SubstrateMessagesDeliveryProof<P::TargetChain>,
+	trace_call: bool,
+) -> Result<Bytes, SubstrateError>
+where
+	P::SourceTransactionSignScheme: TransactionSignScheme<Chain = P::SourceChain>,
+{
+	let call =
+		P::ReceiveMessagesDeliveryProofCallBuilder::build_receive_messages_delivery_proof_call(
+			proof, trace_call,
+		);
+	Ok(Bytes(
+		P::SourceTransactionSignScheme::sign_transaction(SignParam {
+			spec_version,
+			transaction_version,
+			genesis_hash: *source_genesis_hash,
+			signer: source_transaction_params.signer.clone(),
+			era: TransactionEra::new(source_best_block_id, source_transaction_params.mortality),
+			unsigned: UnsignedTransaction::new(call.into(), transaction_nonce),
+		})?
+		.encode(),
+	))
+}
+
 /// Prepare 'dummy' messages delivery proof that will compose the delivery confirmation transaction.
 ///
 /// We don't care about proof actually being the valid proof, because its validity doesn't
 /// affect the call weight - we only care about its size.
 fn prepare_dummy_messages_delivery_proof<SC: Chain, TC: Chain>(
-) -> SubstrateMessagesReceivingProof<TC> {
+) -> SubstrateMessagesDeliveryProof<TC> {
 	let single_message_confirmation_size = bp_messages::InboundLaneData::<()>::encoded_size_hint(
 		SC::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
 		1,
@@ -312,19 +397,19 @@ fn prepare_dummy_messages_delivery_proof<SC: Chain, TC: Chain>(
 /// This function assumes that the chain that is followed by the `self_client` has
 /// bridge GRANDPA pallet deployed and it provides `best_finalized_header_id_method_name`
 /// runtime API to read the best finalized Bridged chain header.
-pub async fn read_client_state<SelfChain, BridgedHeaderHash, BridgedHeaderNumber>(
+///
+/// If `peer_client` is `None`, the value of `actual_best_finalized_peer_at_best_self` will
+/// always match the `best_finalized_peer_at_best_self`.
+pub async fn read_client_state<SelfChain, PeerChain>(
 	self_client: &Client<SelfChain>,
+	peer_client: Option<&Client<PeerChain>>,
 	best_finalized_header_id_method_name: &str,
-) -> Result<
-	ClientState<HeaderIdOf<SelfChain>, HeaderId<BridgedHeaderHash, BridgedHeaderNumber>>,
-	SubstrateError,
->
+) -> Result<ClientState<HeaderIdOf<SelfChain>, HeaderIdOf<PeerChain>>, SubstrateError>
 where
 	SelfChain: Chain,
 	SelfChain::Header: DeserializeOwned,
 	SelfChain::Index: DeserializeOwned,
-	BridgedHeaderHash: Decode,
-	BridgedHeaderNumber: Decode,
+	PeerChain: Chain,
 {
 	// let's read our state first: we need best finalized header hash on **this** chain
 	let self_best_finalized_header_hash = self_client.best_finalized_header_hash().await?;
@@ -346,16 +431,27 @@ where
 			Some(self_best_hash),
 		)
 		.await?;
-	let decoded_best_finalized_peer_on_self: (BridgedHeaderNumber, BridgedHeaderHash) =
+	let decoded_best_finalized_peer_on_self: (BlockNumberOf<PeerChain>, HashOf<PeerChain>) =
 		Decode::decode(&mut &encoded_best_finalized_peer_on_self.0[..])
 			.map_err(SubstrateError::ResponseParseFailed)?;
 	let peer_on_self_best_finalized_id =
 		HeaderId(decoded_best_finalized_peer_on_self.0, decoded_best_finalized_peer_on_self.1);
 
+	// read actual header, matching the `peer_on_self_best_finalized_id` from the peer chain
+	let actual_peer_on_self_best_finalized_id = match peer_client {
+		Some(peer_client) => {
+			let actual_peer_on_self_best_finalized =
+				peer_client.header_by_number(peer_on_self_best_finalized_id.0).await?;
+			HeaderId(peer_on_self_best_finalized_id.0, actual_peer_on_self_best_finalized.hash())
+		},
+		None => peer_on_self_best_finalized_id,
+	};
+
 	Ok(ClientState {
 		best_self: self_best_id,
 		best_finalized_self: self_best_finalized_id,
 		best_finalized_peer_at_best_self: peer_on_self_best_finalized_id,
+		actual_best_finalized_peer_at_best_self: actual_peer_on_self_best_finalized_id,
 	})
 }
 
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/messages_target.rs b/polkadot/bridges/relays/lib-substrate-relay/src/messages_target.rs
index eafc6bd3fc5f7efac68b93fbf16a7860313fc5b6..869a1d280282b8f298e95c4afa959829c749b036 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/messages_target.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/messages_target.rs
@@ -19,18 +19,22 @@
 //! <BridgedName> chain.
 
 use crate::{
-	messages_lane::{StandaloneMessagesMetrics, SubstrateMessageLane},
-	messages_source::{read_client_state, SubstrateMessagesProof},
+	messages_lane::{MessageLaneAdapter, ReceiveMessagesProofCallBuilder, SubstrateMessageLane},
+	messages_metrics::StandaloneMessagesMetrics,
+	messages_source::{ensure_messages_pallet_active, read_client_state, SubstrateMessagesProof},
 	on_demand_headers::OnDemandHeadersRelay,
+	TransactionParams,
 };
 
 use async_trait::async_trait;
-use bp_messages::{LaneId, MessageNonce, UnrewardedRelayersState};
-
+use bp_messages::{
+	storage_keys::inbound_lane_data_key, total_unrewarded_messages, InboundLaneData, LaneId,
+	MessageNonce, UnrewardedRelayersState,
+};
 use bridge_runtime_common::messages::{
 	source::FromBridgedChainMessagesDeliveryProof, target::FromBridgedChainMessagesProof,
 };
-use codec::{Decode, Encode};
+use codec::Encode;
 use frame_support::weights::{Weight, WeightToFeePolynomial};
 use messages_relay::{
 	message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf},
@@ -38,23 +42,26 @@ use messages_relay::{
 };
 use num_traits::{Bounded, Zero};
 use relay_substrate_client::{
-	BalanceOf, BlockNumberOf, Chain, Client, Error as SubstrateError, HashOf, HeaderOf, IndexOf,
-	WeightToFeeOf,
+	AccountIdOf, AccountKeyPairOf, BalanceOf, Chain, ChainWithMessages, Client,
+	Error as SubstrateError, HashOf, HeaderIdOf, IndexOf, SignParam, TransactionEra,
+	TransactionSignScheme, UnsignedTransaction, WeightToFeeOf,
 };
-use relay_utils::{relay_loop::Client as RelayClient, BlockNumberBase, HeaderId};
-use sp_core::Bytes;
-use sp_runtime::{traits::Saturating, DeserializeOwned, FixedPointNumber, FixedU128};
-use std::{convert::TryFrom, ops::RangeInclusive};
+use relay_utils::{relay_loop::Client as RelayClient, HeaderId};
+use sp_core::{Bytes, Pair};
+use sp_runtime::{traits::Saturating, FixedPointNumber, FixedU128};
+use std::{collections::VecDeque, convert::TryFrom, ops::RangeInclusive};
 
 /// Message receiving proof returned by the target Substrate node.
-pub type SubstrateMessagesReceivingProof<C> =
+pub type SubstrateMessagesDeliveryProof<C> =
 	(UnrewardedRelayersState, FromBridgedChainMessagesDeliveryProof<HashOf<C>>);
 
 /// Substrate client as Substrate messages target.
 pub struct SubstrateMessagesTarget<P: SubstrateMessageLane> {
-	client: Client<P::TargetChain>,
-	lane: P,
+	target_client: Client<P::TargetChain>,
+	source_client: Client<P::SourceChain>,
 	lane_id: LaneId,
+	relayer_id_at_source: AccountIdOf<P::SourceChain>,
+	transaction_params: TransactionParams<AccountKeyPairOf<P::TargetTransactionSignScheme>>,
 	metric_values: StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>,
 	source_to_target_headers_relay: Option<OnDemandHeadersRelay<P::SourceChain>>,
 }
@@ -62,28 +69,55 @@ pub struct SubstrateMessagesTarget<P: SubstrateMessageLane> {
 impl<P: SubstrateMessageLane> SubstrateMessagesTarget<P> {
 	/// Create new Substrate headers target.
 	pub fn new(
-		client: Client<P::TargetChain>,
-		lane: P,
+		target_client: Client<P::TargetChain>,
+		source_client: Client<P::SourceChain>,
 		lane_id: LaneId,
+		relayer_id_at_source: AccountIdOf<P::SourceChain>,
+		transaction_params: TransactionParams<AccountKeyPairOf<P::TargetTransactionSignScheme>>,
 		metric_values: StandaloneMessagesMetrics<P::SourceChain, P::TargetChain>,
 		source_to_target_headers_relay: Option<OnDemandHeadersRelay<P::SourceChain>>,
 	) -> Self {
 		SubstrateMessagesTarget {
-			client,
-			lane,
+			target_client,
+			source_client,
 			lane_id,
+			relayer_id_at_source,
+			transaction_params,
 			metric_values,
 			source_to_target_headers_relay,
 		}
 	}
+
+	/// Read inbound lane state from the on-chain storage at given block.
+	async fn inbound_lane_data(
+		&self,
+		id: TargetHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<Option<InboundLaneData<AccountIdOf<P::SourceChain>>>, SubstrateError> {
+		self.target_client
+			.storage_value(
+				inbound_lane_data_key(
+					P::SourceChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
+					&self.lane_id,
+				),
+				Some(id.1),
+			)
+			.await
+	}
+
+	/// Ensure that the messages pallet at target chain is active.
+	async fn ensure_pallet_active(&self) -> Result<(), SubstrateError> {
+		ensure_messages_pallet_active::<P::TargetChain, P::SourceChain>(&self.target_client).await
+	}
 }
 
 impl<P: SubstrateMessageLane> Clone for SubstrateMessagesTarget<P> {
 	fn clone(&self) -> Self {
 		Self {
-			client: self.client.clone(),
-			lane: self.lane.clone(),
+			target_client: self.target_client.clone(),
+			source_client: self.source_client.clone(),
 			lane_id: self.lane_id,
+			relayer_id_at_source: self.relayer_id_at_source.clone(),
+			transaction_params: self.transaction_params.clone(),
 			metric_values: self.metric_values.clone(),
 			source_to_target_headers_relay: self.source_to_target_headers_relay.clone(),
 		}
@@ -95,115 +129,98 @@ impl<P: SubstrateMessageLane> RelayClient for SubstrateMessagesTarget<P> {
 	type Error = SubstrateError;
 
 	async fn reconnect(&mut self) -> Result<(), SubstrateError> {
-		self.client.reconnect().await
+		self.target_client.reconnect().await?;
+		self.source_client.reconnect().await
 	}
 }
 
 #[async_trait]
-impl<P> TargetClient<P::MessageLane> for SubstrateMessagesTarget<P>
+impl<P: SubstrateMessageLane> TargetClient<MessageLaneAdapter<P>> for SubstrateMessagesTarget<P>
 where
-	P: SubstrateMessageLane,
-	P::SourceChain: Chain<
-		Hash = <P::MessageLane as MessageLane>::SourceHeaderHash,
-		BlockNumber = <P::MessageLane as MessageLane>::SourceHeaderNumber,
-		Balance = <P::MessageLane as MessageLane>::SourceChainBalance,
-	>,
-	BalanceOf<P::SourceChain>: TryFrom<BalanceOf<P::TargetChain>> + Bounded,
-	P::TargetChain: Chain<
-		Hash = <P::MessageLane as MessageLane>::TargetHeaderHash,
-		BlockNumber = <P::MessageLane as MessageLane>::TargetHeaderNumber,
-	>,
-	IndexOf<P::TargetChain>: DeserializeOwned,
-	HashOf<P::TargetChain>: Copy,
-	BlockNumberOf<P::TargetChain>: Copy,
-	HeaderOf<P::TargetChain>: DeserializeOwned,
-	BlockNumberOf<P::TargetChain>: BlockNumberBase,
-	P::MessageLane: MessageLane<
-		MessagesProof = SubstrateMessagesProof<P::SourceChain>,
-		MessagesReceivingProof = SubstrateMessagesReceivingProof<P::TargetChain>,
-	>,
-	<P::MessageLane as MessageLane>::SourceHeaderNumber: Decode,
-	<P::MessageLane as MessageLane>::SourceHeaderHash: Decode,
+	AccountIdOf<P::TargetChain>:
+		From<<AccountKeyPairOf<P::TargetTransactionSignScheme> as Pair>::Public>,
+	P::TargetTransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
+	BalanceOf<P::SourceChain>: TryFrom<BalanceOf<P::TargetChain>>,
 {
-	async fn state(&self) -> Result<TargetClientState<P::MessageLane>, SubstrateError> {
+	async fn state(&self) -> Result<TargetClientState<MessageLaneAdapter<P>>, SubstrateError> {
 		// we can't continue to deliver messages if target node is out of sync, because
 		// it may have already received (some of) messages that we're going to deliver
-		self.client.ensure_synced().await?;
-
-		read_client_state::<
-			_,
-			<P::MessageLane as MessageLane>::SourceHeaderHash,
-			<P::MessageLane as MessageLane>::SourceHeaderNumber,
-		>(&self.client, P::BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET)
+		self.target_client.ensure_synced().await?;
+		// we can't relay messages if messages pallet at target chain is halted
+		self.ensure_pallet_active().await?;
+
+		read_client_state(
+			&self.target_client,
+			Some(&self.source_client),
+			P::SourceChain::BEST_FINALIZED_HEADER_ID_METHOD,
+		)
 		.await
 	}
 
 	async fn latest_received_nonce(
 		&self,
-		id: TargetHeaderIdOf<P::MessageLane>,
-	) -> Result<(TargetHeaderIdOf<P::MessageLane>, MessageNonce), SubstrateError> {
-		let encoded_response = self
-			.client
-			.state_call(
-				P::INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD.into(),
-				Bytes(self.lane_id.encode()),
-				Some(id.1),
-			)
-			.await?;
-		let latest_received_nonce: MessageNonce = Decode::decode(&mut &encoded_response.0[..])
-			.map_err(SubstrateError::ResponseParseFailed)?;
+		id: TargetHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<(TargetHeaderIdOf<MessageLaneAdapter<P>>, MessageNonce), SubstrateError> {
+		// lane data missing from the storage is fine until first message is received
+		let latest_received_nonce = self
+			.inbound_lane_data(id)
+			.await?
+			.map(|data| data.last_delivered_nonce())
+			.unwrap_or(0);
 		Ok((id, latest_received_nonce))
 	}
 
 	async fn latest_confirmed_received_nonce(
 		&self,
-		id: TargetHeaderIdOf<P::MessageLane>,
-	) -> Result<(TargetHeaderIdOf<P::MessageLane>, MessageNonce), SubstrateError> {
-		let encoded_response = self
-			.client
-			.state_call(
-				P::INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD.into(),
-				Bytes(self.lane_id.encode()),
-				Some(id.1),
-			)
-			.await?;
-		let latest_received_nonce: MessageNonce = Decode::decode(&mut &encoded_response.0[..])
-			.map_err(SubstrateError::ResponseParseFailed)?;
-		Ok((id, latest_received_nonce))
+		id: TargetHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<(TargetHeaderIdOf<MessageLaneAdapter<P>>, MessageNonce), SubstrateError> {
+		// lane data missing from the storage is fine until first message is received
+		let last_confirmed_nonce = self
+			.inbound_lane_data(id)
+			.await?
+			.map(|data| data.last_confirmed_nonce)
+			.unwrap_or(0);
+		Ok((id, last_confirmed_nonce))
 	}
 
 	async fn unrewarded_relayers_state(
 		&self,
-		id: TargetHeaderIdOf<P::MessageLane>,
-	) -> Result<(TargetHeaderIdOf<P::MessageLane>, UnrewardedRelayersState), SubstrateError> {
-		let encoded_response = self
-			.client
-			.state_call(
-				P::INBOUND_LANE_UNREWARDED_RELAYERS_STATE.into(),
-				Bytes(self.lane_id.encode()),
-				Some(id.1),
-			)
-			.await?;
-		let unrewarded_relayers_state: UnrewardedRelayersState =
-			Decode::decode(&mut &encoded_response.0[..])
-				.map_err(SubstrateError::ResponseParseFailed)?;
+		id: TargetHeaderIdOf<MessageLaneAdapter<P>>,
+	) -> Result<(TargetHeaderIdOf<MessageLaneAdapter<P>>, UnrewardedRelayersState), SubstrateError>
+	{
+		let relayers = self
+			.inbound_lane_data(id)
+			.await?
+			.map(|data| data.relayers)
+			.unwrap_or_else(|| VecDeque::new());
+		let unrewarded_relayers_state = bp_messages::UnrewardedRelayersState {
+			unrewarded_relayer_entries: relayers.len() as _,
+			messages_in_oldest_entry: relayers
+				.front()
+				.map(|entry| 1 + entry.messages.end - entry.messages.begin)
+				.unwrap_or(0),
+			total_messages: total_unrewarded_messages(&relayers).unwrap_or(MessageNonce::MAX),
+		};
 		Ok((id, unrewarded_relayers_state))
 	}
 
 	async fn prove_messages_receiving(
 		&self,
-		id: TargetHeaderIdOf<P::MessageLane>,
+		id: TargetHeaderIdOf<MessageLaneAdapter<P>>,
 	) -> Result<
-		(TargetHeaderIdOf<P::MessageLane>, <P::MessageLane as MessageLane>::MessagesReceivingProof),
+		(
+			TargetHeaderIdOf<MessageLaneAdapter<P>>,
+			<MessageLaneAdapter<P> as MessageLane>::MessagesReceivingProof,
+		),
 		SubstrateError,
 	> {
 		let (id, relayers_state) = self.unrewarded_relayers_state(id).await?;
-		let inbound_data_key = pallet_bridge_messages::storage_keys::inbound_lane_data_key(
-			P::MESSAGE_PALLET_NAME_AT_TARGET,
+		let inbound_data_key = bp_messages::storage_keys::inbound_lane_data_key(
+			P::SourceChain::WITH_CHAIN_MESSAGES_PALLET_NAME,
 			&self.lane_id,
 		);
 		let proof = self
-			.client
+			.target_client
 			.prove_storage(vec![inbound_data_key], id.1)
 			.await?
 			.iter_nodes()
@@ -218,22 +235,31 @@ where
 
 	async fn submit_messages_proof(
 		&self,
-		generated_at_header: SourceHeaderIdOf<P::MessageLane>,
+		_generated_at_header: SourceHeaderIdOf<MessageLaneAdapter<P>>,
 		nonces: RangeInclusive<MessageNonce>,
-		proof: <P::MessageLane as MessageLane>::MessagesProof,
+		proof: <MessageLaneAdapter<P> as MessageLane>::MessagesProof,
 	) -> Result<RangeInclusive<MessageNonce>, SubstrateError> {
-		let lane = self.lane.clone();
+		let genesis_hash = *self.target_client.genesis_hash();
+		let transaction_params = self.transaction_params.clone();
+		let relayer_id_at_source = self.relayer_id_at_source.clone();
 		let nonces_clone = nonces.clone();
-		self.client
+		let (spec_version, transaction_version) =
+			self.target_client.simple_runtime_version().await?;
+		self.target_client
 			.submit_signed_extrinsic(
-				self.lane.target_transactions_author(),
+				self.transaction_params.signer.public().into(),
 				move |best_block_id, transaction_nonce| {
-					lane.make_messages_delivery_transaction(
+					make_messages_delivery_transaction::<P>(
+						spec_version,
+						transaction_version,
+						&genesis_hash,
+						&transaction_params,
 						best_block_id,
 						transaction_nonce,
-						generated_at_header,
+						relayer_id_at_source,
 						nonces_clone,
 						proof,
+						true,
 					)
 				},
 			)
@@ -241,7 +267,7 @@ where
 		Ok(nonces)
 	}
 
-	async fn require_source_header_on_target(&self, id: SourceHeaderIdOf<P::MessageLane>) {
+	async fn require_source_header_on_target(&self, id: SourceHeaderIdOf<MessageLaneAdapter<P>>) {
 		if let Some(ref source_to_target_headers_relay) = self.source_to_target_headers_relay {
 			source_to_target_headers_relay.require_finalized_header(id).await;
 		}
@@ -253,7 +279,7 @@ where
 		total_prepaid_nonces: MessageNonce,
 		total_dispatch_weight: Weight,
 		total_size: u32,
-	) -> Result<<P::MessageLane as MessageLane>::SourceChainBalance, SubstrateError> {
+	) -> Result<<MessageLaneAdapter<P> as MessageLane>::SourceChainBalance, SubstrateError> {
 		let conversion_rate =
 			self.metric_values.target_to_source_conversion_rate().await.ok_or_else(|| {
 				SubstrateError::Custom(format!(
@@ -263,19 +289,26 @@ where
 				))
 			})?;
 
+		let (spec_version, transaction_version) =
+			self.target_client.simple_runtime_version().await?;
 		// Prepare 'dummy' delivery transaction - we only care about its length and dispatch weight.
-		let delivery_tx = self.lane.make_messages_delivery_transaction(
+		let delivery_tx = make_messages_delivery_transaction::<P>(
+			spec_version,
+			transaction_version,
+			self.target_client.genesis_hash(),
+			&self.transaction_params,
 			HeaderId(Default::default(), Default::default()),
 			Zero::zero(),
-			HeaderId(Default::default(), Default::default()),
+			self.relayer_id_at_source.clone(),
 			nonces.clone(),
 			prepare_dummy_messages_proof::<P::SourceChain>(
 				nonces.clone(),
 				total_dispatch_weight,
 				total_size,
 			),
-		);
-		let delivery_tx_fee = self.client.estimate_extrinsic_fee(delivery_tx).await?;
+			false,
+		)?;
+		let delivery_tx_fee = self.target_client.estimate_extrinsic_fee(delivery_tx).await?;
 		let inclusion_fee_in_target_tokens = delivery_tx_fee.inclusion_fee();
 
 		// The pre-dispatch cost of delivery transaction includes additional fee to cover dispatch
@@ -297,23 +330,29 @@ where
 		let expected_refund_in_target_tokens = if total_prepaid_nonces != 0 {
 			const WEIGHT_DIFFERENCE: Weight = 100;
 
+			let (spec_version, transaction_version) =
+				self.target_client.simple_runtime_version().await?;
 			let larger_dispatch_weight = total_dispatch_weight.saturating_add(WEIGHT_DIFFERENCE);
-			let larger_delivery_tx_fee = self
-				.client
-				.estimate_extrinsic_fee(self.lane.make_messages_delivery_transaction(
-					HeaderId(Default::default(), Default::default()),
-					Zero::zero(),
-					HeaderId(Default::default(), Default::default()),
+			let dummy_tx = make_messages_delivery_transaction::<P>(
+				spec_version,
+				transaction_version,
+				self.target_client.genesis_hash(),
+				&self.transaction_params,
+				HeaderId(Default::default(), Default::default()),
+				Zero::zero(),
+				self.relayer_id_at_source.clone(),
+				nonces.clone(),
+				prepare_dummy_messages_proof::<P::SourceChain>(
 					nonces.clone(),
-					prepare_dummy_messages_proof::<P::SourceChain>(
-						nonces.clone(),
-						larger_dispatch_weight,
-						total_size,
-					),
-				))
-				.await?;
+					larger_dispatch_weight,
+					total_size,
+				),
+				false,
+			)?;
+			let larger_delivery_tx_fee =
+				self.target_client.estimate_extrinsic_fee(dummy_tx).await?;
 
-			compute_prepaid_messages_refund::<P>(
+			compute_prepaid_messages_refund::<P::TargetChain>(
 				total_prepaid_nonces,
 				compute_fee_multiplier::<P::TargetChain>(
 					delivery_tx_fee.adjusted_weight_fee,
@@ -359,6 +398,45 @@ where
 	}
 }
 
+/// Make messages delivery transaction from given proof.
+#[allow(clippy::too_many_arguments)]
+fn make_messages_delivery_transaction<P: SubstrateMessageLane>(
+	spec_version: u32,
+	transaction_version: u32,
+	target_genesis_hash: &HashOf<P::TargetChain>,
+	target_transaction_params: &TransactionParams<AccountKeyPairOf<P::TargetTransactionSignScheme>>,
+	target_best_block_id: HeaderIdOf<P::TargetChain>,
+	transaction_nonce: IndexOf<P::TargetChain>,
+	relayer_id_at_source: AccountIdOf<P::SourceChain>,
+	nonces: RangeInclusive<MessageNonce>,
+	proof: SubstrateMessagesProof<P::SourceChain>,
+	trace_call: bool,
+) -> Result<Bytes, SubstrateError>
+where
+	P::TargetTransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
+{
+	let messages_count = nonces.end() - nonces.start() + 1;
+	let dispatch_weight = proof.0;
+	let call = P::ReceiveMessagesProofCallBuilder::build_receive_messages_proof_call(
+		relayer_id_at_source,
+		proof,
+		messages_count as _,
+		dispatch_weight,
+		trace_call,
+	);
+	Ok(Bytes(
+		P::TargetTransactionSignScheme::sign_transaction(SignParam {
+			spec_version,
+			transaction_version,
+			genesis_hash: *target_genesis_hash,
+			signer: target_transaction_params.signer.clone(),
+			era: TransactionEra::new(target_best_block_id, target_transaction_params.mortality),
+			unsigned: UnsignedTransaction::new(call.into(), transaction_nonce),
+		})?
+		.encode(),
+	))
+}
+
 /// Prepare 'dummy' messages proof that will compose the delivery transaction.
 ///
 /// We don't care about proof actually being the valid proof, because its validity doesn't
@@ -425,80 +503,20 @@ fn compute_fee_multiplier<C: Chain>(
 
 /// Compute fee that will be refunded to the relayer because dispatch of `total_prepaid_nonces`
 /// messages has been paid at the source chain.
-fn compute_prepaid_messages_refund<P: SubstrateMessageLane>(
+fn compute_prepaid_messages_refund<C: ChainWithMessages>(
 	total_prepaid_nonces: MessageNonce,
 	fee_multiplier: FixedU128,
-) -> BalanceOf<P::TargetChain> {
-	fee_multiplier.saturating_mul_int(WeightToFeeOf::<P::TargetChain>::calc(
-		&P::PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN.saturating_mul(total_prepaid_nonces),
+) -> BalanceOf<C> {
+	fee_multiplier.saturating_mul_int(WeightToFeeOf::<C>::calc(
+		&C::PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN.saturating_mul(total_prepaid_nonces),
 	))
 }
 
 #[cfg(test)]
 mod tests {
 	use super::*;
-	use relay_rococo_client::{Rococo, SigningParams as RococoSigningParams};
-	use relay_wococo_client::{SigningParams as WococoSigningParams, Wococo};
-
-	#[derive(Clone)]
-	struct TestSubstrateMessageLane;
-
-	impl SubstrateMessageLane for TestSubstrateMessageLane {
-		type MessageLane = crate::messages_lane::SubstrateMessageLaneToSubstrate<
-			Rococo,
-			RococoSigningParams,
-			Wococo,
-			WococoSigningParams,
-		>;
-
-		const OUTBOUND_LANE_MESSAGE_DETAILS_METHOD: &'static str = "";
-		const OUTBOUND_LANE_LATEST_GENERATED_NONCE_METHOD: &'static str = "";
-		const OUTBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = "";
-
-		const INBOUND_LANE_LATEST_RECEIVED_NONCE_METHOD: &'static str = "";
-		const INBOUND_LANE_LATEST_CONFIRMED_NONCE_METHOD: &'static str = "";
-		const INBOUND_LANE_UNREWARDED_RELAYERS_STATE: &'static str = "";
-
-		const BEST_FINALIZED_SOURCE_HEADER_ID_AT_TARGET: &'static str = "";
-		const BEST_FINALIZED_TARGET_HEADER_ID_AT_SOURCE: &'static str = "";
-
-		const MESSAGE_PALLET_NAME_AT_SOURCE: &'static str = "";
-		const MESSAGE_PALLET_NAME_AT_TARGET: &'static str = "";
-
-		const PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN: Weight = 100_000;
-
-		type SourceChain = Rococo;
-		type TargetChain = Wococo;
-
-		fn source_transactions_author(&self) -> bp_rococo::AccountId {
-			unreachable!()
-		}
-
-		fn make_messages_receiving_proof_transaction(
-			&self,
-			_best_block_id: SourceHeaderIdOf<Self::MessageLane>,
-			_transaction_nonce: IndexOf<Rococo>,
-			_generated_at_block: TargetHeaderIdOf<Self::MessageLane>,
-			_proof: <Self::MessageLane as MessageLane>::MessagesReceivingProof,
-		) -> Bytes {
-			unreachable!()
-		}
-
-		fn target_transactions_author(&self) -> bp_wococo::AccountId {
-			unreachable!()
-		}
-
-		fn make_messages_delivery_transaction(
-			&self,
-			_best_block_id: TargetHeaderIdOf<Self::MessageLane>,
-			_transaction_nonce: IndexOf<Wococo>,
-			_generated_at_header: SourceHeaderIdOf<Self::MessageLane>,
-			_nonces: RangeInclusive<MessageNonce>,
-			_proof: <Self::MessageLane as MessageLane>::MessagesProof,
-		) -> Bytes {
-			unreachable!()
-		}
-	}
+	use relay_rococo_client::Rococo;
+	use relay_wococo_client::Wococo;
 
 	#[test]
 	fn prepare_dummy_messages_proof_works() {
@@ -556,11 +574,10 @@ mod tests {
 	#[test]
 	fn compute_prepaid_messages_refund_returns_sane_results() {
 		assert!(
-			compute_prepaid_messages_refund::<TestSubstrateMessageLane>(
+			compute_prepaid_messages_refund::<Wococo>(
 				10,
 				FixedU128::saturating_from_rational(110, 100),
-			) > (10 * TestSubstrateMessageLane::PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_TARGET_CHAIN)
-				.into()
+			) > (10 * Wococo::PAY_INBOUND_DISPATCH_FEE_WEIGHT_AT_CHAIN).into()
 		);
 	}
 }
diff --git a/polkadot/bridges/relays/lib-substrate-relay/src/on_demand_headers.rs b/polkadot/bridges/relays/lib-substrate-relay/src/on_demand_headers.rs
index ee141866eb97d5d4ff3c82874b4d9b5296b1c88c..c1401a28a6dd861826aa02cbc3deaef256c8b43c 100644
--- a/polkadot/bridges/relays/lib-substrate-relay/src/on_demand_headers.rs
+++ b/polkadot/bridges/relays/lib-substrate-relay/src/on_demand_headers.rs
@@ -16,31 +16,24 @@
 
 //! On-demand Substrate -> Substrate headers relay.
 
-use std::fmt::Debug;
-
 use async_std::sync::{Arc, Mutex};
 use futures::{select, FutureExt};
-use num_traits::{CheckedSub, One, Zero};
+use num_traits::{One, Zero};
 
-use finality_relay::{
-	FinalitySyncParams, FinalitySyncPipeline, SourceClient as FinalitySourceClient, SourceHeader,
-	TargetClient as FinalityTargetClient,
-};
+use finality_relay::{FinalitySyncParams, SourceHeader, TargetClient as FinalityTargetClient};
 use relay_substrate_client::{
-	finality_source::{FinalitySource as SubstrateFinalitySource, RequiredHeaderNumberRef},
-	Chain, Client, HeaderIdOf, SyncHeader,
+	AccountIdOf, AccountKeyPairOf, BlockNumberOf, Chain, Client, HeaderIdOf, HeaderOf, SyncHeader,
+	TransactionSignScheme,
 };
 use relay_utils::{
-	metrics::MetricsParams, relay_loop::Client as RelayClient, BlockNumberBase, FailedClient,
-	MaybeConnectionError,
+	metrics::MetricsParams, relay_loop::Client as RelayClient, FailedClient, MaybeConnectionError,
 };
 
 use crate::{
-	finality_pipeline::{
-		SubstrateFinalitySyncPipeline, SubstrateFinalityToSubstrate, RECENT_FINALITY_PROOFS_LIMIT,
-	},
+	finality_pipeline::{SubstrateFinalitySyncPipeline, RECENT_FINALITY_PROOFS_LIMIT},
+	finality_source::{RequiredHeaderNumberRef, SubstrateFinalitySource},
 	finality_target::SubstrateFinalityTarget,
-	STALL_TIMEOUT,
+	TransactionParams, STALL_TIMEOUT,
 };
 
 /// On-demand Substrate <-> Substrate headers relay.
@@ -58,41 +51,27 @@ pub struct OnDemandHeadersRelay<SourceChain: Chain> {
 
 impl<SourceChain: Chain> OnDemandHeadersRelay<SourceChain> {
 	/// Create new on-demand headers relay.
-	pub fn new<TargetChain: Chain, TargetSign, P>(
-		source_client: Client<SourceChain>,
-		target_client: Client<TargetChain>,
-		target_transactions_mortality: Option<u32>,
-		pipeline: P,
-		maximal_headers_difference: SourceChain::BlockNumber,
+	pub fn new<P: SubstrateFinalitySyncPipeline<SourceChain = SourceChain>>(
+		source_client: Client<P::SourceChain>,
+		target_client: Client<P::TargetChain>,
+		target_transaction_params: TransactionParams<AccountKeyPairOf<P::TransactionSignScheme>>,
 		only_mandatory_headers: bool,
 	) -> Self
 	where
-		SourceChain: Chain + Debug,
-		SourceChain::BlockNumber: BlockNumberBase,
-		TargetChain: Chain + Debug,
-		TargetChain::BlockNumber: BlockNumberBase,
-		TargetSign: Clone + Send + Sync + 'static,
-		P: SubstrateFinalitySyncPipeline<
-			FinalitySyncPipeline = SubstrateFinalityToSubstrate<
-				SourceChain,
-				TargetChain,
-				TargetSign,
-			>,
-			TargetChain = TargetChain,
-		>,
+		AccountIdOf<P::TargetChain>:
+			From<<AccountKeyPairOf<P::TransactionSignScheme> as sp_core::Pair>::Public>,
+		P::TransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
 	{
 		let required_header_number = Arc::new(Mutex::new(Zero::zero()));
 		let this = OnDemandHeadersRelay {
-			relay_task_name: on_demand_headers_relay_name::<SourceChain, TargetChain>(),
+			relay_task_name: on_demand_headers_relay_name::<P::SourceChain, P::TargetChain>(),
 			required_header_number: required_header_number.clone(),
 		};
 		async_std::task::spawn(async move {
-			background_task(
+			background_task::<P>(
 				source_client,
 				target_client,
-				target_transactions_mortality,
-				pipeline,
-				maximal_headers_difference,
+				target_transaction_params,
 				only_mandatory_headers,
 				required_header_number,
 			)
@@ -120,35 +99,25 @@ impl<SourceChain: Chain> OnDemandHeadersRelay<SourceChain> {
 }
 
 /// Background task that is responsible for starting headers relay.
-async fn background_task<SourceChain, TargetChain, TargetSign, P>(
-	source_client: Client<SourceChain>,
-	target_client: Client<TargetChain>,
-	target_transactions_mortality: Option<u32>,
-	pipeline: P,
-	maximal_headers_difference: SourceChain::BlockNumber,
+async fn background_task<P: SubstrateFinalitySyncPipeline>(
+	source_client: Client<P::SourceChain>,
+	target_client: Client<P::TargetChain>,
+	target_transaction_params: TransactionParams<AccountKeyPairOf<P::TransactionSignScheme>>,
 	only_mandatory_headers: bool,
-	required_header_number: RequiredHeaderNumberRef<SourceChain>,
+	required_header_number: RequiredHeaderNumberRef<P::SourceChain>,
 ) where
-	SourceChain: Chain + Debug,
-	SourceChain::BlockNumber: BlockNumberBase,
-	TargetChain: Chain + Debug,
-	TargetChain::BlockNumber: BlockNumberBase,
-	TargetSign: Clone + Send + Sync + 'static,
-	P: SubstrateFinalitySyncPipeline<
-		FinalitySyncPipeline = SubstrateFinalityToSubstrate<SourceChain, TargetChain, TargetSign>,
-		TargetChain = TargetChain,
-	>,
+	AccountIdOf<P::TargetChain>:
+		From<<AccountKeyPairOf<P::TransactionSignScheme> as sp_core::Pair>::Public>,
+	P::TransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
 {
-	let relay_task_name = on_demand_headers_relay_name::<SourceChain, TargetChain>();
-	let mut finality_source = SubstrateFinalitySource::<
-		_,
-		SubstrateFinalityToSubstrate<SourceChain, TargetChain, TargetSign>,
-	>::new(source_client.clone(), Some(required_header_number.clone()));
-	let mut finality_target = SubstrateFinalityTarget::new(
-		target_client.clone(),
-		pipeline.clone(),
-		target_transactions_mortality,
+	let relay_task_name = on_demand_headers_relay_name::<P::SourceChain, P::TargetChain>();
+	let target_transactions_mortality = target_transaction_params.mortality;
+	let mut finality_source = SubstrateFinalitySource::<P>::new(
+		source_client.clone(),
+		Some(required_header_number.clone()),
 	);
+	let mut finality_target =
+		SubstrateFinalityTarget::new(target_client.clone(), target_transaction_params);
 	let mut latest_non_mandatory_at_source = Zero::zero();
 
 	let mut restart_relay = true;
@@ -157,7 +126,7 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 
 	loop {
 		select! {
-			_ = async_std::task::sleep(TargetChain::AVERAGE_BLOCK_INTERVAL).fuse() => {},
+			_ = async_std::task::sleep(P::TargetChain::AVERAGE_BLOCK_INTERVAL).fuse() => {},
 			_ = finality_relay_task => {
 				// this should never happen in practice given the current code
 				restart_relay = true;
@@ -179,12 +148,8 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 		}
 
 		// read best finalized source header number from target
-		let best_finalized_source_header_at_target = best_finalized_source_header_at_target::<
-			SourceChain,
-			_,
-			_,
-		>(&finality_target, &relay_task_name)
-		.await;
+		let best_finalized_source_header_at_target =
+			best_finalized_source_header_at_target::<P>(&finality_target, &relay_task_name).await;
 		if matches!(best_finalized_source_header_at_target, Err(ref e) if e.is_connection_error()) {
 			relay_utils::relay_loop::reconnect_failed_client(
 				FailedClient::Target,
@@ -197,15 +162,28 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 		}
 
 		// submit mandatory header if some headers are missing
+		let best_finalized_source_header_at_source_fmt =
+			format!("{:?}", best_finalized_source_header_at_source);
 		let best_finalized_source_header_at_target_fmt =
 			format!("{:?}", best_finalized_source_header_at_target);
-		let mandatory_scan_range = mandatory_headers_scan_range::<SourceChain>(
+		let required_header_number_value = *required_header_number.lock().await;
+		let mandatory_scan_range = mandatory_headers_scan_range::<P::SourceChain>(
 			best_finalized_source_header_at_source.ok(),
 			best_finalized_source_header_at_target.ok(),
-			maximal_headers_difference,
-			&required_header_number,
+			required_header_number_value,
 		)
 		.await;
+
+		log::trace!(
+			target: "bridge",
+			"Mandatory headers scan range in {}: ({:?}, {:?}, {:?}) -> {:?}",
+			relay_task_name,
+			required_header_number_value,
+			best_finalized_source_header_at_source_fmt,
+			best_finalized_source_header_at_target_fmt,
+			mandatory_scan_range,
+		);
+
 		if let Some(mandatory_scan_range) = mandatory_scan_range {
 			let relay_mandatory_header_result = relay_mandatory_header_from_range(
 				&finality_source,
@@ -224,8 +202,25 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 					// there are no (or we don't need to relay them) mandatory headers in the range
 					// => to avoid scanning the same headers over and over again, remember that
 					latest_non_mandatory_at_source = mandatory_scan_range.1;
+
+					log::trace!(
+						target: "bridge",
+						"No mandatory {} headers in the range {:?} of {} relay",
+						P::SourceChain::NAME,
+						mandatory_scan_range,
+						relay_task_name,
+					);
 				},
-				Err(e) =>
+				Err(e) => {
+					log::warn!(
+						target: "bridge",
+						"Failed to scan mandatory {} headers range in {} relay (range: {:?}): {:?}",
+						P::SourceChain::NAME,
+						relay_task_name,
+						mandatory_scan_range,
+						e,
+					);
+
 					if e.is_connection_error() {
 						relay_utils::relay_loop::reconnect_failed_client(
 							FailedClient::Source,
@@ -235,23 +230,43 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 						)
 						.await;
 						continue
-					},
+					}
+				},
 			}
 		}
 
 		// start/restart relay
 		if restart_relay {
+			let stall_timeout = relay_substrate_client::transaction_stall_timeout(
+				target_transactions_mortality,
+				P::TargetChain::AVERAGE_BLOCK_INTERVAL,
+				STALL_TIMEOUT,
+			);
+
+			log::info!(
+				target: "bridge",
+				"Starting {} relay\n\t\
+					Only mandatory headers: {}\n\t\
+					Tx mortality: {:?} (~{}m)\n\t\
+					Stall timeout: {:?}",
+				relay_task_name,
+				only_mandatory_headers,
+				target_transactions_mortality,
+				stall_timeout.as_secs_f64() / 60.0f64,
+				stall_timeout,
+			);
+
 			finality_relay_task.set(
 				finality_relay::run(
 					finality_source.clone(),
 					finality_target.clone(),
 					FinalitySyncParams {
 						tick: std::cmp::max(
-							SourceChain::AVERAGE_BLOCK_INTERVAL,
-							TargetChain::AVERAGE_BLOCK_INTERVAL,
+							P::SourceChain::AVERAGE_BLOCK_INTERVAL,
+							P::TargetChain::AVERAGE_BLOCK_INTERVAL,
 						),
 						recent_finality_proofs_limit: RECENT_FINALITY_PROOFS_LIMIT,
-						stall_timeout: STALL_TIMEOUT,
+						stall_timeout,
 						only_mandatory_headers,
 					},
 					MetricsParams::disabled(),
@@ -270,11 +285,8 @@ async fn background_task<SourceChain, TargetChain, TargetSign, P>(
 async fn mandatory_headers_scan_range<C: Chain>(
 	best_finalized_source_header_at_source: Option<C::BlockNumber>,
 	best_finalized_source_header_at_target: Option<C::BlockNumber>,
-	maximal_headers_difference: C::BlockNumber,
-	required_header_number: &RequiredHeaderNumberRef<C>,
+	required_header_number: BlockNumberOf<C>,
 ) -> Option<(C::BlockNumber, C::BlockNumber)> {
-	let required_header_number = *required_header_number.lock().await;
-
 	// if we have been unable to read header number from the target, then let's assume
 	// that it is the same as required header number. Otherwise we risk submitting
 	// unneeded transactions
@@ -286,23 +298,8 @@ async fn mandatory_headers_scan_range<C: Chain>(
 	let best_finalized_source_header_at_source =
 		best_finalized_source_header_at_source.unwrap_or(best_finalized_source_header_at_target);
 
-	// if there are too many source headers missing from the target node, sync mandatory
-	// headers to target
-	//
-	// why do we need that? When complex headers+messages relay is used, it'll normally only relay
-	// headers when there are undelivered messages/confirmations. But security model of the
-	// `pallet-bridge-grandpa` module relies on the fact that headers are synced in real-time and
-	// that it'll see authorities-change header before unbonding period will end for previous
-	// authorities set.
-	let current_headers_difference = best_finalized_source_header_at_source
-		.checked_sub(&best_finalized_source_header_at_target)
-		.unwrap_or_else(Zero::zero);
-	if current_headers_difference <= maximal_headers_difference {
-		return None
-	}
-
-	// if relay is already asked to sync headers, don't do anything yet
-	if required_header_number > best_finalized_source_header_at_target {
+	// if relay is already asked to sync more headers than we have at source, don't do anything yet
+	if required_header_number >= best_finalized_source_header_at_source {
 		return None
 	}
 
@@ -316,17 +313,13 @@ async fn mandatory_headers_scan_range<C: Chain>(
 /// it.
 ///
 /// Returns `true` if header was found and (asked to be) relayed and `false` otherwise.
-async fn relay_mandatory_header_from_range<SourceChain: Chain, P>(
-	finality_source: &SubstrateFinalitySource<SourceChain, P>,
-	required_header_number: &RequiredHeaderNumberRef<SourceChain>,
+async fn relay_mandatory_header_from_range<P: SubstrateFinalitySyncPipeline>(
+	finality_source: &SubstrateFinalitySource<P>,
+	required_header_number: &RequiredHeaderNumberRef<P::SourceChain>,
 	best_finalized_source_header_at_target: String,
-	range: (SourceChain::BlockNumber, SourceChain::BlockNumber),
+	range: (BlockNumberOf<P::SourceChain>, BlockNumberOf<P::SourceChain>),
 	relay_task_name: &str,
-) -> Result<bool, relay_substrate_client::Error>
-where
-	SubstrateFinalitySource<SourceChain, P>: FinalitySourceClient<P>,
-	P: FinalitySyncPipeline<Number = SourceChain::BlockNumber>,
-{
+) -> Result<bool, relay_substrate_client::Error> {
 	// search for mandatory header first
 	let mandatory_source_header_number =
 		find_mandatory_header_in_range(finality_source, range).await?;
@@ -347,7 +340,7 @@ where
 	log::trace!(
 		target: "bridge",
 		"Too many {} headers missing at target in {} relay ({} vs {}). Going to sync up to the mandatory {}",
-		SourceChain::NAME,
+		P::SourceChain::NAME,
 		relay_task_name,
 		best_finalized_source_header_at_target,
 		range.1,
@@ -361,14 +354,10 @@ where
 /// Read best finalized source block number from source client.
 ///
 /// Returns `None` if we have failed to read the number.
-async fn best_finalized_source_header_at_source<SourceChain: Chain, P>(
-	finality_source: &SubstrateFinalitySource<SourceChain, P>,
+async fn best_finalized_source_header_at_source<P: SubstrateFinalitySyncPipeline>(
+	finality_source: &SubstrateFinalitySource<P>,
 	relay_task_name: &str,
-) -> Result<SourceChain::BlockNumber, relay_substrate_client::Error>
-where
-	SubstrateFinalitySource<SourceChain, P>: FinalitySourceClient<P>,
-	P: FinalitySyncPipeline<Number = SourceChain::BlockNumber>,
-{
+) -> Result<BlockNumberOf<P::SourceChain>, relay_substrate_client::Error> {
 	finality_source.on_chain_best_finalized_block_number().await.map_err(|error| {
 		log::error!(
 			target: "bridge",
@@ -384,41 +373,41 @@ where
 /// Read best finalized source block number from target client.
 ///
 /// Returns `None` if we have failed to read the number.
-async fn best_finalized_source_header_at_target<SourceChain: Chain, TargetChain: Chain, P>(
-	finality_target: &SubstrateFinalityTarget<TargetChain, P>,
+async fn best_finalized_source_header_at_target<P: SubstrateFinalitySyncPipeline>(
+	finality_target: &SubstrateFinalityTarget<P>,
 	relay_task_name: &str,
-) -> Result<SourceChain::BlockNumber, <SubstrateFinalityTarget<TargetChain, P> as RelayClient>::Error>
+) -> Result<BlockNumberOf<P::SourceChain>, <SubstrateFinalityTarget<P> as RelayClient>::Error>
 where
-	SubstrateFinalityTarget<TargetChain, P>: FinalityTargetClient<P::FinalitySyncPipeline>,
-	P: SubstrateFinalitySyncPipeline,
-	P::FinalitySyncPipeline: FinalitySyncPipeline<Number = SourceChain::BlockNumber>,
+	AccountIdOf<P::TargetChain>:
+		From<<AccountKeyPairOf<P::TransactionSignScheme> as sp_core::Pair>::Public>,
+	P::TransactionSignScheme: TransactionSignScheme<Chain = P::TargetChain>,
 {
-	finality_target.best_finalized_source_block_number().await.map_err(|error| {
-		log::error!(
-			target: "bridge",
-			"Failed to read best finalized source header from target in {} relay: {:?}",
-			relay_task_name,
-			error,
-		);
+	finality_target
+		.best_finalized_source_block_id()
+		.await
+		.map_err(|error| {
+			log::error!(
+				target: "bridge",
+				"Failed to read best finalized source header from target in {} relay: {:?}",
+				relay_task_name,
+				error,
+			);
 
-		error
-	})
+			error
+		})
+		.map(|id| id.0)
 }
 
 /// Read first mandatory header in given inclusive range.
 ///
 /// Returns `Ok(None)` if there were no mandatory headers in the range.
-async fn find_mandatory_header_in_range<SourceChain: Chain, P>(
-	finality_source: &SubstrateFinalitySource<SourceChain, P>,
-	range: (SourceChain::BlockNumber, SourceChain::BlockNumber),
-) -> Result<Option<SourceChain::BlockNumber>, relay_substrate_client::Error>
-where
-	SubstrateFinalitySource<SourceChain, P>: FinalitySourceClient<P>,
-	P: FinalitySyncPipeline<Number = SourceChain::BlockNumber>,
-{
+async fn find_mandatory_header_in_range<P: SubstrateFinalitySyncPipeline>(
+	finality_source: &SubstrateFinalitySource<P>,
+	range: (BlockNumberOf<P::SourceChain>, BlockNumberOf<P::SourceChain>),
+) -> Result<Option<BlockNumberOf<P::SourceChain>>, relay_substrate_client::Error> {
 	let mut current = range.0;
 	while current <= range.1 {
-		let header: SyncHeader<SourceChain::Header> =
+		let header: SyncHeader<HeaderOf<P::SourceChain>> =
 			finality_source.client().header_by_number(current).await?.into();
 		if header.is_mandatory() {
 			return Ok(Some(current))
@@ -445,29 +434,18 @@ mod tests {
 	const AT_TARGET: Option<bp_rococo::BlockNumber> = Some(1);
 
 	#[async_std::test]
-	async fn mandatory_headers_scan_range_selects_range_if_too_many_headers_are_missing() {
+	async fn mandatory_headers_scan_range_selects_range_if_some_headers_are_missing() {
 		assert_eq!(
-			mandatory_headers_scan_range::<TestChain>(
-				AT_SOURCE,
-				AT_TARGET,
-				5,
-				&Arc::new(Mutex::new(0))
-			)
-			.await,
+			mandatory_headers_scan_range::<TestChain>(AT_SOURCE, AT_TARGET, 0,).await,
 			Some((AT_TARGET.unwrap() + 1, AT_SOURCE.unwrap())),
 		);
 	}
 
 	#[async_std::test]
-	async fn mandatory_headers_scan_range_selects_nothing_if_enough_headers_are_relayed() {
+	async fn mandatory_headers_scan_range_selects_nothing_if_already_queued() {
 		assert_eq!(
-			mandatory_headers_scan_range::<TestChain>(
-				AT_SOURCE,
-				AT_TARGET,
-				10,
-				&Arc::new(Mutex::new(0))
-			)
-			.await,
+			mandatory_headers_scan_range::<TestChain>(AT_SOURCE, AT_TARGET, AT_SOURCE.unwrap(),)
+				.await,
 			None,
 		);
 	}
diff --git a/polkadot/bridges/relays/messages/Cargo.toml b/polkadot/bridges/relays/messages/Cargo.toml
index b11f00b957a42b55c35023900181cca8b11a93fb..b3357994b1251f932024835a3b72eeb3b21e5398 100644
--- a/polkadot/bridges/relays/messages/Cargo.toml
+++ b/polkadot/bridges/relays/messages/Cargo.toml
@@ -2,7 +2,7 @@
 name = "messages-relay"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -18,6 +18,7 @@ parking_lot = "0.11.0"
 
 bp-messages = { path = "../../primitives/messages" }
 bp-runtime = { path = "../../primitives/runtime" }
+finality-relay = { path = "../finality" }
 relay-utils = { path = "../utils" }
 
 sp-arithmetic = { git = "https://github.com/paritytech/substrate", branch = "master" }
diff --git a/polkadot/bridges/relays/messages/src/message_lane_loop.rs b/polkadot/bridges/relays/messages/src/message_lane_loop.rs
index 1e7dc6e65fd670d37f73840302afa5d4962d8abd..1f293990e444300d68889cf4ac92cfac83994430 100644
--- a/polkadot/bridges/relays/messages/src/message_lane_loop.rs
+++ b/polkadot/bridges/relays/messages/src/message_lane_loop.rs
@@ -233,6 +233,9 @@ pub struct ClientState<SelfHeaderId, PeerHeaderId> {
 	/// Best finalized header id of the peer chain read at the best block of this chain (at
 	/// `best_finalized_self`).
 	pub best_finalized_peer_at_best_self: PeerHeaderId,
+	/// Header id of the peer chain with the number, matching the
+	/// `best_finalized_peer_at_best_self`.
+	pub actual_best_finalized_peer_at_best_self: PeerHeaderId,
 }
 
 /// State of source client in one-way message lane.
@@ -843,12 +846,14 @@ pub(crate) mod tests {
 					best_self: HeaderId(0, 0),
 					best_finalized_self: HeaderId(0, 0),
 					best_finalized_peer_at_best_self: HeaderId(0, 0),
+					actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
 				},
 				source_latest_generated_nonce: 1,
 				target_state: ClientState {
 					best_self: HeaderId(0, 0),
 					best_finalized_self: HeaderId(0, 0),
 					best_finalized_peer_at_best_self: HeaderId(0, 0),
+					actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
 				},
 				target_latest_received_nonce: 0,
 				..Default::default()
@@ -888,12 +893,14 @@ pub(crate) mod tests {
 					best_self: HeaderId(10, 10),
 					best_finalized_self: HeaderId(10, 10),
 					best_finalized_peer_at_best_self: HeaderId(0, 0),
+					actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
 				},
 				source_latest_generated_nonce: 10,
 				target_state: ClientState {
 					best_self: HeaderId(0, 0),
 					best_finalized_self: HeaderId(0, 0),
 					best_finalized_peer_at_best_self: HeaderId(0, 0),
+					actual_best_finalized_peer_at_best_self: HeaderId(0, 0),
 				},
 				target_latest_received_nonce: 0,
 				..Default::default()
diff --git a/polkadot/bridges/relays/messages/src/metrics.rs b/polkadot/bridges/relays/messages/src/metrics.rs
index eac2f703692a19297cb5fd8d012e8a28fee724ce..4decb7e092e7108a89a0bbf1c1c80fd49435c1d4 100644
--- a/polkadot/bridges/relays/messages/src/metrics.rs
+++ b/polkadot/bridges/relays/messages/src/metrics.rs
@@ -22,6 +22,7 @@ use crate::{
 };
 
 use bp_messages::MessageNonce;
+use finality_relay::SyncLoopMetrics;
 use relay_utils::metrics::{
 	metric_name, register, GaugeVec, Metric, Opts, PrometheusError, Registry, U64,
 };
@@ -31,8 +32,10 @@ use relay_utils::metrics::{
 /// Cloning only clones references.
 #[derive(Clone)]
 pub struct MessageLaneLoopMetrics {
+	/// Best finalized block numbers - "source", "source_at_target", "target_at_source".
+	source_to_target_finality_metrics: SyncLoopMetrics,
 	/// Best finalized block numbers - "source", "target", "source_at_target", "target_at_source".
-	best_block_numbers: GaugeVec<U64>,
+	target_to_source_finality_metrics: SyncLoopMetrics,
 	/// Lane state nonces: "source_latest_generated", "source_latest_confirmed",
 	/// "target_latest_received", "target_latest_confirmed".
 	lane_state_nonces: GaugeVec<U64>,
@@ -42,12 +45,15 @@ impl MessageLaneLoopMetrics {
 	/// Create and register messages loop metrics.
 	pub fn new(prefix: Option<&str>) -> Result<Self, PrometheusError> {
 		Ok(MessageLaneLoopMetrics {
-			best_block_numbers: GaugeVec::new(
-				Opts::new(
-					metric_name(prefix, "best_block_numbers"),
-					"Best finalized block numbers",
-				),
-				&["type"],
+			source_to_target_finality_metrics: SyncLoopMetrics::new(
+				prefix,
+				"source",
+				"source_at_target",
+			)?,
+			target_to_source_finality_metrics: SyncLoopMetrics::new(
+				prefix,
+				"target",
+				"target_at_source",
 			)?,
 			lane_state_nonces: GaugeVec::new(
 				Opts::new(metric_name(prefix, "lane_state_nonces"), "Nonces of the lane state"),
@@ -58,22 +64,28 @@ impl MessageLaneLoopMetrics {
 
 	/// Update source client state metrics.
 	pub fn update_source_state<P: MessageLane>(&self, source_client_state: SourceClientState<P>) {
-		self.best_block_numbers
-			.with_label_values(&["source"])
-			.set(source_client_state.best_self.0.into());
-		self.best_block_numbers
-			.with_label_values(&["target_at_source"])
-			.set(source_client_state.best_finalized_peer_at_best_self.0.into());
+		self.source_to_target_finality_metrics
+			.update_best_block_at_source(source_client_state.best_self.0.into());
+		self.target_to_source_finality_metrics.update_best_block_at_target(
+			source_client_state.best_finalized_peer_at_best_self.0.into(),
+		);
+		self.target_to_source_finality_metrics.update_using_same_fork(
+			source_client_state.best_finalized_peer_at_best_self.1 ==
+				source_client_state.actual_best_finalized_peer_at_best_self.1,
+		);
 	}
 
 	/// Update target client state metrics.
 	pub fn update_target_state<P: MessageLane>(&self, target_client_state: TargetClientState<P>) {
-		self.best_block_numbers
-			.with_label_values(&["target"])
-			.set(target_client_state.best_self.0.into());
-		self.best_block_numbers
-			.with_label_values(&["source_at_target"])
-			.set(target_client_state.best_finalized_peer_at_best_self.0.into());
+		self.target_to_source_finality_metrics
+			.update_best_block_at_source(target_client_state.best_self.0.into());
+		self.source_to_target_finality_metrics.update_best_block_at_target(
+			target_client_state.best_finalized_peer_at_best_self.0.into(),
+		);
+		self.source_to_target_finality_metrics.update_using_same_fork(
+			target_client_state.best_finalized_peer_at_best_self.1 ==
+				target_client_state.actual_best_finalized_peer_at_best_self.1,
+		);
 	}
 
 	/// Update latest generated nonce at source.
@@ -119,7 +131,8 @@ impl MessageLaneLoopMetrics {
 
 impl Metric for MessageLaneLoopMetrics {
 	fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
-		register(self.best_block_numbers.clone(), registry)?;
+		self.source_to_target_finality_metrics.register(registry)?;
+		self.target_to_source_finality_metrics.register(registry)?;
 		register(self.lane_state_nonces.clone(), registry)?;
 		Ok(())
 	}
diff --git a/polkadot/bridges/relays/utils/Cargo.toml b/polkadot/bridges/relays/utils/Cargo.toml
index a08c3b3d688df07361b0c09872a38858de911c70..bb69849da26b0f75e8f6d2624cd71ac84b9637c3 100644
--- a/polkadot/bridges/relays/utils/Cargo.toml
+++ b/polkadot/bridges/relays/utils/Cargo.toml
@@ -2,7 +2,7 @@
 name = "relay-utils"
 version = "0.1.0"
 authors = ["Parity Technologies <admin@parity.io>"]
-edition = "2018"
+edition = "2021"
 license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
 
 [dependencies]
@@ -19,7 +19,8 @@ log = "0.4.11"
 num-traits = "0.2"
 serde_json = "1.0"
 sysinfo = "0.15"
-time = "0.2"
+time = { version = "0.3", features = ["formatting", "local-offset", "std"] }
+tokio = { version = "1.8", features = ["rt"] }
 thiserror = "1.0.26"
 
 # Bridge dependencies
diff --git a/polkadot/bridges/relays/utils/src/initialize.rs b/polkadot/bridges/relays/utils/src/initialize.rs
index 8c13a4d61cb3a5bc4062cf2ed1373bdb580fada1..ad69a766e6236ae81938062b815fe9f2f11132c5 100644
--- a/polkadot/bridges/relays/utils/src/initialize.rs
+++ b/polkadot/bridges/relays/utils/src/initialize.rs
@@ -29,15 +29,21 @@ pub fn initialize_relay() {
 
 /// Initialize Relay logger instance.
 pub fn initialize_logger(with_timestamp: bool) {
+	let format = time::format_description::parse(
+		"[year]-[month]-[day] \
+		[hour repr:24]:[minute]:[second] [offset_hour sign:mandatory]",
+	)
+	.expect("static format string is valid");
+
 	let mut builder = env_logger::Builder::new();
 	builder.filter_level(log::LevelFilter::Warn);
 	builder.filter_module("bridge", log::LevelFilter::Info);
 	builder.parse_default_env();
 	if with_timestamp {
 		builder.format(move |buf, record| {
-			let timestamp = time::OffsetDateTime::try_now_local()
-				.unwrap_or_else(|_| time::OffsetDateTime::now_utc())
-				.format("%Y-%m-%d %H:%M:%S %z");
+			let timestamp = time::OffsetDateTime::now_local()
+				.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
+			let timestamp = timestamp.format(&format).unwrap_or_else(|_| timestamp.to_string());
 
 			let log_level = color_level(record.level());
 			let log_target = color_target(record.target());
diff --git a/polkadot/bridges/relays/utils/src/metrics.rs b/polkadot/bridges/relays/utils/src/metrics.rs
index 805fe70bfe8586d8052c446c5f143f0f885c748f..084f72e7950c527a927a3c764d121a3f5c9d1da7 100644
--- a/polkadot/bridges/relays/utils/src/metrics.rs
+++ b/polkadot/bridges/relays/utils/src/metrics.rs
@@ -18,7 +18,7 @@ pub use float_json_value::FloatJsonValueMetric;
 pub use global::GlobalMetrics;
 pub use substrate_prometheus_endpoint::{
 	prometheus::core::{Atomic, Collector},
-	register, Counter, CounterVec, Gauge, GaugeVec, Opts, PrometheusError, Registry, F64, U64,
+	register, Counter, CounterVec, Gauge, GaugeVec, Opts, PrometheusError, Registry, F64, I64, U64,
 };
 
 use async_std::sync::{Arc, RwLock};
@@ -30,6 +30,8 @@ mod global;
 
 /// Shared reference to `f64` value that is updated by the metric.
 pub type F64SharedRef = Arc<RwLock<Option<f64>>>;
+/// Int gauge metric type.
+pub type IntGauge = Gauge<U64>;
 
 /// Unparsed address that needs to be used to expose Prometheus metrics.
 #[derive(Debug, Clone)]
diff --git a/polkadot/bridges/relays/utils/src/relay_loop.rs b/polkadot/bridges/relays/utils/src/relay_loop.rs
index a992aaaf57ee505f970e86d6b571115702b43621..521a6345d3e39c988657266b1a68f300c469460a 100644
--- a/polkadot/bridges/relays/utils/src/relay_loop.rs
+++ b/polkadot/bridges/relays/utils/src/relay_loop.rs
@@ -187,12 +187,32 @@ impl<SC, TC, LM> LoopMetrics<SC, TC, LM> {
 
 			let registry = self.registry;
 			async_std::task::spawn(async move {
-				let result = init_prometheus(socket_addr, registry).await;
-				log::trace!(
-					target: "bridge-metrics",
-					"Prometheus endpoint has exited with result: {:?}",
-					result,
-				);
+				let runtime =
+					match tokio::runtime::Builder::new_current_thread().enable_all().build() {
+						Ok(runtime) => runtime,
+						Err(err) => {
+							log::trace!(
+								target: "bridge-metrics",
+								"Failed to create tokio runtime. Prometheus meterics are not available: {:?}",
+								err,
+							);
+							return
+						},
+					};
+
+				let _ = runtime.block_on(async move {
+					log::trace!(
+						target: "bridge-metrics",
+						"Starting prometheus endpoint at: {:?}",
+						socket_addr,
+					);
+					let result = init_prometheus(socket_addr, registry).await;
+					log::trace!(
+						target: "bridge-metrics",
+						"Prometheus endpoint has exited with result: {:?}",
+						result,
+					);
+				});
 			});
 		}
 
diff --git a/polkadot/bridges/scripts/dump-logs.sh b/polkadot/bridges/scripts/dump-logs.sh
old mode 100644
new mode 100755
index 02aa4af2f7564b14d82f1c010adb921bc04fc15d..e5a3a403adac11362adfa46aaed33746a3cac737
--- a/polkadot/bridges/scripts/dump-logs.sh
+++ b/polkadot/bridges/scripts/dump-logs.sh
@@ -28,7 +28,7 @@ SERVICES=(\
 for SVC in ${SERVICES[*]}
 do
 	SHORT_NAME="${SVC//deployments_/}"
-	docker logs $SVC &> $SHORT_NAME.log
+	docker logs $SVC &> $SHORT_NAME.log | true
 done
 
 cd -
diff --git a/polkadot/bridges/scripts/update-weights.sh b/polkadot/bridges/scripts/update-weights.sh
index 5ee7bb9e8d8e1c42a24ffdeead289441569c8315..b772386e75930e6a11da6738f1c64f1610e36d41 100755
--- a/polkadot/bridges/scripts/update-weights.sh
+++ b/polkadot/bridges/scripts/update-weights.sh
@@ -6,7 +6,7 @@
 
 set -eux
 
-time cargo run --release -p rialto-bridge-node --features=runtime-benchmarks -- benchmark \
+time cargo run --release -p millau-bridge-node --features=runtime-benchmarks -- benchmark \
 	--chain=dev \
 	--steps=50 \
 	--repeat=20 \
@@ -16,9 +16,9 @@ time cargo run --release -p rialto-bridge-node --features=runtime-benchmarks --
 	--wasm-execution=Compiled \
 	--heap-pages=4096 \
 	--output=./modules/messages/src/weights.rs \
-	--template=./.maintain/rialto-weight-template.hbs
+	--template=./.maintain/millau-weight-template.hbs
 
-time cargo run --release -p rialto-bridge-node --features=runtime-benchmarks -- benchmark \
+time cargo run --release -p millau-bridge-node --features=runtime-benchmarks -- benchmark \
 	--chain=dev \
 	--steps=50 \
 	--repeat=20 \
@@ -28,7 +28,7 @@ time cargo run --release -p rialto-bridge-node --features=runtime-benchmarks --
 	--wasm-execution=Compiled \
 	--heap-pages=4096 \
 	--output=./modules/grandpa/src/weights.rs \
-	--template=./.maintain/rialto-weight-template.hbs
+	--template=./.maintain/millau-weight-template.hbs
 
 time cargo run --release -p millau-bridge-node --features=runtime-benchmarks -- benchmark \
 	--chain=dev \
diff --git a/polkadot/runtime/rococo/src/bridge_messages.rs b/polkadot/runtime/rococo/src/bridge_messages.rs
index 17a15836db46e7a3fd0b9c6f73d0594c65d6077a..ca18aef8148fbf064584257d618445ce9f4f91af 100644
--- a/polkadot/runtime/rococo/src/bridge_messages.rs
+++ b/polkadot/runtime/rococo/src/bridge_messages.rs
@@ -18,32 +18,32 @@
 
 pub use self::{at_rococo::*, at_wococo::*};
 
+use crate::{Balances, Runtime};
+
 use bp_messages::{
-	source_chain::TargetHeaderChain,
+	source_chain::{SenderOrigin, TargetHeaderChain},
 	target_chain::{ProvedMessages, SourceHeaderChain},
 	InboundLaneData, LaneId, Message, MessageNonce,
 };
-use bp_rococo::{
-	max_extrinsic_size, max_extrinsic_weight, EXTRA_STORAGE_PROOF_SIZE,
-	MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-};
-use bp_runtime::{ChainId, ROCOCO_CHAIN_ID, WOCOCO_CHAIN_ID};
+use bp_rococo::{Balance, Rococo, EXTRA_STORAGE_PROOF_SIZE, MAXIMAL_ENCODED_ACCOUNT_ID_SIZE};
+use bp_runtime::{Chain, ChainId, ROCOCO_CHAIN_ID, WOCOCO_CHAIN_ID};
 use bridge_runtime_common::messages::{
-	source as messages_source, target as messages_target, BridgedChainWithMessages,
-	ChainWithMessages, MessageBridge, MessageTransaction, ThisChainWithMessages,
+	source as messages_source, target as messages_target, transaction_payment,
+	BridgedChainWithMessages, ChainWithMessages, MessageBridge, MessageTransaction,
+	ThisChainWithMessages,
 };
 use frame_support::{
 	traits::Get,
 	weights::{Weight, WeightToFeePolynomial},
 	RuntimeDebug,
 };
-use sp_std::{convert::TryFrom, marker::PhantomData, ops::RangeInclusive};
-
 use rococo_runtime_constants::fee::WeightToFee;
+use sp_runtime::FixedU128;
+use sp_std::{convert::TryFrom, marker::PhantomData, ops::RangeInclusive};
 
 /// Maximal number of pending outbound messages.
 const MAXIMAL_PENDING_MESSAGES_AT_OUTBOUND_LANE: MessageNonce =
-	bp_rococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE;
+	bp_rococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
 /// Maximal weight of single message delivery confirmation transaction on Rococo/Wococo chain.
 ///
 /// This value is a result of `pallet_bridge_messages::Pallet::receive_messages_delivery_proof` weight formula
@@ -102,13 +102,14 @@ impl<B, GI> ChainWithMessages for RococoLikeChain<B, GI> {
 	type Signer = primitives::v2::AccountPublic;
 	type Signature = crate::Signature;
 	type Weight = Weight;
-	type Balance = crate::Balance;
+	type Balance = Balance;
 }
 
 impl<B, GI> ThisChainWithMessages for RococoLikeChain<B, GI> {
+	type Origin = crate::Origin;
 	type Call = crate::Call;
 
-	fn is_outbound_lane_enabled(lane: &LaneId) -> bool {
+	fn is_message_accepted(_submitter: &crate::Origin, lane: &LaneId) -> bool {
 		*lane == [0, 0, 0, 0]
 	}
 
@@ -132,14 +133,14 @@ impl<B, GI> ThisChainWithMessages for RococoLikeChain<B, GI> {
 		}
 	}
 
-	fn transaction_payment(transaction: MessageTransaction<Weight>) -> crate::Balance {
+	fn transaction_payment(transaction: MessageTransaction<Weight>) -> Balance {
 		// current fee multiplier is used here
-		bridge_runtime_common::messages::transaction_payment(
+		transaction_payment(
 			crate::BlockWeights::get()
 				.get(frame_support::weights::DispatchClass::Normal)
 				.base_extrinsic,
 			crate::TransactionByteFee::get(),
-			pallet_transaction_payment::Pallet::<crate::Runtime>::next_fee_multiplier(),
+			pallet_transaction_payment::Pallet::<Runtime>::next_fee_multiplier(),
 			|weight| WeightToFee::calc(&weight),
 			transaction,
 		)
@@ -148,13 +149,14 @@ impl<B, GI> ThisChainWithMessages for RococoLikeChain<B, GI> {
 
 impl<B, GI> BridgedChainWithMessages for RococoLikeChain<B, GI> {
 	fn maximal_extrinsic_size() -> u32 {
-		max_extrinsic_size()
+		Rococo::max_extrinsic_size()
 	}
 
 	fn message_weight_limits(_message_payload: &[u8]) -> RangeInclusive<Weight> {
 		// we don't want to relay too large messages + keep reserve for future upgrades
-		let upper_limit =
-			messages_target::maximal_incoming_message_dispatch_weight(max_extrinsic_weight());
+		let upper_limit = messages_target::maximal_incoming_message_dispatch_weight(
+			Rococo::max_extrinsic_weight(),
+		);
 
 		// we're charging for payload bytes in `With(Wococo | Rococo)MessageBridge::transaction_payment` function
 		//
@@ -189,14 +191,14 @@ impl<B, GI> BridgedChainWithMessages for RococoLikeChain<B, GI> {
 		}
 	}
 
-	fn transaction_payment(transaction: MessageTransaction<Weight>) -> crate::Balance {
+	fn transaction_payment(transaction: MessageTransaction<Weight>) -> Balance {
 		// current fee multiplier is used here
 		bridge_runtime_common::messages::transaction_payment(
 			crate::BlockWeights::get()
 				.get(frame_support::weights::DispatchClass::Normal)
 				.base_extrinsic,
 			crate::TransactionByteFee::get(),
-			pallet_transaction_payment::Pallet::<crate::Runtime>::next_fee_multiplier(),
+			pallet_transaction_payment::Pallet::<Runtime>::next_fee_multiplier(),
 			|weight| WeightToFee::calc(&weight),
 			transaction,
 		)
@@ -210,30 +212,35 @@ where
 	B::ThisChain: ChainWithMessages<AccountId = crate::AccountId>,
 	B::BridgedChain: ChainWithMessages<Hash = crate::Hash>,
 	GI: 'static,
-	crate::Runtime: pallet_bridge_grandpa::Config<GI>,
-	<<crate::Runtime as pallet_bridge_grandpa::Config<GI>>::BridgedChain as bp_runtime::Chain>::Hash: From<crate::Hash>,
+	Runtime: pallet_bridge_grandpa::Config<GI>,
+	<<Runtime as pallet_bridge_grandpa::Config<GI>>::BridgedChain as bp_runtime::Chain>::Hash:
+		From<crate::Hash>,
 {
 	type Error = &'static str;
-	type MessagesDeliveryProof = messages_source::FromBridgedChainMessagesDeliveryProof<crate::Hash>;
+	type MessagesDeliveryProof =
+		messages_source::FromBridgedChainMessagesDeliveryProof<crate::Hash>;
 
-	fn verify_message(payload: &messages_source::FromThisChainMessagePayload<B>) -> Result<(), Self::Error> {
+	fn verify_message(
+		payload: &messages_source::FromThisChainMessagePayload<B>,
+	) -> Result<(), Self::Error> {
 		messages_source::verify_chain_message::<B>(payload)
 	}
 
 	fn verify_messages_delivery_proof(
 		proof: Self::MessagesDeliveryProof,
 	) -> Result<(LaneId, InboundLaneData<crate::AccountId>), Self::Error> {
-		messages_source::verify_messages_delivery_proof::<B, crate::Runtime, GI>(proof)
+		messages_source::verify_messages_delivery_proof::<B, Runtime, GI>(proof)
 	}
 }
 
-impl<B, GI> SourceHeaderChain<crate::Balance> for RococoLikeChain<B, GI>
+impl<B, GI> SourceHeaderChain<Balance> for RococoLikeChain<B, GI>
 where
 	B: MessageBridge,
-	B::BridgedChain: ChainWithMessages<Balance = crate::Balance, Hash = crate::Hash>,
+	B::BridgedChain: ChainWithMessages<Balance = Balance, Hash = crate::Hash>,
 	GI: 'static,
-	crate::Runtime: pallet_bridge_grandpa::Config<GI>,
-	<<crate::Runtime as pallet_bridge_grandpa::Config<GI>>::BridgedChain as bp_runtime::Chain>::Hash: From<crate::Hash>,
+	Runtime: pallet_bridge_grandpa::Config<GI>,
+	<<Runtime as pallet_bridge_grandpa::Config<GI>>::BridgedChain as bp_runtime::Chain>::Hash:
+		From<crate::Hash>,
 {
 	type Error = &'static str;
 	type MessagesProof = messages_target::FromBridgedChainMessagesProof<crate::Hash>;
@@ -241,8 +248,9 @@ where
 	fn verify_messages_proof(
 		proof: Self::MessagesProof,
 		messages_count: u32,
-	) -> Result<ProvedMessages<Message<crate::Balance>>, Self::Error> {
-		messages_target::verify_messages_proof::<B, crate::Runtime, GI>(proof, messages_count).and_then(verify_inbound_messages_lane)
+	) -> Result<ProvedMessages<Message<Balance>>, Self::Error> {
+		messages_target::verify_messages_proof::<B, Runtime, GI>(proof, messages_count)
+			.and_then(verify_inbound_messages_lane)
 	}
 }
 
@@ -251,8 +259,8 @@ const INBOUND_LANE_DISABLED: &str = "The inbound message lane is disabled.";
 
 /// Verify that lanes of inbound messages are enabled.
 fn verify_inbound_messages_lane(
-	messages: ProvedMessages<Message<crate::Balance>>,
-) -> Result<ProvedMessages<Message<crate::Balance>>, &'static str> {
+	messages: ProvedMessages<Message<Balance>>,
+) -> Result<ProvedMessages<Message<Balance>>, &'static str> {
 	let allowed_incoming_lanes = [[0, 0, 0, 0]];
 	if messages.keys().any(|lane_id| !allowed_incoming_lanes.contains(lane_id)) {
 		return Err(INBOUND_LANE_DISABLED)
@@ -263,14 +271,27 @@ fn verify_inbound_messages_lane(
 /// The cost of delivery confirmation transaction.
 pub struct GetDeliveryConfirmationTransactionFee;
 
-impl Get<crate::Balance> for GetDeliveryConfirmationTransactionFee {
-	fn get() -> crate::Balance {
+impl Get<Balance> for GetDeliveryConfirmationTransactionFee {
+	fn get() -> Balance {
 		<RococoAtRococo as ThisChainWithMessages>::transaction_payment(
 			RococoAtRococo::estimate_delivery_confirmation_transaction(),
 		)
 	}
 }
 
+impl SenderOrigin<crate::AccountId> for crate::Origin {
+	fn linked_account(&self) -> Option<crate::AccountId> {
+		match self.caller {
+			crate::OriginCaller::system(frame_system::RawOrigin::Signed(ref submitter)) =>
+				Some(submitter.clone()),
+			crate::OriginCaller::system(frame_system::RawOrigin::Root) |
+			crate::OriginCaller::system(frame_system::RawOrigin::None) =>
+				crate::RootAccountForPayments::get(),
+			_ => None,
+		}
+	}
+}
+
 /// This module contains definitions that are used by the messages pallet instance, "deployed" at Rococo.
 mod at_rococo {
 	use super::*;
@@ -284,13 +305,14 @@ mod at_rococo {
 		const BRIDGED_CHAIN_ID: ChainId = WOCOCO_CHAIN_ID;
 		const RELAYER_FEE_PERCENT: u32 = 10;
 		const BRIDGED_MESSAGES_PALLET_NAME: &'static str =
-			bp_wococo::WITH_ROCOCO_MESSAGES_PALLET_NAME;
+			bp_rococo::WITH_ROCOCO_MESSAGES_PALLET_NAME;
 
 		type ThisChain = RococoAtRococo;
 		type BridgedChain = WococoAtRococo;
 
 		fn bridged_balance_to_this_balance(
 			bridged_balance: bp_wococo::Balance,
+			_bridged_to_this_conversion_rate_override: Option<FixedU128>,
 		) -> bp_rococo::Balance {
 			bridged_balance
 		}
@@ -315,8 +337,8 @@ mod at_rococo {
 	/// Call-dispatch based message dispatch for Wococo -> Rococo messages.
 	pub type FromWococoMessageDispatch = messages_target::FromBridgedChainMessageDispatch<
 		AtRococoWithWococoMessageBridge,
-		crate::Runtime,
-		pallet_balances::Pallet<crate::Runtime>,
+		Runtime,
+		Balances,
 		crate::AtRococoFromWococoMessagesDispatch,
 	>;
 }
@@ -334,13 +356,14 @@ mod at_wococo {
 		const BRIDGED_CHAIN_ID: ChainId = ROCOCO_CHAIN_ID;
 		const RELAYER_FEE_PERCENT: u32 = 10;
 		const BRIDGED_MESSAGES_PALLET_NAME: &'static str =
-			bp_rococo::WITH_WOCOCO_MESSAGES_PALLET_NAME;
+			bp_wococo::WITH_WOCOCO_MESSAGES_PALLET_NAME;
 
 		type ThisChain = WococoAtWococo;
 		type BridgedChain = RococoAtWococo;
 
 		fn bridged_balance_to_this_balance(
 			bridged_balance: bp_rococo::Balance,
+			_bridged_to_this_conversion_rate_override: Option<FixedU128>,
 		) -> bp_wococo::Balance {
 			bridged_balance
 		}
@@ -365,8 +388,8 @@ mod at_wococo {
 	/// Call-dispatch based message dispatch for Rococo -> Wococo messages.
 	pub type FromRococoMessageDispatch = messages_target::FromBridgedChainMessageDispatch<
 		AtWococoWithRococoMessageBridge,
-		crate::Runtime,
-		pallet_balances::Pallet<crate::Runtime>,
+		Runtime,
+		Balances,
 		crate::AtWococoFromRococoMessagesDispatch,
 	>;
 }
@@ -390,7 +413,7 @@ mod tests {
 
 		// we don't have any knowledge of messages-at-Rococo weights, so we'll be using
 		// weights of one of our testnets, which should be accurate enough
-		type Weights = pallet_bridge_messages::weights::RialtoWeight<crate::Runtime>;
+		type Weights = pallet_bridge_messages::weights::MillauWeight<Runtime>;
 
 		pallet_bridge_messages::ensure_weights_are_correct::<Weights>(
 			DEFAULT_MESSAGE_DELIVERY_TX_WEIGHT,
@@ -401,30 +424,30 @@ mod tests {
 		);
 
 		let max_incoming_message_proof_size = bp_rococo::EXTRA_STORAGE_PROOF_SIZE.saturating_add(
-			messages::target::maximal_incoming_message_size(bp_rococo::max_extrinsic_size()),
+			messages::target::maximal_incoming_message_size(Rococo::max_extrinsic_size()),
 		);
 		pallet_bridge_messages::ensure_able_to_receive_message::<Weights>(
-			bp_rococo::max_extrinsic_size(),
-			bp_rococo::max_extrinsic_weight(),
+			Rococo::max_extrinsic_size(),
+			Rococo::max_extrinsic_weight(),
 			max_incoming_message_proof_size,
 			messages::target::maximal_incoming_message_dispatch_weight(
-				bp_rococo::max_extrinsic_weight(),
+				Rococo::max_extrinsic_weight(),
 			),
 		);
 
 		let max_incoming_inbound_lane_data_proof_size =
 			bp_messages::InboundLaneData::<()>::encoded_size_hint(
 				bp_rococo::MAXIMAL_ENCODED_ACCOUNT_ID_SIZE,
-				bp_rococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE as _,
-				bp_rococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE as _,
+				bp_rococo::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX as _,
+				bp_rococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX as _,
 			)
 			.unwrap_or(u32::MAX);
 		pallet_bridge_messages::ensure_able_to_receive_confirmation::<Weights>(
-			bp_rococo::max_extrinsic_size(),
-			bp_rococo::max_extrinsic_weight(),
+			Rococo::max_extrinsic_size(),
+			Rococo::max_extrinsic_weight(),
 			max_incoming_inbound_lane_data_proof_size,
-			bp_rococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE,
-			bp_rococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE,
+			bp_rococo::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX,
+			bp_rococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX,
 			crate::RocksDbWeight::get(),
 		);
 	}
@@ -465,7 +488,7 @@ mod tests {
 		);
 	}
 
-	fn proved_messages(lane_id: LaneId) -> ProvedMessages<Message<crate::Balance>> {
+	fn proved_messages(lane_id: LaneId) -> ProvedMessages<Message<Balance>> {
 		vec![(
 			lane_id,
 			ProvedLaneMessages {
diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs
index f459ca597536801470dc50c6528622cefd02e9fe..b534311bca840043a2b047275f40b8c8adab0581 100644
--- a/polkadot/runtime/rococo/src/lib.rs
+++ b/polkadot/runtime/rococo/src/lib.rs
@@ -55,7 +55,7 @@ use sp_runtime::{
 		OpaqueKeys, SaturatedConversion, Verify,
 	},
 	transaction_validity::{TransactionPriority, TransactionSource, TransactionValidity},
-	ApplyExtrinsicResult, KeyTypeId,
+	ApplyExtrinsicResult, FixedU128, KeyTypeId,
 };
 use sp_staking::SessionIndex;
 use sp_std::{collections::btree_map::BTreeMap, prelude::*};
@@ -735,7 +735,7 @@ impl pallet_bridge_grandpa::Config for Runtime {
 	type MaxRequests = MaxRequests;
 	type HeadersToKeep = HeadersToKeep;
 
-	type WeightInfo = pallet_bridge_grandpa::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_grandpa::weights::MillauWeight<Runtime>;
 }
 
 pub type WococoGrandpaInstance = pallet_bridge_grandpa::Instance1;
@@ -744,7 +744,7 @@ impl pallet_bridge_grandpa::Config<WococoGrandpaInstance> for Runtime {
 	type MaxRequests = MaxRequests;
 	type HeadersToKeep = HeadersToKeep;
 
-	type WeightInfo = pallet_bridge_grandpa::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_grandpa::weights::MillauWeight<Runtime>;
 }
 
 // Instance that is "deployed" at Wococo chain. Responsible for dispatching Rococo -> Wococo messages.
@@ -778,9 +778,9 @@ impl pallet_bridge_dispatch::Config<AtRococoFromWococoMessagesDispatch> for Runt
 parameter_types! {
 	pub const MaxMessagesToPruneAtOnce: bp_messages::MessageNonce = 8;
 	pub const MaxUnrewardedRelayerEntriesAtInboundLane: bp_messages::MessageNonce =
-		bp_rococo::MAX_UNREWARDED_RELAYER_ENTRIES_AT_INBOUND_LANE;
+		bp_rococo::MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX;
 	pub const MaxUnconfirmedMessagesAtInboundLane: bp_messages::MessageNonce =
-		bp_rococo::MAX_UNCONFIRMED_MESSAGES_AT_INBOUND_LANE;
+		bp_rococo::MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX;
 	pub const RootAccountForPayments: Option<AccountId> = None;
 	pub const RococoChainId: bp_runtime::ChainId = bp_runtime::ROCOCO_CHAIN_ID;
 	pub const WococoChainId: bp_runtime::ChainId = bp_runtime::WOCOCO_CHAIN_ID;
@@ -792,7 +792,7 @@ pub type AtWococoWithRococoMessagesInstance = ();
 impl pallet_bridge_messages::Config<AtWococoWithRococoMessagesInstance> for Runtime {
 	type Event = Event;
 	type BridgedChainId = RococoChainId;
-	type WeightInfo = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_messages::weights::MillauWeight<Runtime>;
 	type Parameter = ();
 	type MaxMessagesToPruneAtOnce = MaxMessagesToPruneAtOnce;
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
@@ -815,7 +815,6 @@ impl pallet_bridge_messages::Config<AtWococoWithRococoMessagesInstance> for Runt
 			AtWococoWithRococoMessagesInstance,
 			pallet_balances::Pallet<Runtime>,
 			crate::bridge_messages::GetDeliveryConfirmationTransactionFee,
-			RootAccountForPayments,
 		>;
 	type OnDeliveryConfirmed = ();
 	type OnMessageAccepted = ();
@@ -830,7 +829,7 @@ pub type AtRococoWithWococoMessagesInstance = pallet_bridge_messages::Instance1;
 impl pallet_bridge_messages::Config<AtRococoWithWococoMessagesInstance> for Runtime {
 	type Event = Event;
 	type BridgedChainId = WococoChainId;
-	type WeightInfo = pallet_bridge_messages::weights::RialtoWeight<Runtime>;
+	type WeightInfo = pallet_bridge_messages::weights::MillauWeight<Runtime>;
 	type Parameter = ();
 	type MaxMessagesToPruneAtOnce = MaxMessagesToPruneAtOnce;
 	type MaxUnrewardedRelayerEntriesAtInboundLane = MaxUnrewardedRelayerEntriesAtInboundLane;
@@ -853,7 +852,6 @@ impl pallet_bridge_messages::Config<AtRococoWithWococoMessagesInstance> for Runt
 			AtRococoWithWococoMessagesInstance,
 			pallet_balances::Pallet<Runtime>,
 			crate::bridge_messages::GetDeliveryConfirmationTransactionFee,
-			RootAccountForPayments,
 		>;
 	type OnDeliveryConfirmed = ();
 	type OnMessageAccepted = ();
@@ -1373,10 +1371,6 @@ sp_api::impl_runtime_apis! {
 			let header = BridgeRococoGrandpa::best_finalized();
 			(header.number, header.hash())
 		}
-
-		fn is_known_header(hash: bp_rococo::Hash) -> bool {
-			BridgeRococoGrandpa::is_known_header(hash)
-		}
 	}
 
 	impl bp_wococo::WococoFinalityApi<Block> for Runtime {
@@ -1384,20 +1378,18 @@ sp_api::impl_runtime_apis! {
 			let header = BridgeWococoGrandpa::best_finalized();
 			(header.number, header.hash())
 		}
-
-		fn is_known_header(hash: bp_wococo::Hash) -> bool {
-			BridgeWococoGrandpa::is_known_header(hash)
-		}
 	}
 
 	impl bp_rococo::ToRococoOutboundLaneApi<Block, Balance, bridge_messages::ToRococoMessagePayload> for Runtime {
 		fn estimate_message_delivery_and_dispatch_fee(
 			_lane_id: bp_messages::LaneId,
 			payload: bridge_messages::ToWococoMessagePayload,
+			rococo_to_wococo_conversion_rate: Option<FixedU128>,
 		) -> Option<Balance> {
 			estimate_message_dispatch_and_delivery_fee::<bridge_messages::AtWococoWithRococoMessageBridge>(
 				&payload,
 				bridge_messages::AtWococoWithRococoMessageBridge::RELAYER_FEE_PERCENT,
+				rococo_to_wococo_conversion_rate,
 			).ok()
 		}
 
@@ -1421,38 +1413,18 @@ sp_api::impl_runtime_apis! {
 			})
 			.collect()
 		}
-
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRococoMessages::outbound_latest_received_nonce(lane)
-		}
-
-		fn latest_generated_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRococoMessages::outbound_latest_generated_nonce(lane)
-		}
-	}
-
-	impl bp_rococo::FromRococoInboundLaneApi<Block> for Runtime {
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRococoMessages::inbound_latest_received_nonce(lane)
-		}
-
-		fn latest_confirmed_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeRococoMessages::inbound_latest_confirmed_nonce(lane)
-		}
-
-		fn unrewarded_relayers_state(lane: bp_messages::LaneId) -> bp_messages::UnrewardedRelayersState {
-			BridgeRococoMessages::inbound_unrewarded_relayers_state(lane)
-		}
 	}
 
 	impl bp_wococo::ToWococoOutboundLaneApi<Block, Balance, bridge_messages::ToWococoMessagePayload> for Runtime {
 		fn estimate_message_delivery_and_dispatch_fee(
 			_lane_id: bp_messages::LaneId,
 			payload: bridge_messages::ToWococoMessagePayload,
+			wococo_to_rococo_conversion_rate: Option<FixedU128>,
 		) -> Option<Balance> {
 			estimate_message_dispatch_and_delivery_fee::<bridge_messages::AtRococoWithWococoMessageBridge>(
 				&payload,
 				bridge_messages::AtRococoWithWococoMessageBridge::RELAYER_FEE_PERCENT,
+				wococo_to_rococo_conversion_rate,
 			).ok()
 		}
 
@@ -1476,28 +1448,6 @@ sp_api::impl_runtime_apis! {
 			})
 			.collect()
 		}
-
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeWococoMessages::outbound_latest_received_nonce(lane)
-		}
-
-		fn latest_generated_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeWococoMessages::outbound_latest_generated_nonce(lane)
-		}
-	}
-
-	impl bp_wococo::FromWococoInboundLaneApi<Block> for Runtime {
-		fn latest_received_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeWococoMessages::inbound_latest_received_nonce(lane)
-		}
-
-		fn latest_confirmed_nonce(lane: bp_messages::LaneId) -> bp_messages::MessageNonce {
-			BridgeWococoMessages::inbound_latest_confirmed_nonce(lane)
-		}
-
-		fn unrewarded_relayers_state(lane: bp_messages::LaneId) -> bp_messages::UnrewardedRelayersState {
-			BridgeWococoMessages::inbound_unrewarded_relayers_state(lane)
-		}
 	}
 
 	impl frame_system_rpc_runtime_api::AccountNonceApi<Block, AccountId, Nonce> for Runtime {