diff --git a/substrate/client/network/src/protocol/notifications/tests/conformance.rs b/substrate/client/network/src/protocol/notifications/tests/conformance.rs
new file mode 100644
index 0000000000000000000000000000000000000000..421177997f99840a1cbf6aa753b42fcd5f2278c7
--- /dev/null
+++ b/substrate/client/network/src/protocol/notifications/tests/conformance.rs
@@ -0,0 +1,1070 @@
+// This file is part of Substrate.
+
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program 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.
+
+// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::{
+	peer_store::{PeerStore, PeerStoreHandle, PeerStoreProvider},
+	protocol::notifications::{
+		service::notification_service, Notifications, NotificationsOut, ProtocolConfig,
+	},
+	protocol_controller::{ProtoSetConfig, ProtocolController, SetId},
+	service::metrics::NotificationMetrics,
+	NotificationService,
+};
+
+use futures::prelude::*;
+use libp2p::{
+	core::upgrade,
+	identity,
+	swarm::{Swarm, SwarmEvent},
+	PeerId,
+};
+use sc_utils::mpsc::tracing_unbounded;
+use std::{collections::HashSet, iter, num::NonZeroUsize, pin::Pin, sync::Arc, time::Duration};
+
+use litep2p::{
+	config::ConfigBuilder as Litep2pConfigBuilder,
+	protocol::notification::{
+		Config as NotificationConfig, NotificationEvent as Litep2pNotificationEvent,
+		NotificationHandle, ValidationResult as Litep2pValidationResult,
+	},
+	transport::tcp::config::Config as TcpConfig,
+	types::protocol::ProtocolName as Litep2pProtocolName,
+	Litep2p, Litep2pEvent,
+};
+
+fn setup_libp2p(
+	in_peers: u32,
+	out_peers: u32,
+) -> (Swarm<Notifications>, PeerStoreHandle, Box<dyn NotificationService>) {
+	let local_key = identity::Keypair::generate_ed25519();
+	let local_peer_id = PeerId::from(local_key.public());
+	let peer_store = PeerStore::new(vec![], None);
+
+	let (to_notifications, from_controller) =
+		tracing_unbounded("test_protocol_controller_to_notifications", 10_000);
+
+	let (protocol_handle_pair, notif_service) = notification_service("/foo".into());
+
+	let (controller_handle, controller) = ProtocolController::new(
+		SetId::from(0usize),
+		ProtoSetConfig {
+			in_peers,
+			out_peers,
+			reserved_nodes: HashSet::new(),
+			reserved_only: false,
+		},
+		to_notifications.clone(),
+		Arc::new(peer_store.handle()),
+	);
+	let peer_store_handle = peer_store.handle();
+	tokio::spawn(controller.run());
+	tokio::spawn(peer_store.run());
+
+	let (notif_handle, command_stream) = protocol_handle_pair.split();
+
+	let behaviour = Notifications::new(
+		vec![controller_handle],
+		from_controller,
+		NotificationMetrics::new(None),
+		iter::once((
+			ProtocolConfig {
+				name: "/foo".into(),
+				fallback_names: Vec::new(),
+				handshake: vec![1, 2, 3, 4],
+				max_notification_size: 1024 * 1024,
+			},
+			notif_handle,
+			command_stream,
+		)),
+	);
+	let transport = crate::transport::build_transport(local_key.clone().into(), false);
+
+	let mut swarm = {
+		struct SpawnImpl {}
+		impl libp2p::swarm::Executor for SpawnImpl {
+			fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) {
+				tokio::spawn(f);
+			}
+		}
+
+		let config = libp2p::swarm::Config::with_executor(SpawnImpl {})
+			.with_substream_upgrade_protocol_override(upgrade::Version::V1)
+			.with_notify_handler_buffer_size(NonZeroUsize::new(32).expect("32 != 0; qed"))
+			// NOTE: 24 is somewhat arbitrary and should be tuned in the future if
+			// necessary. See <https://github.com/paritytech/substrate/pull/6080>
+			.with_per_connection_event_buffer_size(24)
+			.with_max_negotiating_inbound_streams(2048)
+			.with_idle_connection_timeout(Duration::from_secs(5));
+
+		Swarm::new(transport.0, behaviour, local_peer_id, config)
+	};
+
+	swarm.listen_on("/ip6/::1/tcp/0".parse().unwrap()).unwrap();
+
+	(swarm, peer_store_handle, notif_service)
+}
+
+async fn setup_litep2p() -> (Litep2p, NotificationHandle) {
+	let (notif_config, handle) = NotificationConfig::new(
+		Litep2pProtocolName::from("/foo"),
+		1024 * 1024,
+		vec![1, 2, 3, 4],
+		Vec::new(),
+		false,
+		64,
+		64,
+		true,
+	);
+
+	let config1 = Litep2pConfigBuilder::new()
+		.with_tcp(TcpConfig {
+			listen_addresses: vec!["/ip6/::1/tcp/0".parse().unwrap()],
+			..Default::default()
+		})
+		.with_notification_protocol(notif_config)
+		.build();
+	let litep2p = Litep2p::new(config1).unwrap();
+
+	(litep2p, handle)
+}
+
+/// Test ensures litep2p can dial and connect to libp2p.
+#[tokio::test]
+async fn test_libp2p_litep2p_connectivity() {
+	let (mut litep2p, _notif_handle) = setup_litep2p().await;
+	let (mut libp2p, _peerstore, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_address = loop {
+		let event = libp2p.select_next_some().await;
+		match event {
+			SwarmEvent::NewListenAddr { address, .. } => {
+				break address;
+			},
+			_ => {},
+		}
+	};
+	let libp2p_address = libp2p_address.with_p2p(*libp2p.local_peer_id()).unwrap();
+	let libp2p_address: sc_network_types::multiaddr::Multiaddr = libp2p_address.clone().into();
+	litep2p.dial_address(libp2p_address.into()).await.unwrap();
+
+	let mut libp2p_connected = false;
+	let mut litep2p_connected = false;
+
+	loop {
+		tokio::select! {
+			event = litep2p.next_event() => match event.unwrap() {
+				Litep2pEvent::ConnectionEstablished { .. } => {
+					litep2p_connected = true;
+				}
+				_ => {},
+			},
+
+			event = libp2p.select_next_some() => match event {
+				SwarmEvent::ConnectionEstablished { .. } => {
+					libp2p_connected = true;
+				}
+				_ => {},
+			},
+		}
+
+		if libp2p_connected && litep2p_connected {
+			break;
+		}
+	}
+}
+
+/// A ping pong between libp2p and low level litep2p at notification level.
+#[tokio::test]
+async fn libp2p_to_litep2p_substream() {
+	let (mut litep2p, mut handle) = setup_litep2p().await;
+	let (mut libp2p, peerstore, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_peer = *libp2p.local_peer_id();
+	let litep2p_peer = *litep2p.local_peer_id();
+
+	let litep2p_address = litep2p.listen_addresses().into_iter().next().unwrap().clone();
+	let address: sc_network_types::multiaddr::Multiaddr = litep2p_address.clone().into();
+	let address: libp2p::multiaddr::Multiaddr = address.into();
+	libp2p.dial(address).unwrap();
+
+	let mut libp2p_ready = false;
+	let mut litep2p_ready = false;
+	let mut litep2p_3333_seen = false;
+	let mut litep2p_4444_seen = false;
+	let mut libp2p_1111_seen = false;
+	let mut libp2p_2222_seen = false;
+
+	while !libp2p_ready ||
+		!litep2p_ready ||
+		!litep2p_3333_seen ||
+		!litep2p_4444_seen ||
+		!libp2p_1111_seen ||
+		!libp2p_2222_seen
+	{
+		tokio::select! {
+			event = libp2p.select_next_some() => match event {
+				SwarmEvent::ConnectionEstablished { .. } => {
+					peerstore.add_known_peer(litep2p_peer.into());
+				}
+				SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen {  peer_id, set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+					assert_eq!(peer_id.to_bytes(), litep2p_peer.to_bytes());
+					assert_eq!(set_id, SetId::from(0usize));
+					assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+					assert!(negotiated_fallback.is_none());
+
+					notifications_sink.reserve_notification().await.unwrap().send(vec![3, 3, 3, 3]).unwrap();
+					notifications_sink.send_sync_notification(vec![4, 4, 4, 4]);
+
+					libp2p_ready = true;
+				}
+				SwarmEvent::Behaviour(NotificationsOut::Notification { peer_id, set_id, message }) => {
+					assert_eq!(peer_id.to_bytes(), litep2p_peer.to_bytes());
+					assert_eq!(set_id, SetId::from(0usize));
+
+					if message == vec![1, 1, 1, 1] {
+						libp2p_1111_seen = true;
+					} else if message == vec![2, 2, 2, 2] {
+						libp2p_2222_seen = true;
+					}
+				}
+				_ => {},
+			},
+
+			_event = litep2p.next_event() => {},
+
+			event = handle.next() => match event.unwrap() {
+				Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+					assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+					assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+					handle.send_validation_result(peer, Litep2pValidationResult::Accept);
+					litep2p_ready = true;
+				}
+				Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+					assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+					assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+					handle.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+					handle.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+				}
+				Litep2pNotificationEvent::NotificationReceived { peer, notification } => {
+					assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+
+					if notification == vec![3, 3, 3, 3] {
+						litep2p_3333_seen = true;
+					} else if notification == vec![4, 4, 4, 4] {
+						litep2p_4444_seen = true;
+					}
+				}
+				_ => {},
+			}
+		}
+	}
+}
+
+/// Litep2p rejects the libp2p substream. The connection finishes due to the keep-alive mechanism
+/// detecting the connection as idle. In this case, substrate does not force reopen the substreams.
+#[tokio::test]
+async fn litep2p_rejects_libp2p_substream() {
+	let (mut litep2p, mut handle) = setup_litep2p().await;
+	let (mut libp2p, peerstore, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_peer = *libp2p.local_peer_id();
+	let litep2p_peer = *litep2p.local_peer_id();
+
+	let litep2p_address = litep2p.listen_addresses().into_iter().next().unwrap().clone();
+	let address: sc_network_types::multiaddr::Multiaddr = litep2p_address.clone().into();
+	let address: libp2p::multiaddr::Multiaddr = address.into();
+	libp2p.dial(address).unwrap();
+
+	const RETRY_DURATION: Duration = Duration::from_secs(5);
+	// Check that libp2p does not eagerly retry opening the rejected substream by litep2p.
+	let mut first_notification = None;
+
+	loop {
+		tokio::select! {
+			event = libp2p.select_next_some() => {
+				log::info!("[libp2p] event: {event:?}");
+
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore.add_known_peer(litep2p_peer.into());
+					}
+					_ => {},
+				}
+			},
+
+			event = litep2p.next_event() => {
+				log::info!("[litep2p] event: {event:?}");
+
+				match event {
+					Some(Litep2pEvent::ConnectionClosed { .. }) => break,
+					_ => {},
+				}
+			},
+
+			event = handle.next() => match event.unwrap() {
+				Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+					assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+					assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+					handle.send_validation_result(peer, Litep2pValidationResult::Reject);
+					log::info!("reject substream");
+
+					match first_notification {
+						None => {
+							first_notification = Some(std::time::Instant::now());
+						},
+						Some(instant) => {
+							let elapsed = instant.elapsed();
+							if elapsed < RETRY_DURATION {
+								log::error!("Expecting libp2p substream to retry after 5 seconds, elapsed: {elapsed:?}");
+								panic!("Libp2p substream was not rejected and is expected to retry after 5 seconds");
+							} else {
+								// Finish testing.
+								return;
+							}
+						},
+					}
+				}
+				_ => {},
+			},
+		}
+	}
+}
+
+/// Libp2p LHS disconnects from Libp2p RHS after receiving a notification.
+/// The protocol controller reopens the substream and the connection is re-established.
+#[tokio::test]
+async fn libp2p_disconnects_libp2p_substream() {
+	let (mut libp2p_lhs, peerstore_lhs, _notification_service) = setup_libp2p(1, 1);
+	let (mut libp2p_rhs, peerstore_rhs, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_lhs_peer = *libp2p_lhs.local_peer_id();
+	let libp2p_rhs_peer = *libp2p_rhs.local_peer_id();
+
+	let libp2p_lhs_address = loop {
+		let event = libp2p_lhs.select_next_some().await;
+		match event {
+			SwarmEvent::NewListenAddr { address, .. } => {
+				log::info!("libp2p lhs address: {address:?}");
+				break address.clone();
+			},
+			_ => {},
+		}
+	};
+
+	libp2p_rhs.dial(libp2p_lhs_address).unwrap();
+
+	// Disarm first timer interval that fires immediately.
+	let mut timer = tokio::time::interval(std::time::Duration::from_secs(5));
+	timer.tick().await;
+
+	let mut sink = None;
+	let mut notification_count = 0;
+	let mut open_times = 0;
+	let mut recv_1111 = 0;
+	let mut recv_2222 = 0;
+	let mut recv_3333 = 0;
+	let mut recv_4444 = 0;
+	let mut recv_5555 = 0;
+	let mut recv_6666 = 0;
+
+	loop {
+		tokio::select! {
+			_ = timer.tick() => {
+				break;
+			}
+
+			event = libp2p_lhs.select_next_some() => {
+				log::info!("[libp2p lhs] event: {event:?}");
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore_lhs.add_known_peer(libp2p_rhs_peer.into());
+					},
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						notifications_sink.reserve_notification().await.unwrap().send(vec![3, 3, 3, 3]).unwrap();
+						notifications_sink.send_sync_notification(vec![4, 4, 4, 4]);
+
+						open_times += 1;
+					},
+					SwarmEvent::Behaviour(NotificationsOut::Notification { peer_id, set_id, message }) => {
+						if message == vec![1, 1, 1, 1] {
+							recv_1111 += 1;
+						} else if message == vec![2, 2, 2, 2] {
+							recv_2222 += 1;
+						} else if message == vec![3, 3, 3, 3] {
+							recv_3333+= 1;
+						} else if message == vec![4, 4, 4, 4] {
+							recv_4444 += 1;
+						} else if message == vec![5, 5, 5, 5] {
+							recv_5555 += 1;
+						} else if message == vec![6, 6, 6, 6] {
+							recv_6666 += 1;
+						}
+
+						notification_count += 1;
+						if notification_count == 2 {
+							// Disconnect the peer.
+							log::info!("Disconnecting peer: {peer_id:?}");
+
+							libp2p_lhs.behaviour_mut().disconnect_peer(&peer_id, set_id);
+						}
+					},
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => {
+						// Use the rhs sink to send notifications to the lhs node.
+						//
+						// The rhs node is not aware that the lhs node has disconnected.
+						// This is because lhs disconnected the node on the second notification received above,
+						// and because of this it is guaranteed for the LHS swarm to promptly generate
+						// the `NotificationsOut::CustomProtocolClosed` event. At the same time, the rhs swarm
+						// is not polled.
+						//
+						// Because the lhs substream is closed, these notifications are lost. However,
+						// the rhs behaves as if the substream is still open.
+						let sink: crate::service::NotificationsSink = sink.take().unwrap();
+						sink.reserve_notification().await.unwrap().send(vec![5, 5, 5, 5]).unwrap();
+						sink.send_sync_notification(vec![6, 6, 6, 6]);
+					}
+					_ => {},
+				}
+			}
+
+			event = libp2p_rhs.select_next_some() => {
+				log::info!("[libp2p lhs] event: {event:?}");
+
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore_rhs.add_known_peer(libp2p_lhs_peer.into());
+					},
+					SwarmEvent::ConnectionClosed { .. } => {
+						panic!("Keep alive should not close the connection because the notification controller should reopen the substream");
+					}
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						notifications_sink.reserve_notification().await.unwrap().send(vec![1, 1, 1, 1]).unwrap();
+						notifications_sink.send_sync_notification(vec![2, 2, 2, 2]);
+						sink = Some(notifications_sink);
+					},
+					SwarmEvent::Behaviour(NotificationsOut::Notification {message, .. }) => {
+						if message == vec![1, 1, 1, 1] {
+							recv_1111 += 1;
+						} else if message == vec![2, 2, 2, 2] {
+							recv_2222 += 1;
+						} else if message == vec![3, 3, 3, 3] {
+							recv_3333+= 1;
+						} else if message == vec![4, 4, 4, 4] {
+							recv_4444 += 1;
+						} else if message == vec![5, 5, 5, 5] {
+							recv_5555 += 1;
+						} else if message == vec![6, 6, 6, 6] {
+							recv_6666 += 1;
+						}
+					}
+					_ => {},
+				}
+
+			},
+		}
+	}
+
+	assert_eq!(open_times, 2);
+	assert_eq!(notification_count, 4);
+	assert_eq!(recv_1111, 2);
+	assert_eq!(recv_2222, 2);
+	assert_eq!(recv_3333, 2);
+	assert_eq!(recv_4444, 2);
+	assert_eq!(recv_5555, 0);
+	assert_eq!(recv_6666, 0);
+}
+
+/// Libp2p disconnects the litep2p substream.
+/// Then, the libp2p protocol controller reopens the substream and the connection is
+/// re-established.
+///
+/// # Warning
+///
+/// It is unclear at the moment if this behavior is intended, considering that libp2p disconnected
+/// the peer.
+#[tokio::test]
+async fn libp2p_disconnects_litep2p_substream() {
+	let (mut litep2p, mut handle) = setup_litep2p().await;
+	let (mut libp2p, peerstore, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_peer = *libp2p.local_peer_id();
+	let litep2p_peer = *litep2p.local_peer_id();
+
+	let litep2p_address = litep2p.listen_addresses().into_iter().next().unwrap().clone();
+	let address: sc_network_types::multiaddr::Multiaddr = litep2p_address.clone().into();
+	let address: libp2p::multiaddr::Multiaddr = address.into();
+	libp2p.dial(address).unwrap();
+
+	// Disarm first timer interval.
+	let mut timer = tokio::time::interval(std::time::Duration::from_secs(5));
+	timer.tick().await;
+
+	let mut notification_count = 0;
+	let mut open_times = 0;
+	let mut recv_1111 = 0;
+	let mut recv_2222 = 0;
+	let mut recv_3333 = 0;
+	let mut recv_4444 = 0;
+	let mut recv_5555 = 0;
+	let mut recv_6666 = 0;
+
+	loop {
+		tokio::select! {
+			_ = timer.tick() => {
+				break;
+			}
+
+			event = libp2p.select_next_some() => {
+				log::info!("[libp2p lhs] event: {event:?}");
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore.add_known_peer(litep2p_peer.into());
+					},
+
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						notifications_sink.reserve_notification().await.unwrap().send(vec![3, 3, 3, 3]).unwrap();
+						notifications_sink.send_sync_notification(vec![4, 4, 4, 4]);
+
+						open_times += 1;
+					},
+
+					SwarmEvent::Behaviour(NotificationsOut::Notification { peer_id, set_id, message }) => {
+						if message == vec![1, 1, 1, 1] {
+							recv_1111 += 1;
+						} else if message == vec![2, 2, 2, 2] {
+							recv_2222 += 1;
+						} else if message == vec![3, 3, 3, 3] {
+							recv_3333+= 1;
+						} else if message == vec![4, 4, 4, 4] {
+							recv_4444 += 1;
+						} else if message == vec![5, 5, 5, 5] {
+							recv_5555 += 1;
+						} else if message == vec![6, 6, 6, 6] {
+							recv_6666 += 1;
+						}
+
+						notification_count += 1;
+						if notification_count == 2 {
+							// Disconnect the peer.
+							log::info!("Disconnecting peer: {peer_id:?}");
+
+							libp2p.behaviour_mut().disconnect_peer(&peer_id, set_id);
+						}
+					},
+
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => {
+						// At this point libp2p is disconnected from litep2p.
+						// However, litep2p still thinks its connected to libp2p and this notification is entirely lost.
+						let libp2p_peer: sc_network_types::PeerId = libp2p_peer.into();
+						handle.send_sync_notification(libp2p_peer.into(), vec![5, 5, 5, 5]).unwrap();
+						handle.send_async_notification(libp2p_peer.into(), vec![6, 6, 6, 6]).await.unwrap();
+					}
+					_ => {},
+				}
+			}
+
+			event = litep2p.next_event() => {
+				log::info!("[litep2p] event: {event:?}");
+
+				match event {
+					Some(Litep2pEvent::ConnectionClosed { .. }) => {
+						panic!("Litep2p connection should not be closed by libp2p");
+					}
+					_ => {},
+				}
+			},
+
+			event = handle.next() => {
+				log::info!("[litep2p handle] event: {event:?}");
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle.send_validation_result(peer, Litep2pValidationResult::Accept);
+					},
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, notification: message } => {
+						if message == vec![1, 1, 1, 1] {
+							recv_1111 += 1;
+						} else if message == vec![2, 2, 2, 2] {
+							recv_2222 += 1;
+						} else if message == vec![3, 3, 3, 3] {
+							recv_3333+= 1;
+						} else if message == vec![4, 4, 4, 4] {
+							recv_4444 += 1;
+						} else if message == vec![5, 5, 5, 5] {
+							recv_5555 += 1;
+						} else if message == vec![6, 6, 6, 6] {
+							recv_6666 += 1;
+						}
+
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+					}
+					event => log::info!("unhandled notification event: {event:?}"),
+				}
+			},
+		}
+	}
+
+	assert_eq!(open_times, 2);
+	assert_eq!(notification_count, 4);
+	assert_eq!(recv_1111, 2);
+	assert_eq!(recv_2222, 2);
+	assert_eq!(recv_3333, 2);
+	assert_eq!(recv_4444, 2);
+	assert_eq!(recv_5555, 0);
+	assert_eq!(recv_6666, 0);
+}
+
+/// Litep2p force-closes the substream with libp2p. The protocol controller of libp2p reopens the
+/// substream.
+///
+/// The keep-alive mechanism should not detect the connection as closed, as it has at least one open
+/// substream.
+#[tokio::test]
+async fn litep2p_disconnects_libp2p_substream() {
+	let (mut litep2p, mut handle) = setup_litep2p().await;
+	let (mut libp2p, peerstore, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_peer = *libp2p.local_peer_id();
+	let litep2p_peer = *litep2p.local_peer_id();
+
+	let litep2p_address = litep2p.listen_addresses().into_iter().next().unwrap().clone();
+	let address: sc_network_types::multiaddr::Multiaddr = litep2p_address.clone().into();
+	let address: libp2p::multiaddr::Multiaddr = address.into();
+	libp2p.dial(address).unwrap();
+
+	let mut notification_count = 0;
+	let mut open_times = 0;
+
+	// Disarm first timer interval.
+	let mut timer = tokio::time::interval(std::time::Duration::from_secs(u64::MAX));
+	timer.tick().await;
+
+	loop {
+		tokio::select! {
+			_ = timer.tick() => {
+				assert_eq!(notification_count, 4);
+				assert_eq!(open_times, 2);
+				return;
+			}
+
+			event = libp2p.select_next_some() => {
+				log::info!("[libp2p] event: {event:?}");
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore.add_known_peer(litep2p_peer.into());
+					},
+
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						open_times += 1;
+					},
+
+					SwarmEvent::Behaviour(NotificationsOut::Notification { peer_id, .. }) => {
+						notification_count += 1;
+
+						if notification_count == 2 {
+							// Disconnect the peer.
+							log::info!("Disconnecting peer: {peer_id:?}");
+
+							let libp2p_peer: sc_network_types::PeerId = libp2p_peer.into();
+							handle.close_substream(libp2p_peer.into()).await;
+
+							// After closing the substream set the timer to 6s to ensure the connection does
+							// not close due to the keep-alive mechanism of 5s.
+							timer = tokio::time::interval(std::time::Duration::from_secs(6));
+							timer.tick().await;
+						}
+					},
+					_ => {},
+				}
+			}
+
+			event = litep2p.next_event() => {
+				log::info!("[litep2p] event: {event:?}");
+			},
+
+			event = handle.next() => {
+				log::info!("[litep2p handle] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle.send_validation_result(peer, Litep2pValidationResult::Accept);
+					},
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, .. } => {
+						assert_eq!(peer.to_bytes(), libp2p_peer.to_bytes());
+
+					}
+					Litep2pNotificationEvent::NotificationStreamClosed { .. } => {
+					}
+					_ => {},
+				}
+			},
+		}
+	}
+}
+
+/// Raw litep2p closes the substream with a raw litep2p.
+/// In this case, there's no protocol controller from substrate that will reopen the substream.
+/// Therefore, since the substream is closed, the connection will be closed by the keep-alive.
+#[tokio::test]
+async fn litep2p_disconnects_litep2p_substream() {
+	let (mut litep2p_lhs, mut handle_lhs) = setup_litep2p().await;
+	let (mut litep2p_rhs, mut handle_rhs) = setup_litep2p().await;
+
+	let litep2p_lhs_peer = *litep2p_lhs.local_peer_id();
+	let litep2p_rhs_peer = *litep2p_rhs.local_peer_id();
+
+	let litep2p_address = litep2p_lhs.listen_addresses().into_iter().next().unwrap().clone();
+	litep2p_rhs.dial_address(litep2p_address).await.unwrap();
+
+	let mut num_closed = 0;
+	let mut lhs_opened = 0;
+	let mut lhs_closed = 0;
+	let mut rhs_opened = 0;
+	let mut rhs_closed = 0;
+
+	loop {
+		tokio::select! {
+			event = litep2p_lhs.next_event() => {
+				log::info!("[litep2p_lhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pEvent::ConnectionEstablished { .. } => {
+						handle_rhs.open_substream(litep2p_lhs_peer).await.unwrap();
+					}
+					Litep2pEvent::ConnectionClosed { .. } => {
+						num_closed += 1;
+						if num_closed == 2 {
+							break;
+						}
+					}
+					_ => {},
+				}
+			},
+
+			event = litep2p_rhs.next_event() => {
+				log::info!("[litep2p_rhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pEvent::ConnectionEstablished { .. } => {
+						handle_lhs.open_substream(litep2p_rhs_peer).await.unwrap();
+					}
+					Litep2pEvent::ConnectionClosed { .. } => {
+						num_closed += 1;
+						if num_closed == 2 {
+							break;
+						}
+					}
+					_ => {},
+				}
+			},
+
+			event = handle_rhs.next() => {
+				log::info!("[handle_rhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_rhs.send_validation_result(peer, Litep2pValidationResult::Accept);
+					}
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+						rhs_opened += 1;
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_rhs.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle_rhs.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+
+						handle_lhs.close_substream(litep2p_rhs_peer).await;
+					}
+					Litep2pNotificationEvent::NotificationStreamClosed { peer, .. } => {
+						rhs_closed += 1;
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+					}
+					_ => {},
+				}
+			}
+
+			event = handle_lhs.next() => {
+				log::info!("[handle_lhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_lhs.send_validation_result(peer, Litep2pValidationResult::Accept);
+					}
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+						lhs_opened += 1;
+
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_lhs.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle_lhs.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+					}
+					Litep2pNotificationEvent::NotificationStreamClosed { peer, .. } => {
+						lhs_closed += 1;
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+					}
+					_ => {},
+				}
+			}
+		}
+	}
+
+	assert_eq!(lhs_opened, 1);
+	assert_eq!(lhs_closed, 1);
+	assert_eq!(rhs_opened, 1);
+	assert_eq!(rhs_closed, 1);
+}
+
+/// Keep the substream idle for a long time and ensure the connection is not closed by the
+/// keep-alive mechanism.
+#[tokio::test]
+async fn litep2p_idle_litep2p_substream() {
+	let (mut litep2p_lhs, mut handle_lhs) = setup_litep2p().await;
+	let (mut litep2p_rhs, mut handle_rhs) = setup_litep2p().await;
+
+	let litep2p_lhs_peer = *litep2p_lhs.local_peer_id();
+	let litep2p_rhs_peer = *litep2p_rhs.local_peer_id();
+
+	let litep2p_address = litep2p_lhs.listen_addresses().into_iter().next().unwrap().clone();
+	litep2p_rhs.dial_address(litep2p_address).await.unwrap();
+
+	// Disarm first timer interval.
+	let mut timer = tokio::time::interval(std::time::Duration::from_secs(6));
+	timer.tick().await;
+
+	loop {
+		tokio::select! {
+			_ = timer.tick() => {
+				return;
+			}
+
+			event = litep2p_lhs.next_event() => {
+				log::info!("[litep2p lhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pEvent::ConnectionEstablished { .. } => {
+						handle_rhs.open_substream(litep2p_lhs_peer).await.unwrap();
+					}
+					_ => {},
+				}
+			},
+
+			event = litep2p_rhs.next_event() => {
+				log::info!("[litep2p rhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pEvent::ConnectionEstablished { .. } => {
+						handle_lhs.open_substream(litep2p_lhs_peer).await.unwrap();
+					}
+					_ => {},
+				}
+			},
+
+			event = handle_rhs.next() => {
+				log::info!("[handle rhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_rhs.send_validation_result(peer, Litep2pValidationResult::Accept);
+					}
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_rhs.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle_rhs.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_lhs_peer.to_bytes());
+					}
+					Litep2pNotificationEvent::NotificationStreamClosed { .. } => {
+						panic!("Substream should not be closed by the keep-alive mechanism");
+					}
+					_ => {},
+				}
+			}
+
+			event = handle_lhs.next() => {
+				log::info!("[handle lhs] event: {event:?}");
+
+				match event.unwrap() {
+					Litep2pNotificationEvent::ValidateSubstream { peer, handshake, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_lhs.send_validation_result(peer, Litep2pValidationResult::Accept);
+					}
+					Litep2pNotificationEvent::NotificationStreamOpened { peer, handshake, ..} => {
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+						assert_eq!(handshake, vec![1, 2, 3, 4]);
+
+						handle_lhs.send_sync_notification(peer, vec![1, 1, 1, 1]).unwrap();
+						handle_lhs.send_async_notification(peer, vec![2, 2, 2, 2]).await.unwrap();
+					}
+					Litep2pNotificationEvent::NotificationReceived { peer, .. } => {
+						assert_eq!(peer.to_bytes(), litep2p_rhs_peer.to_bytes());
+					}
+					Litep2pNotificationEvent::NotificationStreamClosed { .. } => {
+						panic!("Substream should not be closed by the keep-alive mechanism");
+					}
+					_ => {},
+				}
+			}
+		}
+	}
+}
+
+/// Keep the substream idle for a long time and ensure the connection is not closed by the
+/// keep-alive mechanism.
+#[tokio::test]
+async fn libp2p_idle_to_libp2p_substream() {
+	let (mut libp2p_lhs, peerstore_lhs, _notification_service) = setup_libp2p(1, 1);
+	let (mut libp2p_rhs, peerstore_rhs, _notification_service) = setup_libp2p(1, 1);
+
+	let libp2p_lhs_peer = *libp2p_lhs.local_peer_id();
+	let libp2p_rhs_peer = *libp2p_rhs.local_peer_id();
+
+	let libp2p_lhs_address = loop {
+		let event = libp2p_lhs.select_next_some().await;
+		match event {
+			SwarmEvent::NewListenAddr { address, .. } => {
+				log::info!("libp2p lhs listener: {address:?}");
+
+				break address.clone();
+			},
+			_ => {},
+		}
+	};
+
+	libp2p_rhs.dial(libp2p_lhs_address).unwrap();
+
+	// Disarm first timer interval.
+	let mut timer = tokio::time::interval(std::time::Duration::from_secs(6));
+	timer.tick().await;
+
+	loop {
+		tokio::select! {
+			_ = timer.tick() => {
+				return;
+			}
+
+			event = libp2p_lhs.select_next_some() => {
+				log::info!("[libp2p lhs] event: {event:?}");
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore_lhs.add_known_peer(libp2p_rhs_peer.into());
+					},
+					SwarmEvent::ConnectionClosed { .. } => {
+						panic!("Connection should not be closed by the keep-alive mechanism");
+					}
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						notifications_sink.reserve_notification().await.unwrap().send(vec![3, 3, 3, 3]).unwrap();
+						notifications_sink.send_sync_notification(vec![4, 4, 4, 4]);
+					},
+					SwarmEvent::Behaviour(NotificationsOut::Notification { .. }) => { },
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => {
+						panic!("Libp2p substream should not be closed by the keep-alive mechanism");
+					}
+					_ => {},
+				}
+			}
+
+			event = libp2p_rhs.select_next_some() => {
+				log::info!("[LIBP2P LHS] event: {event:?}");
+
+				match event {
+					SwarmEvent::ConnectionEstablished { .. } => {
+						peerstore_rhs.add_known_peer(libp2p_lhs_peer.into());
+					},
+					SwarmEvent::ConnectionClosed { .. } => {
+						panic!("Connection should not be closed by the keep-alive mechanism");
+					}
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { set_id, negotiated_fallback, received_handshake, notifications_sink, .. }) => {
+						assert_eq!(set_id, SetId::from(0usize));
+						assert_eq!(received_handshake, vec![1, 2, 3, 4]);
+						assert!(negotiated_fallback.is_none());
+
+						notifications_sink.reserve_notification().await.unwrap().send(vec![3, 3, 3, 3]).unwrap();
+						notifications_sink.send_sync_notification(vec![4, 4, 4, 4]);
+					},
+					SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => {
+						panic!("Libp2p substream should not be closed by the keep-alive mechanism");
+					}
+					_ => {},
+				}
+			},
+		}
+	}
+}
diff --git a/substrate/client/network/src/protocol/notifications/tests.rs b/substrate/client/network/src/protocol/notifications/tests/mod.rs
similarity index 99%
rename from substrate/client/network/src/protocol/notifications/tests.rs
rename to substrate/client/network/src/protocol/notifications/tests/mod.rs
index 50f03b5911b67cb526800b64f7395525a9b14436..80ed0aa9f189ba00c658835aba96ab845b7c61cd 100644
--- a/substrate/client/network/src/protocol/notifications/tests.rs
+++ b/substrate/client/network/src/protocol/notifications/tests/mod.rs
@@ -49,6 +49,9 @@ use std::{
 	time::Duration,
 };
 
+#[cfg(test)]
+mod conformance;
+
 /// Builds two nodes that have each other as bootstrap nodes.
 /// This is to be used only for testing, and a panic will happen if something goes wrong.
 fn build_nodes() -> (Swarm<CustomProtoWithAddr>, Swarm<CustomProtoWithAddr>) {