diff --git a/Cargo.lock b/Cargo.lock
index 5b2879e1ba9b6..a645ecec60d86 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9275,6 +9275,7 @@ dependencies = [
"sc-chain-spec",
"sc-client-api",
"sc-transaction-pool-api",
+ "sc-utils",
"serde",
"serde_json",
"sp-api",
@@ -11954,7 +11955,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"digest 0.10.6",
- "rand 0.7.3",
+ "rand 0.8.5",
"static_assertions",
]
diff --git a/client/rpc-spec-v2/Cargo.toml b/client/rpc-spec-v2/Cargo.toml
index 43fb189081bae..23b96877f3b17 100644
--- a/client/rpc-spec-v2/Cargo.toml
+++ b/client/rpc-spec-v2/Cargo.toml
@@ -43,4 +43,5 @@ substrate-test-runtime = { version = "2.0.0", path = "../../test-utils/runtime"
sp-consensus = { version = "0.10.0-dev", path = "../../primitives/consensus/common" }
sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../primitives/maybe-compressed-blob" }
sc-block-builder = { version = "0.10.0-dev", path = "../block-builder" }
+sc-utils = { version = "4.0.0-dev", path = "../utils" }
assert_matches = "1.3.0"
diff --git a/client/rpc-spec-v2/src/chain_head/mod.rs b/client/rpc-spec-v2/src/chain_head/mod.rs
index afa8d3b2189ae..1c489d323f195 100644
--- a/client/rpc-spec-v2/src/chain_head/mod.rs
+++ b/client/rpc-spec-v2/src/chain_head/mod.rs
@@ -22,6 +22,8 @@
//!
//! Methods are prefixed by `chainHead`.
+#[cfg(test)]
+mod test_utils;
#[cfg(test)]
mod tests;
diff --git a/client/rpc-spec-v2/src/chain_head/test_utils.rs b/client/rpc-spec-v2/src/chain_head/test_utils.rs
new file mode 100644
index 0000000000000..ee563debb4502
--- /dev/null
+++ b/client/rpc-spec-v2/src/chain_head/test_utils.rs
@@ -0,0 +1,320 @@
+// 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 .
+
+use parking_lot::Mutex;
+use sc_client_api::{
+ execution_extensions::ExecutionExtensions, BlockBackend, BlockImportNotification,
+ BlockchainEvents, CallExecutor, ChildInfo, ExecutorProvider, FinalityNotification,
+ FinalityNotifications, FinalizeSummary, ImportNotifications, KeysIter, PairsIter, StorageData,
+ StorageEventStream, StorageKey, StorageProvider,
+};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
+use sp_api::{CallApiAt, CallApiAtParams, NumberFor, RuntimeVersion};
+use sp_blockchain::{BlockStatus, CachedHeaderMetadata, HeaderBackend, HeaderMetadata, Info};
+use sp_consensus::BlockOrigin;
+use sp_runtime::{
+ generic::SignedBlock,
+ traits::{Block as BlockT, Header as HeaderT},
+ Justifications,
+};
+use std::sync::Arc;
+use substrate_test_runtime::{Block, Hash, Header};
+
+pub struct ChainHeadMockClient {
+ client: Arc,
+ import_sinks: Mutex>>>,
+ finality_sinks: Mutex>>>,
+}
+
+impl ChainHeadMockClient {
+ pub fn new(client: Arc) -> Self {
+ ChainHeadMockClient {
+ client,
+ import_sinks: Default::default(),
+ finality_sinks: Default::default(),
+ }
+ }
+
+ pub async fn trigger_import_stream(&self, header: Header) {
+ // Ensure the client called the `import_notification_stream`.
+ while self.import_sinks.lock().is_empty() {
+ tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
+ }
+
+ // Build the notification.
+ let (sink, _stream) = tracing_unbounded("test_sink", 100_000);
+ let notification =
+ BlockImportNotification::new(header.hash(), BlockOrigin::Own, header, true, None, sink);
+
+ for sink in self.import_sinks.lock().iter_mut() {
+ sink.unbounded_send(notification.clone()).unwrap();
+ }
+ }
+
+ pub async fn trigger_finality_stream(&self, header: Header) {
+ // Ensure the client called the `finality_notification_stream`.
+ while self.finality_sinks.lock().is_empty() {
+ tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
+ }
+
+ // Build the notification.
+ let (sink, _stream) = tracing_unbounded("test_sink", 100_000);
+ let summary = FinalizeSummary {
+ header: header.clone(),
+ finalized: vec![header.hash()],
+ stale_heads: vec![],
+ };
+ let notification = FinalityNotification::from_summary(summary, sink);
+
+ for sink in self.finality_sinks.lock().iter_mut() {
+ sink.unbounded_send(notification.clone()).unwrap();
+ }
+ }
+}
+
+// ChainHead calls `import_notification_stream` and `finality_notification_stream` in order to
+// subscribe to block events.
+impl BlockchainEvents for ChainHeadMockClient {
+ fn import_notification_stream(&self) -> ImportNotifications {
+ let (sink, stream) = tracing_unbounded("import_notification_stream", 1024);
+ self.import_sinks.lock().push(sink);
+ stream
+ }
+
+ fn every_import_notification_stream(&self) -> ImportNotifications {
+ unimplemented!()
+ }
+
+ fn finality_notification_stream(&self) -> FinalityNotifications {
+ let (sink, stream) = tracing_unbounded("finality_notification_stream", 1024);
+ self.finality_sinks.lock().push(sink);
+ stream
+ }
+
+ fn storage_changes_notification_stream(
+ &self,
+ _filter_keys: Option<&[StorageKey]>,
+ _child_filter_keys: Option<&[(StorageKey, Option>)]>,
+ ) -> sp_blockchain::Result> {
+ unimplemented!()
+ }
+}
+
+// The following implementations are imposed by the `chainHead` trait bounds.
+
+impl, Client: ExecutorProvider>
+ ExecutorProvider for ChainHeadMockClient
+{
+ type Executor = >::Executor;
+
+ fn executor(&self) -> &Self::Executor {
+ self.client.executor()
+ }
+
+ fn execution_extensions(&self) -> &ExecutionExtensions {
+ self.client.execution_extensions()
+ }
+}
+
+impl<
+ BE: sc_client_api::backend::Backend + Send + Sync + 'static,
+ Block: BlockT,
+ Client: StorageProvider,
+ > StorageProvider for ChainHeadMockClient
+{
+ fn storage(
+ &self,
+ hash: Block::Hash,
+ key: &StorageKey,
+ ) -> sp_blockchain::Result