Repository: tangle-network/gadget (language: rust)

Issue:
[BUG] Public Key gossiping stuck when `n=t`
Current implementation of [`handle_public_key_gossip`](https://github.com/webb-tools/gadget/blob/3e067f3e2a2516edf356475407e850430a0d98af/protocols/dfns-cggmp21/src/protocols/keygen.rs#L453-L453) gets stuck in this loop [here](https://github.com/webb-tools/gadget/blob/3e067f3e2a2516edf356475407e850430a0d98af/protocols/dfns-cggmp21/src/protocols/keygen.rs#L485-L525) when `n = t`, for example a `2/2` keyen does not work.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index d1c390305..b35fd20cc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -147,9 +147,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.81"
+version = "1.0.80"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
 
 [[package]]
 name = "approx"
@@ -634,6 +634,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
 [[package]]
 name = "async-channel"
 version = "1.9.0"
@@ -685,9 +691,9 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "2.3.2"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
+checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
 dependencies = [
  "async-lock 3.3.0",
  "cfg-if",
@@ -839,6 +845,19 @@ dependencies = [
  "pin-project-lite 0.2.13",
 ]
 
+[[package]]
+name = "asynchronous-codec"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233"
+dependencies = [
+ "bytes",
+ "futures-sink",
+ "futures-util",
+ "memchr",
+ "pin-project-lite 0.2.13",
+]
+
 [[package]]
 name = "atoi"
 version = "2.0.0"
@@ -879,6 +898,17 @@ dependencies = [
  "rand 0.8.5",
 ]
 
+[[package]]
+name = "attohttpc"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2"
+dependencies = [
+ "http",
+ "log",
+ "url",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -1164,9 +1194,9 @@ dependencies = [
 
 [[package]]
 name = "blake3"
-version = "1.5.1"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
+checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87"
 dependencies = [
  "arrayref",
  "arrayvec 0.7.4",
@@ -1283,11 +1313,20 @@ dependencies = [
  "tinyvec",
 ]
 
+[[package]]
+name = "build-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdce191bf3fa4995ce948c8c83b4640a1745457a149e73c6db75b4ffe36aad5f"
+dependencies = [
+ "semver 0.6.0",
+]
+
 [[package]]
 name = "bumpalo"
-version = "3.15.4"
+version = "3.15.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
 
 [[package]]
 name = "byte-slice-cast"
@@ -1325,18 +1364,18 @@ dependencies = [
 
 [[package]]
 name = "bytemuck"
-version = "1.15.0"
+version = "1.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
+checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
 dependencies = [
  "bytemuck_derive",
 ]
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.6.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60"
+checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1379,12 +1418,53 @@ dependencies = [
  "ppv-lite86",
 ]
 
+[[package]]
+name = "camino"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver 1.0.22",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
 [[package]]
 name = "cast"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
+[[package]]
+name = "cbor4ii"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b4c883b9cc4757b061600d39001d4d0232bece4a3174696cf8f58a14db107d"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.90"
@@ -1503,9 +1583,9 @@ checksum = "b9b68e3193982cd54187d71afdb2a271ad4cf8af157858e9cb911b91321de143"
 dependencies = [
  "core2",
  "multibase",
- "multihash",
+ "multihash 0.17.0",
  "serde",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
 ]
 
 [[package]]
@@ -1569,6 +1649,16 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
 
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
 [[package]]
 name = "color-eyre"
 version = "0.6.2"
@@ -1582,6 +1672,7 @@ dependencies = [
  "once_cell",
  "owo-colors",
  "tracing-error",
+ "url",
 ]
 
 [[package]]
@@ -1637,6 +1728,19 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "console"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "unicode-width",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "const-crc32"
 version = "1.3.0"
@@ -1645,9 +1749,9 @@ checksum = "68d13f542d70e5b339bf46f6f74704ac052cfd526c58cd87996bd1ef4615b9a0"
 
 [[package]]
 name = "const-hex"
-version = "1.11.3"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78"
+checksum = "efbd12d49ab0eaf8193ba9175e45f56bbc2e4b27d57b8cfe62aa47942a46b9a9"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -2222,6 +2326,50 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "cxx"
+version = "1.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "635179be18797d7e10edb9cd06c859580237750c7351f39ed9b298bfc17544ad"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9324397d262f63ef77eb795d900c0d682a34a43ac0932bec049ed73055d52f63"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a87ff7342ffaa54b7c61618e0ce2bbcf827eba6d55b923b83d82551acbbecfe5"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70b5b86cf65fa0626d85720619d80b288013477a91a0389fa8bc716bf4903ad1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
 [[package]]
 name = "darling"
 version = "0.14.4"
@@ -2871,6 +3019,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
 
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
 [[package]]
 name = "enum-as-inner"
 version = "0.5.1"
@@ -2883,6 +3037,18 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "enum-as-inner"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
 [[package]]
 name = "enum-iterator"
 version = "0.7.0"
@@ -3101,7 +3267,7 @@ dependencies = [
  "rlp",
  "serde",
  "serde_json",
- "strum 0.26.2",
+ "strum 0.26.1",
  "tempfile",
  "thiserror",
  "tiny-keccak",
@@ -3314,6 +3480,18 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "filetime"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.4.1",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "finality-grandpa"
 version = "0.16.2"
@@ -3451,6 +3629,24 @@ dependencies = [
  "static_assertions",
 ]
 
+[[package]]
+name = "frame-executive"
+version = "28.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "frame-support",
+ "frame-system",
+ "frame-try-runtime",
+ "log",
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+]
+
 [[package]]
 name = "frame-metadata"
 version = "15.1.0"
@@ -3576,10 +3772,31 @@ dependencies = [
  "sp-weights",
 ]
 
+[[package]]
+name = "frame-system-rpc-runtime-api"
+version = "26.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "parity-scale-codec 3.6.9",
+ "sp-api",
+]
+
+[[package]]
+name = "frame-try-runtime"
+version = "0.34.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "frame-support",
+ "parity-scale-codec 3.6.9",
+ "sp-api",
+ "sp-runtime",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+]
+
 [[package]]
 name = "frost-core"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "byteorder",
  "debugless-unwrap",
@@ -3632,7 +3849,7 @@ dependencies = [
 [[package]]
 name = "frost-ed25519"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "curve25519-dalek 4.1.2",
  "frost-core 0.6.3",
@@ -3658,7 +3875,7 @@ dependencies = [
 [[package]]
 name = "frost-ed448"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "ed448-goldilocks-plus 0.11.2 (git+https://github.com/webb-tools/Ed448-Goldilocks)",
  "frost-core 0.6.3",
@@ -3684,7 +3901,7 @@ dependencies = [
 [[package]]
 name = "frost-p256"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "frost-core 0.6.3",
  "p256 0.13.2",
@@ -3710,7 +3927,7 @@ dependencies = [
 [[package]]
 name = "frost-p384"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "frost-core 0.6.3",
  "p384",
@@ -3747,7 +3964,7 @@ dependencies = [
 [[package]]
 name = "frost-ristretto255"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "curve25519-dalek 4.1.2",
  "frost-core 0.6.3",
@@ -3773,7 +3990,7 @@ dependencies = [
 [[package]]
 name = "frost-secp256k1"
 version = "1.0.0-rc.0"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "frost-core 0.6.3",
  "k256",
@@ -3835,6 +4052,16 @@ dependencies = [
  "futures-util",
 ]
 
+[[package]]
+name = "futures-bounded"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e2774cc104e198ef3d3e1ff4ab40f86fa3245d6cb6a3a46174f21463cee173"
+dependencies = [
+ "futures-timer",
+ "futures-util",
+]
+
 [[package]]
 name = "futures-channel"
 version = "0.3.30"
@@ -3915,6 +4142,16 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "futures-rustls"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd3cf68c183738046838e300353e4716c674dc5e56890de4826801a6622a28"
+dependencies = [
+ "futures-io",
+ "rustls 0.21.10",
+]
+
 [[package]]
 name = "futures-sink"
 version = "0.3.30"
@@ -3927,6 +4164,17 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
 
+[[package]]
+name = "futures-ticker"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9763058047f713632a52e916cc7f6a4b3fc6e9fc1ff8c5b1dc49e5a89041682e"
+dependencies = [
+ "futures",
+ "futures-timer",
+ "instant",
+]
+
 [[package]]
 name = "futures-timer"
 version = "3.0.3"
@@ -3974,6 +4222,7 @@ dependencies = [
  "gadget-core",
  "hex",
  "lazy_static",
+ "libp2p-gadget",
  "linked-hash-map",
  "log",
  "parity-scale-codec 3.6.9",
@@ -3982,9 +4231,7 @@ dependencies = [
  "round-based 0.1.7",
  "round-based 0.2.1",
  "sc-client-api",
- "sc-network",
- "sc-network-common",
- "sc-network-sync",
+ "sc-network-sync 0.33.0",
  "sc-service",
  "scale-info",
  "serde",
@@ -4014,6 +4261,37 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "gadget-shell"
+version = "0.1.0"
+dependencies = [
+ "async-trait",
+ "bincode2",
+ "color-eyre",
+ "dfns-cggmp21-protocol",
+ "futures",
+ "gadget-common",
+ "gadget-core",
+ "hex",
+ "libp2p 0.53.2",
+ "parity-scale-codec 3.6.9",
+ "sc-keystore",
+ "scale-info",
+ "serde",
+ "sha2 0.10.8",
+ "sp-application-crypto",
+ "sp-core",
+ "sp-keystore",
+ "structopt",
+ "tangle-subxt",
+ "test-utils",
+ "tokio",
+ "toml 0.8.11",
+ "tracing",
+ "tracing-subscriber 0.3.18",
+ "url",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.12.4"
@@ -4413,33 +4691,85 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
 
 [[package]]
-name = "hkdf"
-version = "0.12.4"
+name = "hex_fmt"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
-dependencies = [
- "hmac 0.12.1",
-]
+checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f"
 
 [[package]]
-name = "hmac"
-version = "0.8.1"
+name = "hickory-proto"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf"
 dependencies = [
- "crypto-mac 0.8.0",
- "digest 0.9.0",
+ "async-trait",
+ "cfg-if",
+ "data-encoding",
+ "enum-as-inner 0.6.0",
+ "futures-channel",
+ "futures-io",
+ "futures-util",
+ "idna 0.4.0",
+ "ipnet",
+ "once_cell",
+ "rand 0.8.5",
+ "socket2 0.5.6",
+ "thiserror",
+ "tinyvec",
+ "tokio",
+ "tracing",
+ "url",
 ]
 
 [[package]]
-name = "hmac"
-version = "0.11.0"
+name = "hickory-resolver"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8"
 dependencies = [
- "crypto-mac 0.11.0",
- "digest 0.9.0",
-]
+ "cfg-if",
+ "futures-util",
+ "hickory-proto",
+ "ipconfig",
+ "lru-cache",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "rand 0.8.5",
+ "resolv-conf",
+ "smallvec",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac 0.12.1",
+]
+
+[[package]]
+name = "hmac"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
+dependencies = [
+ "crypto-mac 0.8.0",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac 0.11.0",
+ "digest 0.9.0",
+]
 
 [[package]]
 name = "hmac"
@@ -4607,6 +4937,16 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
 [[package]]
 name = "idna"
 version = "0.5.0"
@@ -4646,6 +4986,25 @@ dependencies = [
  "windows",
 ]
 
+[[package]]
+name = "igd-next"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064d90fec10d541084e7b39ead8875a5a80d9114a2b18791565253bae25f49e4"
+dependencies = [
+ "async-trait",
+ "attohttpc",
+ "bytes",
+ "futures",
+ "http",
+ "hyper",
+ "log",
+ "rand 0.8.5",
+ "tokio",
+ "url",
+ "xmltree",
+]
+
 [[package]]
 name = "impl-codec"
 version = "0.5.1"
@@ -5173,37 +5532,85 @@ dependencies = [
  "futures-timer",
  "getrandom 0.2.12",
  "instant",
- "libp2p-allow-block-list",
- "libp2p-connection-limits",
- "libp2p-core",
- "libp2p-dns",
- "libp2p-identify",
- "libp2p-identity",
- "libp2p-kad",
- "libp2p-mdns",
- "libp2p-metrics",
- "libp2p-noise",
- "libp2p-ping",
- "libp2p-quic",
- "libp2p-request-response",
- "libp2p-swarm",
- "libp2p-tcp",
+ "libp2p-allow-block-list 0.1.1",
+ "libp2p-connection-limits 0.1.0",
+ "libp2p-core 0.39.2",
+ "libp2p-dns 0.39.0",
+ "libp2p-identify 0.42.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-kad 0.43.3",
+ "libp2p-mdns 0.43.1",
+ "libp2p-metrics 0.12.0",
+ "libp2p-noise 0.42.2",
+ "libp2p-ping 0.42.0",
+ "libp2p-quic 0.7.0-alpha.3",
+ "libp2p-request-response 0.24.1",
+ "libp2p-swarm 0.42.2",
+ "libp2p-tcp 0.39.0",
  "libp2p-wasm-ext",
  "libp2p-websocket",
- "libp2p-yamux",
- "multiaddr",
+ "libp2p-yamux 0.43.1",
+ "multiaddr 0.17.1",
  "pin-project",
 ]
 
+[[package]]
+name = "libp2p"
+version = "0.53.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "bytes",
+ "either",
+ "futures",
+ "futures-timer",
+ "getrandom 0.2.12",
+ "instant",
+ "libp2p-allow-block-list 0.3.0",
+ "libp2p-connection-limits 0.3.1",
+ "libp2p-core 0.41.2",
+ "libp2p-dcutr",
+ "libp2p-dns 0.41.1",
+ "libp2p-gossipsub",
+ "libp2p-identify 0.44.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-kad 0.45.4",
+ "libp2p-mdns 0.45.1",
+ "libp2p-metrics 0.14.1",
+ "libp2p-noise 0.44.0",
+ "libp2p-ping 0.44.0",
+ "libp2p-quic 0.10.2",
+ "libp2p-relay",
+ "libp2p-request-response 0.26.2",
+ "libp2p-swarm 0.44.2",
+ "libp2p-tcp 0.41.0",
+ "libp2p-upnp",
+ "libp2p-yamux 0.45.1",
+ "multiaddr 0.18.1",
+ "pin-project",
+ "rw-stream-sink 0.4.0",
+ "thiserror",
+]
+
 [[package]]
 name = "libp2p-allow-block-list"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "510daa05efbc25184458db837f6f9a5143888f1caa742426d92e1833ddd38a50"
 dependencies = [
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
+ "void",
+]
+
+[[package]]
+name = "libp2p-allow-block-list"
+version = "0.3.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
  "void",
 ]
 
@@ -5213,9 +5620,20 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4caa33f1d26ed664c4fe2cca81a08c8e07d4c1c04f2f4ac7655c2dd85467fda0"
 dependencies = [
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
+ "void",
+]
+
+[[package]]
+name = "libp2p-connection-limits"
+version = "0.3.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
  "void",
 ]
 
@@ -5230,20 +5648,69 @@ dependencies = [
  "futures",
  "futures-timer",
  "instant",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "log",
- "multiaddr",
- "multihash",
- "multistream-select",
+ "multiaddr 0.17.1",
+ "multihash 0.17.0",
+ "multistream-select 0.12.1",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "pin-project",
+ "quick-protobuf",
+ "rand 0.8.5",
+ "rw-stream-sink 0.3.0",
+ "smallvec",
+ "thiserror",
+ "unsigned-varint 0.7.2",
+ "void",
+]
+
+[[package]]
+name = "libp2p-core"
+version = "0.41.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "either",
+ "fnv",
+ "futures",
+ "futures-timer",
+ "instant",
+ "libp2p-identity 0.2.8",
+ "multiaddr 0.18.1",
+ "multihash 0.19.1",
+ "multistream-select 0.13.0",
  "once_cell",
  "parking_lot 0.12.1",
  "pin-project",
  "quick-protobuf",
  "rand 0.8.5",
- "rw-stream-sink",
+ "rw-stream-sink 0.4.0",
  "smallvec",
  "thiserror",
- "unsigned-varint",
+ "tracing",
+ "unsigned-varint 0.8.0",
+ "void",
+]
+
+[[package]]
+name = "libp2p-dcutr"
+version = "0.11.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
+ "either",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "lru 0.12.3",
+ "quick-protobuf",
+ "quick-protobuf-codec 0.3.1",
+ "thiserror",
+ "tracing",
  "void",
 ]
 
@@ -5254,35 +5721,156 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "146ff7034daae62077c415c2376b8057368042df6ab95f5432ad5e88568b1554"
 dependencies = [
  "futures",
- "libp2p-core",
+ "libp2p-core 0.39.2",
  "log",
  "parking_lot 0.12.1",
  "smallvec",
  "trust-dns-resolver",
 ]
 
+[[package]]
+name = "libp2p-dns"
+version = "0.41.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "async-trait",
+ "futures",
+ "hickory-resolver",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "parking_lot 0.12.1",
+ "smallvec",
+ "tracing",
+]
+
+[[package]]
+name = "libp2p-gadget"
+version = "0.34.0"
+dependencies = [
+ "array-bytes 6.2.2",
+ "assert_matches",
+ "async-channel 1.9.0",
+ "async-trait",
+ "asynchronous-codec 0.6.2",
+ "auto_impl",
+ "bytes",
+ "either",
+ "fnv",
+ "futures",
+ "futures-timer",
+ "ip_network",
+ "libp2p 0.51.4",
+ "linked_hash_set",
+ "log",
+ "mockall",
+ "multistream-select 0.12.1",
+ "parity-scale-codec 3.6.9",
+ "parking_lot 0.12.1",
+ "partial_sort",
+ "pin-project",
+ "rand 0.8.5",
+ "sc-client-api",
+ "sc-network-common 0.33.0",
+ "sc-network-light 0.33.0",
+ "sc-network-sync 0.33.0",
+ "sc-utils",
+ "serde",
+ "serde_json",
+ "smallvec",
+ "sp-arithmetic",
+ "sp-blockchain",
+ "sp-core",
+ "sp-runtime",
+ "sp-test-primitives",
+ "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "substrate-prometheus-endpoint",
+ "substrate-test-runtime",
+ "substrate-test-runtime-client",
+ "tempfile",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tokio-test",
+ "tokio-util",
+ "unsigned-varint 0.7.2",
+ "wasm-timer",
+ "zeroize",
+]
+
+[[package]]
+name = "libp2p-gossipsub"
+version = "0.46.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
+ "base64 0.21.7",
+ "byteorder",
+ "bytes",
+ "either",
+ "fnv",
+ "futures",
+ "futures-ticker",
+ "getrandom 0.2.12",
+ "hex_fmt",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "prometheus-client 0.22.2",
+ "quick-protobuf",
+ "quick-protobuf-codec 0.3.1",
+ "rand 0.8.5",
+ "regex",
+ "sha2 0.10.8",
+ "smallvec",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-identify"
 version = "0.42.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5455f472243e63b9c497ff320ded0314254a9eb751799a39c283c6f20b793f3c"
 dependencies = [
- "asynchronous-codec",
+ "asynchronous-codec 0.6.2",
  "either",
  "futures",
  "futures-timer",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
  "log",
  "lru 0.10.1",
  "quick-protobuf",
- "quick-protobuf-codec",
+ "quick-protobuf-codec 0.1.0",
  "smallvec",
  "thiserror",
  "void",
 ]
 
+[[package]]
+name = "libp2p-identify"
+version = "0.44.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
+ "either",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "lru 0.12.3",
+ "quick-protobuf",
+ "quick-protobuf-codec 0.3.1",
+ "smallvec",
+ "thiserror",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-identity"
 version = "0.1.3"
@@ -5292,12 +5880,30 @@ dependencies = [
  "bs58 0.4.0",
  "ed25519-dalek",
  "log",
- "multiaddr",
- "multihash",
+ "multiaddr 0.17.1",
+ "multihash 0.17.0",
+ "quick-protobuf",
+ "rand 0.8.5",
+ "sha2 0.10.8",
+ "thiserror",
+ "zeroize",
+]
+
+[[package]]
+name = "libp2p-identity"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "999ec70441b2fb35355076726a6bc466c932e9bdc66f6a11c6c0aa17c7ab9be0"
+dependencies = [
+ "bs58 0.5.0",
+ "ed25519-dalek",
+ "hkdf",
+ "multihash 0.19.1",
  "quick-protobuf",
  "rand 0.8.5",
  "sha2 0.10.8",
  "thiserror",
+ "tracing",
  "zeroize",
 ]
 
@@ -5308,16 +5914,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "39d5ef876a2b2323d63c258e63c2f8e36f205fe5a11f0b3095d59635650790ff"
 dependencies = [
  "arrayvec 0.7.4",
- "asynchronous-codec",
+ "asynchronous-codec 0.6.2",
  "bytes",
  "either",
  "fnv",
  "futures",
  "futures-timer",
  "instant",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
  "log",
  "quick-protobuf",
  "rand 0.8.5",
@@ -5325,7 +5931,35 @@ dependencies = [
  "smallvec",
  "thiserror",
  "uint",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
+ "void",
+]
+
+[[package]]
+name = "libp2p-kad"
+version = "0.45.4"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "arrayvec 0.7.4",
+ "asynchronous-codec 0.7.0",
+ "bytes",
+ "either",
+ "fnv",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "quick-protobuf",
+ "quick-protobuf-codec 0.3.1",
+ "rand 0.8.5",
+ "sha2 0.10.8",
+ "smallvec",
+ "thiserror",
+ "tracing",
+ "uint",
  "void",
 ]
 
@@ -5338,9 +5972,9 @@ dependencies = [
  "data-encoding",
  "futures",
  "if-watch",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
  "log",
  "rand 0.8.5",
  "smallvec",
@@ -5350,18 +5984,58 @@ dependencies = [
  "void",
 ]
 
+[[package]]
+name = "libp2p-mdns"
+version = "0.45.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "data-encoding",
+ "futures",
+ "hickory-proto",
+ "if-watch",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "rand 0.8.5",
+ "smallvec",
+ "socket2 0.5.6",
+ "tokio",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-metrics"
 version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a42ec91e227d7d0dafa4ce88b333cdf5f277253873ab087555c92798db2ddd46"
 dependencies = [
- "libp2p-core",
- "libp2p-identify",
- "libp2p-kad",
- "libp2p-ping",
- "libp2p-swarm",
- "prometheus-client",
+ "libp2p-core 0.39.2",
+ "libp2p-identify 0.42.2",
+ "libp2p-kad 0.43.3",
+ "libp2p-ping 0.42.0",
+ "libp2p-swarm 0.42.2",
+ "prometheus-client 0.19.0",
+]
+
+[[package]]
+name = "libp2p-metrics"
+version = "0.14.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "futures",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-dcutr",
+ "libp2p-gossipsub",
+ "libp2p-identify 0.44.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-kad 0.45.4",
+ "libp2p-ping 0.44.0",
+ "libp2p-relay",
+ "libp2p-swarm 0.44.2",
+ "pin-project",
+ "prometheus-client 0.22.2",
 ]
 
 [[package]]
@@ -5373,8 +6047,8 @@ dependencies = [
  "bytes",
  "curve25519-dalek 3.2.0",
  "futures",
- "libp2p-core",
- "libp2p-identity",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
  "log",
  "once_cell",
  "quick-protobuf",
@@ -5387,6 +6061,31 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "libp2p-noise"
+version = "0.44.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
+ "bytes",
+ "curve25519-dalek 4.1.2",
+ "futures",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "multiaddr 0.18.1",
+ "multihash 0.19.1",
+ "once_cell",
+ "quick-protobuf",
+ "rand 0.8.5",
+ "sha2 0.10.8",
+ "snow",
+ "static_assertions",
+ "thiserror",
+ "tracing",
+ "x25519-dalek 2.0.1",
+ "zeroize",
+]
+
 [[package]]
 name = "libp2p-ping"
 version = "0.42.0"
@@ -5397,13 +6096,30 @@ dependencies = [
  "futures",
  "futures-timer",
  "instant",
- "libp2p-core",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-swarm 0.42.2",
  "log",
  "rand 0.8.5",
  "void",
 ]
 
+[[package]]
+name = "libp2p-ping"
+version = "0.44.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "either",
+ "futures",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "rand 0.8.5",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-quic"
 version = "0.7.0-alpha.3"
@@ -5414,18 +6130,65 @@ dependencies = [
  "futures",
  "futures-timer",
  "if-watch",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-tls",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-tls 0.1.0",
  "log",
  "parking_lot 0.12.1",
- "quinn-proto",
+ "quinn-proto 0.9.6",
  "rand 0.8.5",
  "rustls 0.20.9",
  "thiserror",
  "tokio",
 ]
 
+[[package]]
+name = "libp2p-quic"
+version = "0.10.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "bytes",
+ "futures",
+ "futures-timer",
+ "if-watch",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-tls 0.3.0",
+ "parking_lot 0.12.1",
+ "quinn",
+ "rand 0.8.5",
+ "ring 0.16.20",
+ "rustls 0.21.10",
+ "socket2 0.5.6",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "libp2p-relay"
+version = "0.17.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
+ "bytes",
+ "either",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
+ "quick-protobuf",
+ "quick-protobuf-codec 0.3.1",
+ "rand 0.8.5",
+ "static_assertions",
+ "thiserror",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-request-response"
 version = "0.24.1"
@@ -5435,11 +6198,32 @@ dependencies = [
  "async-trait",
  "futures",
  "instant",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm 0.42.2",
+ "rand 0.8.5",
+ "smallvec",
+]
+
+[[package]]
+name = "libp2p-request-response"
+version = "0.26.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "async-trait",
+ "cbor4ii",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm 0.44.2",
  "rand 0.8.5",
+ "serde",
  "smallvec",
+ "tracing",
+ "void",
 ]
 
 [[package]]
@@ -5453,9 +6237,9 @@ dependencies = [
  "futures",
  "futures-timer",
  "instant",
- "libp2p-core",
- "libp2p-identity",
- "libp2p-swarm-derive",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
+ "libp2p-swarm-derive 0.32.0",
  "log",
  "rand 0.8.5",
  "smallvec",
@@ -5463,6 +6247,29 @@ dependencies = [
  "void",
 ]
 
+[[package]]
+name = "libp2p-swarm"
+version = "0.44.2"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "either",
+ "fnv",
+ "futures",
+ "futures-timer",
+ "instant",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "libp2p-swarm-derive 0.34.3",
+ "lru 0.12.3",
+ "multistream-select 0.13.0",
+ "once_cell",
+ "rand 0.8.5",
+ "smallvec",
+ "tokio",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-swarm-derive"
 version = "0.32.0"
@@ -5474,6 +6281,17 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "libp2p-swarm-derive"
+version = "0.34.3"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
 [[package]]
 name = "libp2p-tcp"
 version = "0.39.0"
@@ -5484,12 +6302,28 @@ dependencies = [
  "futures-timer",
  "if-watch",
  "libc",
- "libp2p-core",
+ "libp2p-core 0.39.2",
  "log",
  "socket2 0.4.10",
  "tokio",
 ]
 
+[[package]]
+name = "libp2p-tcp"
+version = "0.41.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "futures",
+ "futures-timer",
+ "if-watch",
+ "libc",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "socket2 0.5.6",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "libp2p-tls"
 version = "0.1.0"
@@ -5497,18 +6331,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff08d13d0dc66e5e9ba6279c1de417b84fa0d0adc3b03e5732928c180ec02781"
 dependencies = [
  "futures",
- "futures-rustls",
- "libp2p-core",
- "libp2p-identity",
+ "futures-rustls 0.22.2",
+ "libp2p-core 0.39.2",
+ "libp2p-identity 0.1.3",
  "rcgen 0.10.0",
  "ring 0.16.20",
  "rustls 0.20.9",
  "thiserror",
  "webpki",
- "x509-parser",
+ "x509-parser 0.14.0",
+ "yasna",
+]
+
+[[package]]
+name = "libp2p-tls"
+version = "0.3.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "futures",
+ "futures-rustls 0.24.0",
+ "libp2p-core 0.41.2",
+ "libp2p-identity 0.2.8",
+ "rcgen 0.11.3",
+ "ring 0.16.20",
+ "rustls 0.21.10",
+ "rustls-webpki 0.101.7",
+ "thiserror",
+ "x509-parser 0.15.1",
  "yasna",
 ]
 
+[[package]]
+name = "libp2p-upnp"
+version = "0.2.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "futures",
+ "futures-timer",
+ "igd-next",
+ "libp2p-core 0.41.2",
+ "libp2p-swarm 0.44.2",
+ "tokio",
+ "tracing",
+ "void",
+]
+
 [[package]]
 name = "libp2p-wasm-ext"
 version = "0.39.0"
@@ -5517,7 +6384,7 @@ checksum = "77dff9d32353a5887adb86c8afc1de1a94d9e8c3bc6df8b2201d7cdf5c848f43"
 dependencies = [
  "futures",
  "js-sys",
- "libp2p-core",
+ "libp2p-core 0.39.2",
  "parity-send-wrapper",
  "wasm-bindgen",
  "wasm-bindgen-futures",
@@ -5531,12 +6398,12 @@ checksum = "111273f7b3d3510524c752e8b7a5314b7f7a1fee7e68161c01a7d72cbb06db9f"
 dependencies = [
  "either",
  "futures",
- "futures-rustls",
- "libp2p-core",
+ "futures-rustls 0.22.2",
+ "libp2p-core 0.39.2",
  "log",
  "parking_lot 0.12.1",
  "quicksink",
- "rw-stream-sink",
+ "rw-stream-sink 0.3.0",
  "soketto",
  "url",
  "webpki-roots 0.22.6",
@@ -5549,10 +6416,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4dcd21d950662700a385d4c6d68e2f5f54d778e97068cdd718522222ef513bda"
 dependencies = [
  "futures",
- "libp2p-core",
+ "libp2p-core 0.39.2",
  "log",
  "thiserror",
- "yamux",
+ "yamux 0.10.2",
+]
+
+[[package]]
+name = "libp2p-yamux"
+version = "0.45.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "either",
+ "futures",
+ "libp2p-core 0.41.2",
+ "thiserror",
+ "tracing",
+ "yamux 0.12.1",
+ "yamux 0.13.1",
 ]
 
 [[package]]
@@ -5651,6 +6532,15 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "link-cplusplus"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "linked-hash-map"
 version = "0.5.6"
@@ -6127,11 +7017,30 @@ dependencies = [
  "data-encoding",
  "log",
  "multibase",
- "multihash",
+ "multihash 0.17.0",
  "percent-encoding",
  "serde",
  "static_assertions",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
+ "url",
+]
+
+[[package]]
+name = "multiaddr"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b852bc02a2da5feed68cd14fa50d0774b92790a5bdbfa932a813926c8472070"
+dependencies = [
+ "arrayref",
+ "byteorder",
+ "data-encoding",
+ "libp2p-identity 0.2.8",
+ "multibase",
+ "multihash 0.19.1",
+ "percent-encoding",
+ "serde",
+ "static_assertions",
+ "unsigned-varint 0.7.2",
  "url",
 ]
 
@@ -6160,7 +7069,17 @@ dependencies = [
  "multihash-derive",
  "sha2 0.10.8",
  "sha3 0.10.8",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
+]
+
+[[package]]
+name = "multihash"
+version = "0.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "076d548d76a0e2a0d4ab471d0b1c36c577786dfc4471242035d97a12a735c492"
+dependencies = [
+ "core2",
+ "unsigned-varint 0.7.2",
 ]
 
 [[package]]
@@ -6191,10 +7110,23 @@ checksum = "c8552ab875c1313b97b8d20cb857b9fd63e2d1d6a0a1b53ce9821e575405f27a"
 dependencies = [
  "bytes",
  "futures",
- "log",
+ "log",
+ "pin-project",
+ "smallvec",
+ "unsigned-varint 0.7.2",
+]
+
+[[package]]
+name = "multistream-select"
+version = "0.13.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "bytes",
+ "futures",
  "pin-project",
  "smallvec",
- "unsigned-varint",
+ "tracing",
+ "unsigned-varint 0.8.0",
 ]
 
 [[package]]
@@ -6681,7 +7613,7 @@ dependencies = [
  "rand_core 0.6.4",
  "rug",
  "serde",
- "serde_with 3.7.0",
+ "serde_with 3.6.1",
  "thiserror",
 ]
 
@@ -6709,6 +7641,44 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "pallet-authorship"
+version = "28.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "frame-support",
+ "frame-system",
+ "impl-trait-for-tuples",
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "sp-runtime",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+]
+
+[[package]]
+name = "pallet-babe"
+version = "28.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "log",
+ "pallet-authorship",
+ "pallet-session",
+ "pallet-timestamp",
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "sp-application-crypto",
+ "sp-consensus-babe",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-session",
+ "sp-staking",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+]
+
 [[package]]
 name = "pallet-balances"
 version = "28.0.0"
@@ -6728,7 +7698,7 @@ dependencies = [
 [[package]]
 name = "pallet-dkg"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "digest 0.10.7",
  "ecdsa 0.16.9",
@@ -6742,6 +7712,7 @@ dependencies = [
  "frost-p384 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git)",
  "frost-ristretto255 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git)",
  "frost-secp256k1 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git)",
+ "generic-array 0.14.7",
  "generic-ec",
  "generic-ec-zkp",
  "hex",
@@ -6756,7 +7727,7 @@ dependencies = [
  "rand_core 0.6.4",
  "scale-info",
  "serde",
- "serde_with 3.7.0",
+ "serde_with 3.6.1",
  "serdect",
  "sha2 0.10.8",
  "snowbridge-milagro-bls",
@@ -6774,7 +7745,7 @@ dependencies = [
 [[package]]
 name = "pallet-jobs"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
@@ -6791,7 +7762,7 @@ dependencies = [
 [[package]]
 name = "pallet-jobs-rpc-runtime-api"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "parity-scale-codec 3.6.9",
  "sp-api",
@@ -6800,6 +7771,28 @@ dependencies = [
  "tangle-primitives",
 ]
 
+[[package]]
+name = "pallet-session"
+version = "28.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "frame-support",
+ "frame-system",
+ "impl-trait-for-tuples",
+ "log",
+ "pallet-timestamp",
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-session",
+ "sp-staking",
+ "sp-state-machine",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-trie",
+]
+
 [[package]]
 name = "pallet-timestamp"
 version = "27.0.0"
@@ -6823,7 +7816,7 @@ dependencies = [
 [[package]]
 name = "pallet-zksaas"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "frame-benchmarking",
  "frame-support",
@@ -7199,24 +8192,24 @@ dependencies = [
 
 [[package]]
 name = "polkavm-common"
-version = "0.9.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d9428a5cfcc85c5d7b9fc4b6a18c4b802d0173d768182a51cc7751640f08b92"
+checksum = "92c99f7eee94e7be43ba37eef65ad0ee8cbaf89b7c00001c3f6d2be985cb1817"
 
 [[package]]
 name = "polkavm-derive"
-version = "0.9.1"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae8c4bea6f3e11cd89bb18bcdddac10bd9a24015399bd1c485ad68a985a19606"
+checksum = "79fa916f7962348bd1bb1a65a83401675e6fc86c51a0fdbcf92a3108e58e6125"
 dependencies = [
  "polkavm-derive-impl-macro",
 ]
 
 [[package]]
 name = "polkavm-derive-impl"
-version = "0.9.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c4fdfc49717fb9a196e74a5d28e0bc764eb394a2c803eb11133a31ac996c60c"
+checksum = "c10b2654a8a10a83c260bfb93e97b262cf0017494ab94a65d389e0eda6de6c9c"
 dependencies = [
  "polkavm-common",
  "proc-macro2",
@@ -7226,9 +8219,9 @@ dependencies = [
 
 [[package]]
 name = "polkavm-derive-impl-macro"
-version = "0.9.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429"
+checksum = "15e85319a0d5129dc9f021c62607e0804f5fb777a05cdda44d750ac0732def66"
 dependencies = [
  "polkavm-derive-impl",
  "syn 2.0.52",
@@ -7462,9 +8455,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.79"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 dependencies = [
  "unicode-ident",
 ]
@@ -7495,6 +8488,18 @@ dependencies = [
  "prometheus-client-derive-encode",
 ]
 
+[[package]]
+name = "prometheus-client"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ca959da22a332509f2a73ae9e5f23f9dcfc31fd3a54d71f159495bd5909baa"
+dependencies = [
+ "dtoa",
+ "itoa",
+ "parking_lot 0.12.1",
+ "prometheus-client-derive-encode",
+]
+
 [[package]]
 name = "prometheus-client-derive-encode"
 version = "0.4.2"
@@ -7658,11 +8663,32 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1693116345026436eb2f10b677806169c1a1260c1c60eaaffe3fb5a29ae23d8b"
 dependencies = [
- "asynchronous-codec",
+ "asynchronous-codec 0.6.2",
+ "bytes",
+ "quick-protobuf",
+ "thiserror",
+ "unsigned-varint 0.7.2",
+]
+
+[[package]]
+name = "quick-protobuf-codec"
+version = "0.3.1"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "asynchronous-codec 0.7.0",
  "bytes",
  "quick-protobuf",
  "thiserror",
- "unsigned-varint",
+ "unsigned-varint 0.8.0",
+]
+
+[[package]]
+name = "quickcheck"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
+dependencies = [
+ "rand 0.8.5",
 ]
 
 [[package]]
@@ -7676,6 +8702,24 @@ dependencies = [
  "pin-project-lite 0.1.12",
 ]
 
+[[package]]
+name = "quinn"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75"
+dependencies = [
+ "bytes",
+ "futures-io",
+ "pin-project-lite 0.2.13",
+ "quinn-proto 0.10.6",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls 0.21.10",
+ "thiserror",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "quinn-proto"
 version = "0.9.6"
@@ -7694,6 +8738,36 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "quinn-proto"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a"
+dependencies = [
+ "bytes",
+ "rand 0.8.5",
+ "ring 0.16.20",
+ "rustc-hash",
+ "rustls 0.21.10",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7"
+dependencies = [
+ "bytes",
+ "libc",
+ "socket2 0.5.6",
+ "tracing",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.35"
@@ -8423,7 +9497,7 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver",
+ "semver 1.0.22",
 ]
 
 [[package]]
@@ -8599,6 +9673,16 @@ dependencies = [
  "static_assertions",
 ]
 
+[[package]]
+name = "rw-stream-sink"
+version = "0.4.0"
+source = "git+https://github.com/libp2p/rust-libp2p/?rev=3644879956b6ab93b8d23553a33e8fdb838f576f#3644879956b6ab93b8d23553a33e8fdb838f576f"
+dependencies = [
+ "futures",
+ "pin-project",
+ "static_assertions",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.17"
@@ -8747,7 +9831,7 @@ dependencies = [
  "async-trait",
  "futures",
  "futures-timer",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "log",
  "mockall",
  "parking_lot 0.12.1",
@@ -8827,8 +9911,8 @@ dependencies = [
  "log",
  "sc-client-api",
  "sc-network",
- "sc-network-common",
- "sc-network-sync",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sc-network-sync 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
  "sp-blockchain",
  "sp-runtime",
 ]
@@ -8858,10 +9942,10 @@ dependencies = [
  "bytes",
  "futures",
  "futures-timer",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "log",
  "mixnet",
- "multiaddr",
+ "multiaddr 0.17.1",
  "parity-scale-codec 3.6.9",
  "parking_lot 0.12.1",
  "sc-client-api",
@@ -8884,14 +9968,14 @@ dependencies = [
  "array-bytes 6.2.2",
  "async-channel 1.9.0",
  "async-trait",
- "asynchronous-codec",
+ "asynchronous-codec 0.6.2",
  "bytes",
  "either",
  "fnv",
  "futures",
  "futures-timer",
  "ip_network",
- "libp2p",
+ "libp2p 0.51.4",
  "linked_hash_set",
  "log",
  "mockall",
@@ -8901,7 +9985,7 @@ dependencies = [
  "pin-project",
  "rand 0.8.5",
  "sc-client-api",
- "sc-network-common",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
  "sc-utils",
  "serde",
  "serde_json",
@@ -8914,7 +9998,7 @@ dependencies = [
  "thiserror",
  "tokio",
  "tokio-stream",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
  "wasm-timer",
  "zeroize",
 ]
@@ -8927,7 +10011,7 @@ dependencies = [
  "async-channel 1.9.0",
  "cid",
  "futures",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "log",
  "prost 0.12.3",
  "prost-build",
@@ -8936,7 +10020,24 @@ dependencies = [
  "sp-blockchain",
  "sp-runtime",
  "thiserror",
- "unsigned-varint",
+ "unsigned-varint 0.7.2",
+]
+
+[[package]]
+name = "sc-network-common"
+version = "0.33.0"
+dependencies = [
+ "async-trait",
+ "bitflags 1.3.2",
+ "futures",
+ "libp2p-identity 0.1.3",
+ "parity-scale-codec 3.6.9",
+ "prost-build",
+ "sc-consensus",
+ "sp-consensus",
+ "sp-consensus-grandpa",
+ "sp-runtime",
+ "tempfile",
 ]
 
 [[package]]
@@ -8947,7 +10048,7 @@ dependencies = [
  "async-trait",
  "bitflags 1.3.2",
  "futures",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "parity-scale-codec 3.6.9",
  "prost-build",
  "sc-consensus",
@@ -8956,6 +10057,26 @@ dependencies = [
  "sp-runtime",
 ]
 
+[[package]]
+name = "sc-network-light"
+version = "0.33.0"
+dependencies = [
+ "array-bytes 6.2.2",
+ "async-channel 1.9.0",
+ "futures",
+ "libp2p-gadget",
+ "libp2p-identity 0.1.3",
+ "log",
+ "parity-scale-codec 3.6.9",
+ "prost 0.12.3",
+ "prost-build",
+ "sc-client-api",
+ "sp-blockchain",
+ "sp-core",
+ "sp-runtime",
+ "thiserror",
+]
+
 [[package]]
 name = "sc-network-light"
 version = "0.33.0"
@@ -8964,7 +10085,7 @@ dependencies = [
  "array-bytes 6.2.2",
  "async-channel 1.9.0",
  "futures",
- "libp2p-identity",
+ "libp2p-identity 0.1.3",
  "log",
  "parity-scale-codec 3.6.9",
  "prost 0.12.3",
@@ -8977,6 +10098,46 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "sc-network-sync"
+version = "0.33.0"
+dependencies = [
+ "array-bytes 6.2.2",
+ "async-channel 1.9.0",
+ "async-trait",
+ "fork-tree",
+ "futures",
+ "futures-timer",
+ "libp2p 0.51.4",
+ "libp2p-gadget",
+ "log",
+ "mockall",
+ "parity-scale-codec 3.6.9",
+ "prost 0.12.3",
+ "prost-build",
+ "quickcheck",
+ "sc-block-builder",
+ "sc-client-api",
+ "sc-consensus",
+ "sc-network-common 0.33.0",
+ "sc-utils",
+ "schnellru",
+ "smallvec",
+ "sp-arithmetic",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-consensus-grandpa",
+ "sp-core",
+ "sp-runtime",
+ "sp-test-primitives",
+ "sp-tracing 16.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "substrate-prometheus-endpoint",
+ "substrate-test-runtime-client",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+]
+
 [[package]]
 name = "sc-network-sync"
 version = "0.33.0"
@@ -8988,7 +10149,7 @@ dependencies = [
  "fork-tree",
  "futures",
  "futures-timer",
- "libp2p",
+ "libp2p 0.51.4",
  "log",
  "mockall",
  "parity-scale-codec 3.6.9",
@@ -8997,7 +10158,7 @@ dependencies = [
  "sc-client-api",
  "sc-consensus",
  "sc-network",
- "sc-network-common",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
  "sc-utils",
  "schnellru",
  "smallvec",
@@ -9020,18 +10181,52 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot
 dependencies = [
  "array-bytes 6.2.2",
  "futures",
- "libp2p",
+ "libp2p 0.51.4",
  "log",
  "parity-scale-codec 3.6.9",
  "sc-network",
- "sc-network-common",
- "sc-network-sync",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sc-network-sync 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
  "sc-utils",
  "sp-consensus",
  "sp-runtime",
  "substrate-prometheus-endpoint",
 ]
 
+[[package]]
+name = "sc-offchain"
+version = "29.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "array-bytes 6.2.2",
+ "bytes",
+ "fnv",
+ "futures",
+ "futures-timer",
+ "hyper",
+ "hyper-rustls",
+ "libp2p 0.51.4",
+ "log",
+ "num_cpus",
+ "once_cell",
+ "parity-scale-codec 3.6.9",
+ "parking_lot 0.12.1",
+ "rand 0.8.5",
+ "sc-client-api",
+ "sc-network",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sc-transaction-pool-api",
+ "sc-utils",
+ "sp-api",
+ "sp-core",
+ "sp-externalities 0.25.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-keystore",
+ "sp-offchain",
+ "sp-runtime",
+ "threadpool",
+ "tracing",
+]
+
 [[package]]
 name = "sc-rpc"
 version = "29.0.0"
@@ -9154,9 +10349,9 @@ dependencies = [
  "sc-keystore",
  "sc-network",
  "sc-network-bitswap",
- "sc-network-common",
- "sc-network-light",
- "sc-network-sync",
+ "sc-network-common 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sc-network-light 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sc-network-sync 0.33.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
  "sc-network-transactions",
  "sc-rpc",
  "sc-rpc-server",
@@ -9231,7 +10426,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot
 dependencies = [
  "chrono",
  "futures",
- "libp2p",
+ "libp2p 0.51.4",
  "log",
  "parking_lot 0.12.1",
  "pin-project",
@@ -9411,9 +10606,9 @@ dependencies = [
 
 [[package]]
 name = "scale-info"
-version = "2.11.0"
+version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ef2175c2907e7c8bc0a9c3f86aeb5ec1f3b275300ad58a44d0c3ae379a5e52e"
+checksum = "7f7d66a1128282b7ef025a8ead62a4a9fcf017382ec53b8ffbf4d7bf77bd3c60"
 dependencies = [
  "bitvec 1.0.1",
  "cfg-if",
@@ -9425,9 +10620,9 @@ dependencies = [
 
 [[package]]
 name = "scale-info-derive"
-version = "2.11.0"
+version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b8eb8fd61c5cdd3390d9b2132300a7e7618955b98b8416f118c1b4e144f"
+checksum = "abf2c68b89cafb3b8d918dd07b42be0da66ff202cf1155c5739a4e0c1ea0dc19"
 dependencies = [
  "proc-macro-crate 1.1.3",
  "proc-macro2",
@@ -9513,6 +10708,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "scratch"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
+
 [[package]]
 name = "sct"
 version = "0.7.1"
@@ -9652,10 +10853,19 @@ dependencies = [
 name = "security-framework-sys"
 version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a3186ec9e65071a2095434b1f5bb24838d4e8e130f584c790f6033c79943537"
 dependencies = [
- "core-foundation-sys",
- "libc",
+ "semver-parser",
 ]
 
 [[package]]
@@ -9663,6 +10873,15 @@ name = "semver"
 version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
@@ -9759,13 +10978,13 @@ dependencies = [
 
 [[package]]
 name = "serde_with"
-version = "3.7.0"
+version = "3.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a"
+checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
 dependencies = [
  "serde",
  "serde_derive",
- "serde_with_macros 3.7.0",
+ "serde_with_macros 3.6.1",
 ]
 
 [[package]]
@@ -9782,9 +11001,9 @@ dependencies = [
 
 [[package]]
 name = "serde_with_macros"
-version = "3.7.0"
+version = "3.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655"
+checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
 dependencies = [
  "darling 0.20.8",
  "proc-macro2",
@@ -10411,6 +11630,23 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "sp-consensus-aura"
+version = "0.32.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "async-trait",
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "sp-api",
+ "sp-application-crypto",
+ "sp-consensus-slots",
+ "sp-inherents",
+ "sp-runtime",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-timestamp",
+]
+
 [[package]]
 name = "sp-consensus-babe"
 version = "0.32.0"
@@ -10523,7 +11759,7 @@ dependencies = [
 [[package]]
 name = "sp-crypto-ec-utils"
 version = "0.10.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "ark-bls12-377",
  "ark-bls12-377-ext",
@@ -10586,7 +11822,7 @@ dependencies = [
 [[package]]
 name = "sp-debug-derive"
 version = "14.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -10607,7 +11843,7 @@ dependencies = [
 [[package]]
 name = "sp-externalities"
 version = "0.25.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "environmental",
  "parity-scale-codec 3.6.9",
@@ -10665,6 +11901,16 @@ dependencies = [
  "tracing-core",
 ]
 
+[[package]]
+name = "sp-keyring"
+version = "31.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "sp-core",
+ "sp-runtime",
+ "strum 0.24.1",
+]
+
 [[package]]
 name = "sp-keystore"
 version = "0.34.0"
@@ -10784,7 +12030,7 @@ dependencies = [
 [[package]]
 name = "sp-runtime-interface"
 version = "24.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "bytes",
  "impl-trait-for-tuples",
@@ -10816,7 +12062,7 @@ dependencies = [
 [[package]]
 name = "sp-runtime-interface-proc-macro"
 version = "17.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "Inflector",
  "expander",
@@ -10909,7 +12155,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot
 [[package]]
 name = "sp-std"
 version = "14.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 
 [[package]]
 name = "sp-storage"
@@ -10927,7 +12173,7 @@ dependencies = [
 [[package]]
 name = "sp-storage"
 version = "19.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "impl-serde",
  "parity-scale-codec 3.6.9",
@@ -10937,6 +12183,20 @@ dependencies = [
  "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk)",
 ]
 
+[[package]]
+name = "sp-test-primitives"
+version = "2.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "parity-scale-codec 3.6.9",
+ "scale-info",
+ "serde",
+ "sp-application-crypto",
+ "sp-core",
+ "sp-runtime",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+]
+
 [[package]]
 name = "sp-timestamp"
 version = "26.0.0"
@@ -10965,7 +12225,7 @@ dependencies = [
 [[package]]
 name = "sp-tracing"
 version = "16.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "parity-scale-codec 3.6.9",
  "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk)",
@@ -11066,7 +12326,7 @@ dependencies = [
 [[package]]
 name = "sp-wasm-interface"
 version = "20.0.0"
-source = "git+https://github.com/paritytech/polkadot-sdk#256546b7b9033ea99eb22b92c1d4cdec64c56516"
+source = "git+https://github.com/paritytech/polkadot-sdk#a0c9a3d65adb63d83afdf5b29d2e555dea0fa35e"
 dependencies = [
  "anyhow",
  "impl-trait-for-tuples",
@@ -11509,6 +12769,15 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "strum"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+dependencies = [
+ "strum_macros 0.24.3",
+]
+
 [[package]]
 name = "strum"
 version = "0.25.0"
@@ -11520,11 +12789,24 @@ dependencies = [
 
 [[package]]
 name = "strum"
-version = "0.26.2"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
+checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
 dependencies = [
- "strum_macros 0.26.2",
+ "strum_macros 0.26.1",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -11542,9 +12824,9 @@ dependencies = [
 
 [[package]]
 name = "strum_macros"
-version = "0.26.2"
+version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
+checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
 dependencies = [
  "heck 0.4.1",
  "proc-macro2",
@@ -11593,6 +12875,111 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "substrate-test-client"
+version = "2.0.1"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "array-bytes 6.2.2",
+ "async-trait",
+ "futures",
+ "parity-scale-codec 3.6.9",
+ "sc-client-api",
+ "sc-client-db",
+ "sc-consensus",
+ "sc-executor",
+ "sc-offchain",
+ "sc-service",
+ "serde",
+ "serde_json",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-core",
+ "sp-keyring",
+ "sp-keystore",
+ "sp-runtime",
+ "sp-state-machine",
+ "tokio",
+]
+
+[[package]]
+name = "substrate-test-runtime"
+version = "2.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "array-bytes 6.2.2",
+ "frame-executive",
+ "frame-support",
+ "frame-system",
+ "frame-system-rpc-runtime-api",
+ "log",
+ "pallet-babe",
+ "pallet-balances",
+ "pallet-timestamp",
+ "parity-scale-codec 3.6.9",
+ "sc-service",
+ "scale-info",
+ "sp-api",
+ "sp-application-crypto",
+ "sp-block-builder",
+ "sp-consensus-aura",
+ "sp-consensus-babe",
+ "sp-consensus-grandpa",
+ "sp-core",
+ "sp-crypto-hashing",
+ "sp-externalities 0.25.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-genesis-builder",
+ "sp-inherents",
+ "sp-io",
+ "sp-keyring",
+ "sp-offchain",
+ "sp-runtime",
+ "sp-session",
+ "sp-state-machine",
+ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0)",
+ "sp-transaction-pool",
+ "sp-trie",
+ "sp-version",
+ "substrate-wasm-builder",
+ "trie-db",
+]
+
+[[package]]
+name = "substrate-test-runtime-client"
+version = "2.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "futures",
+ "sc-block-builder",
+ "sc-client-api",
+ "sc-consensus",
+ "sp-api",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-core",
+ "sp-runtime",
+ "substrate-test-client",
+ "substrate-test-runtime",
+]
+
+[[package]]
+name = "substrate-wasm-builder"
+version = "17.0.0"
+source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.7.0#162c6796bac740802fe11c9e39f3094677d1f08f"
+dependencies = [
+ "build-helper",
+ "cargo_metadata",
+ "console",
+ "filetime",
+ "parity-wasm",
+ "sp-maybe-compressed-blob",
+ "strum 0.24.1",
+ "tempfile",
+ "toml 0.8.11",
+ "walkdir",
+ "wasm-opt",
+]
+
 [[package]]
 name = "subtle"
 version = "1.0.0"
@@ -11786,7 +13173,7 @@ dependencies = [
 [[package]]
 name = "tangle-crypto-primitives"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "parity-scale-codec 3.6.9",
  "scale-info",
@@ -11796,7 +13183,7 @@ dependencies = [
 [[package]]
 name = "tangle-primitives"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "ark-bn254",
  "ark-crypto-primitives",
@@ -11824,7 +13211,7 @@ dependencies = [
 [[package]]
 name = "tangle-subxt"
 version = "0.6.3"
-source = "git+https://github.com/webb-tools/tangle.git#768aa634ae267177bacbee1344490101b094f8db"
+source = "git+https://github.com/webb-tools/tangle.git#1c49ca065ac1aec892a7b1f108be92b0a2b56b4a"
 dependencies = [
  "parity-scale-codec 3.6.9",
  "scale-info",
@@ -11943,18 +13330,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.53"
+version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2cd5904763bad08ad5513ddbb12cf2ae273ca53fa9f68e843e236ec6dfccc09"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.53"
+version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3dcf4a824cce0aeacd6f38ae6f24234c8e80d68632338ebaa1443b5df9e29e19"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -11991,6 +13378,15 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
 [[package]]
 name = "threshold-bls-protocol"
 version = "0.1.0"
@@ -12103,6 +13499,7 @@ dependencies = [
  "num_cpus",
  "parking_lot 0.12.1",
  "pin-project-lite 0.2.13",
+ "signal-hook-registry",
  "socket2 0.5.6",
  "tokio-macros",
  "windows-sys 0.48.0",
@@ -12152,6 +13549,19 @@ dependencies = [
  "tokio-util",
 ]
 
+[[package]]
+name = "tokio-test"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89b3cbabd3ae862100094ae433e1def582cf86451b4e9bf83aa7ac1d8a7d719"
+dependencies = [
+ "async-stream",
+ "bytes",
+ "futures-core",
+ "tokio",
+ "tokio-stream",
+]
+
 [[package]]
 name = "tokio-util"
 version = "0.7.10"
@@ -12434,7 +13844,7 @@ dependencies = [
  "async-trait",
  "cfg-if",
  "data-encoding",
- "enum-as-inner",
+ "enum-as-inner 0.5.1",
  "futures-channel",
  "futures-io",
  "futures-util",
@@ -12610,12 +14020,18 @@ version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105"
 dependencies = [
- "asynchronous-codec",
+ "asynchronous-codec 0.6.2",
  "bytes",
  "futures-io",
  "futures-util",
 ]
 
+[[package]]
+name = "unsigned-varint"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
+
 [[package]]
 name = "untrusted"
 version = "0.7.1"
@@ -12637,6 +14053,7 @@ dependencies = [
  "form_urlencoded",
  "idna 0.5.0",
  "percent-encoding",
+ "serde",
 ]
 
 [[package]]
@@ -12853,6 +14270,46 @@ dependencies = [
  "parity-wasm",
 ]
 
+[[package]]
+name = "wasm-opt"
+version = "0.116.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc942673e7684671f0c5708fc18993569d184265fd5223bb51fc8e5b9b6cfd52"
+dependencies = [
+ "anyhow",
+ "libc",
+ "strum 0.24.1",
+ "strum_macros 0.24.3",
+ "tempfile",
+ "thiserror",
+ "wasm-opt-cxx-sys",
+ "wasm-opt-sys",
+]
+
+[[package]]
+name = "wasm-opt-cxx-sys"
+version = "0.116.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c57b28207aa724318fcec6575fe74803c23f6f266fce10cbc9f3f116762f12e"
+dependencies = [
+ "anyhow",
+ "cxx",
+ "cxx-build",
+ "wasm-opt-sys",
+]
+
+[[package]]
+name = "wasm-opt-sys"
+version = "0.116.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a1cce564dc768dacbdb718fc29df2dba80bd21cb47d8f77ae7e3d95ceb98cbe"
+dependencies = [
+ "anyhow",
+ "cc",
+ "cxx",
+ "cxx-build",
+]
+
 [[package]]
 name = "wasm-timer"
 version = "0.2.5"
@@ -13421,9 +14878,9 @@ dependencies = [
 
 [[package]]
 name = "whoami"
-version = "1.5.1"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9"
+checksum = "0fec781d48b41f8163426ed18e8fc2864c12937df9ce54c88ede7bd47270893e"
 dependencies = [
  "redox_syscall 0.4.1",
  "wasite",
@@ -13829,6 +15286,38 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "x509-parser"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "rusticata-macros",
+ "thiserror",
+ "time",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
+
+[[package]]
+name = "xmltree"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
+dependencies = [
+ "xml-rs",
+]
+
 [[package]]
 name = "yamux"
 version = "0.10.2"
@@ -13843,6 +15332,37 @@ dependencies = [
  "static_assertions",
 ]
 
+[[package]]
+name = "yamux"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776"
+dependencies = [
+ "futures",
+ "log",
+ "nohash-hasher",
+ "parking_lot 0.12.1",
+ "pin-project",
+ "rand 0.8.5",
+ "static_assertions",
+]
+
+[[package]]
+name = "yamux"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad1d0148b89300047e72994bee99ecdabd15a9166a7b70c8b8c37c314dcc9002"
+dependencies = [
+ "futures",
+ "instant",
+ "log",
+ "nohash-hasher",
+ "parking_lot 0.12.1",
+ "pin-project",
+ "rand 0.8.5",
+ "static_assertions",
+]
+
 [[package]]
 name = "yap"
 version = "0.11.0"
diff --git a/Cargo.toml b/Cargo.toml
index 4674b027a..c1aecfc9e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,30 +1,37 @@
 [workspace]
 resolver = "2"
 members = [
-    "gadget-core",
-    "gadget-common",
-    "test-gadget",
+    "core",
+    "common",
+    "shell",
     "protocols/dfns-cggmp21",
     "protocols/silent-shard-dkls23-ll",
     "protocols/zk-saas",
     "protocols/zcash-frost",
     "protocols/bls",
     "protocols/stub",
+    "test-gadget",
     "test-utils",
     "protocol-macros",
+    "libp2p",
+    "libp2p/sync",
+    "libp2p/common",
 ]
 
 [workspace.dependencies]
-gadget-core = { path = "./gadget-core" }
-gadget-common = { path = "./gadget-common" }
+gadget-core = { path = "./core" }
+gadget-common = { path = "./common" }
 zk-saas-protocol = { path = "./protocols/zk-saas" }
 threshold-bls-protocol = { path = "./protocols/bls" }
 stub-protocol = { path = "./protocols/stub" }
-test-utils = { path = "./test-utils" }
-protocol-macros = { path = "./protocol-macros" }
 dfns-cggmp21-protocol = { path = "./protocols/dfns-cggmp21" }
 zcash-frost-protocol = { path = "./protocols/zcash-frost" }
 silent-shard-dkls23-ll-protocol = { path = "./protocols/silent-shard-dkls23-ll" }
+test-utils = { path = "./test-utils" }
+protocol-macros = { path = "./protocol-macros" }
+libp2p-gadget = { path = "./libp2p" }
+sc-network-common = { path = "./libp2p/common"  }
+sc-network-sync = { path = "./libp2p/sync" }
 
 snowbridge-milagro-bls = { git = "https://github.com/Snowfork/milagro_bls", default-features = false, rev = "43a5d480ed6e3b83de4cf54888680d51604199e6" }
 gennaro-dkg = { git = "https://github.com/mikelodder7/gennaro-dkg.git", default-features = false }
@@ -51,7 +58,7 @@ curv = { package = "curv-kzen", version = "0.10.0" }
 digest = "0.10"
 # dfns-cggmp21 = { package = "cggmp21", version = "0.1.1" }
 dfns-cggmp21 = { package = "cggmp21", git = "https://github.com/webb-tools/cggmp21", branch = "v0.1.1-fix" }
-udigest = { version = "0.1", features = ["std", "derive"]}
+udigest = { version = "0.1", features = ["std", "derive"] }
 
 frost-core = { git = "https://github.com/salman01zp/frost", branch = "salman/bump-curve25519-dalek" }
 frost-ed25519 = { git = "https://github.com/salman01zp/frost", branch = "salman/bump-curve25519-dalek" }
@@ -72,7 +79,9 @@ sp-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "rel
 sc-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
 sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-application-crypto = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sp-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sp-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-keyring = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-keystore = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
@@ -80,6 +89,9 @@ sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "
 sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
 sp-version = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sp-externalities = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sp-arithmetic = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
+sp-test-primitives = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sp-tracing = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 
 sc-client-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
 sc-offchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
@@ -87,10 +99,8 @@ sc-basic-authorship = { git = "https://github.com/paritytech/polkadot-sdk", bran
 sc-consensus-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-consensus-grandpa = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
-sc-network = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
-sc-network-common = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
-sc-network-sync = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-service = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sc-keystore = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-telemetry = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-transaction-pool-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
@@ -98,12 +108,18 @@ sc-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "r
 sc-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-rpc-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 sc-rpc-spec-v2 = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+sc-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 parity-scale-codec = "3.6.5"
 
+prometheus-endpoint = { package = "substrate-prometheus-endpoint", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
+substrate-test-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
+substrate-test-runtime-client = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
+
 substrate-build-script-utils = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
 pallet-im-online = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 pallet-transaction-payment-rpc = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
+fork-tree = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
 scale-info = "2.10.0"
 
 frame-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0"  }
@@ -137,8 +153,11 @@ bincode2 = "2"
 futures-util = "0.3.30"
 serde = "1.0.188"
 serde_json = "1.0"
+url = { version = "2.5", default-features = false }
 async-trait = "0.1.73"
 log = "0.4.20"
+tracing = { version = "0.1", default-features = false }
+tracing-subscriber = { version = "0.3", default-features = false }
 parking_lot = "0.12.1"
 futures = "0.3.30"
 hex = "0.4.3"
@@ -147,7 +166,7 @@ uuid = "1.4.1"
 bytes = "1.5.0"
 serde_bytes = "0.11.12"
 itertools = "0.12.0"
-strum = "0.25.0"
+strum = "0.26"
 clap = "4.0.32"
 hex-literal = "0.4.1"
 rand = "0.8.5"
@@ -162,10 +181,15 @@ starknet-crypto = "0.6.1"
 anyhow = "1.0.79"
 libsecp256k1 = "0.7.1"
 rayon = { version = "1.8.0" }
-thiserror = { version = "=1.0.53" }
+thiserror = { version = "=1.0.57" }
 substrate-prometheus-endpoint = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.7.0" }
 lazy_static = "1.4.0"
 sqlx = "=0.7.3"
 postcard = "1.0.8"
 sha2 = "0.10.8"
 derivation-path = "0.2.0"
+libp2p = { git = "https://github.com/libp2p/rust-libp2p/", rev = "3644879956b6ab93b8d23553a33e8fdb838f576f" }
+
+
+[profile.dev.package.backtrace]
+opt-level = 3
diff --git a/gadget-common/Cargo.toml b/common/Cargo.toml
similarity index 92%
rename from gadget-common/Cargo.toml
rename to common/Cargo.toml
index fb7d0d9f4..49bc18275 100644
--- a/gadget-common/Cargo.toml
+++ b/common/Cargo.toml
@@ -35,15 +35,14 @@ sp-application-crypto = { workspace = true, optional = true }
 sp-runtime = { workspace = true, optional = true }
 sc-service = { workspace = true, optional = true }
 sc-client-api = { workspace = true, optional = true }
-sc-network = { workspace = true, optional = true }
-sc-network-common = { workspace = true, optional = true }
-sc-network-sync = { workspace = true, optional = true }
 substrate-prometheus-endpoint = { workspace = true, optional = true }
 frame-support = { workspace = true, optional = true }
 linked-hash-map = { workspace = true, optional = true }
 
 parity-scale-codec = { workspace = true, optional = true }
 scale-info = { workspace = true, optional = true }
+libp2p-gadget = { workspace = true, optional = true }
+sc-network-sync = { workspace = true, optional = true }
 
 
 [features]
@@ -59,9 +58,8 @@ substrate = [
   "sp-application-crypto",
   "sc-service",
   "sc-client-api",
-  "sc-network",
-  "sc-network-common",
-  "sc-network-sync",
+  "libp2p-gadget",
+    "sc-network-sync",
   "frame-support",
   "linked-hash-map",
   "substrate-prometheus-endpoint",
diff --git a/gadget-common/src/channels.rs b/common/src/channels.rs
similarity index 100%
rename from gadget-common/src/channels.rs
rename to common/src/channels.rs
diff --git a/gadget-common/src/client.rs b/common/src/client.rs
similarity index 90%
rename from gadget-common/src/client.rs
rename to common/src/client.rs
index d6f39f063..f8e9b57e8 100644
--- a/gadget-common/src/client.rs
+++ b/common/src/client.rs
@@ -11,7 +11,8 @@ use subxt::OnlineClient;
 use tangle_subxt::subxt;
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_primitives::{jobs, roles};
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_testnet_runtime::{
-    MaxDataLen, MaxKeyLen, MaxParticipants, MaxProofLen, MaxSignatureLen, MaxSubmissionLen,
+    MaxAdditionalParamsLen, MaxDataLen, MaxKeyLen, MaxParticipants, MaxProofLen, MaxSignatureLen,
+    MaxSubmissionLen,
 };
 
 pub struct JobsClient<C> {
@@ -78,6 +79,7 @@ pub trait PhaseResultExt {
 }
 
 #[async_trait]
+#[auto_impl(Arc)]
 pub trait ClientWithApi: Client {
     /// Query jobs associated with a specific validator.
     ///
@@ -98,7 +100,17 @@ pub trait ClientWithApi: Client {
         at: [u8; 32],
         validator: AccountId32,
     ) -> Result<
-        Option<Vec<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>>,
+        Option<
+            Vec<
+                jobs::RpcResponseJobsData<
+                    AccountId32,
+                    u64,
+                    MaxParticipants,
+                    MaxSubmissionLen,
+                    MaxAdditionalParamsLen,
+                >,
+            >,
+        >,
         crate::Error,
     >;
     /// Queries a job by its key and ID.
@@ -117,7 +129,15 @@ pub trait ClientWithApi: Client {
         role_type: roles::RoleType,
         job_id: u64,
     ) -> Result<
-        Option<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>,
+        Option<
+            jobs::RpcResponseJobsData<
+                AccountId32,
+                u64,
+                MaxParticipants,
+                MaxSubmissionLen,
+                MaxAdditionalParamsLen,
+            >,
+        >,
         crate::Error,
     >;
 
@@ -147,6 +167,7 @@ pub trait ClientWithApi: Client {
                 MaxSignatureLen,
                 MaxSubmissionLen,
                 MaxProofLen,
+                MaxAdditionalParamsLen,
             >,
         >,
         crate::Error,
@@ -175,7 +196,15 @@ impl<C: ClientWithApi> JobsClient<C> {
         at: [u8; 32],
         validator: sr25519::Public,
     ) -> Result<
-        Vec<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>,
+        Vec<
+            jobs::RpcResponseJobsData<
+                AccountId32,
+                u64,
+                MaxParticipants,
+                MaxSubmissionLen,
+                MaxAdditionalParamsLen,
+            >,
+        >,
         crate::Error,
     > {
         Ok(self
@@ -204,7 +233,15 @@ impl<C: ClientWithApi> JobsClient<C> {
         role_type: roles::RoleType,
         job_id: u64,
     ) -> Result<
-        Option<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>,
+        Option<
+            jobs::RpcResponseJobsData<
+                AccountId32,
+                u64,
+                MaxParticipants,
+                MaxSubmissionLen,
+                MaxAdditionalParamsLen,
+            >,
+        >,
         crate::Error,
     > {
         self.client.query_job_by_id(at, role_type, job_id).await
@@ -226,6 +263,7 @@ impl<C: ClientWithApi> JobsClient<C> {
                 MaxSignatureLen,
                 MaxSubmissionLen,
                 MaxProofLen,
+                MaxAdditionalParamsLen,
             >,
         >,
         crate::Error,
@@ -243,6 +281,7 @@ impl<C: ClientWithApi> JobsClient<C> {
             MaxSignatureLen,
             MaxDataLen,
             MaxProofLen,
+            MaxAdditionalParamsLen,
         >,
     ) -> Result<(), crate::Error> {
         self.pallet_tx
@@ -251,8 +290,8 @@ impl<C: ClientWithApi> JobsClient<C> {
     }
 }
 
-impl<MaxParticipants, MaxSubmissionLen> JobTypeExt
-    for jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>
+impl<MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen> JobTypeExt
+    for jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>
 {
     fn is_phase_one(&self) -> bool {
         use jobs::JobType::*;
@@ -318,6 +357,7 @@ impl<
         MaxSignatureLen,
         MaxSubmissionLen,
         MaxProofLen,
+        MaxAdditionalParamsLen,
     > PhaseResultExt
     for jobs::PhaseResult<
         AccountId32,
@@ -328,6 +368,7 @@ impl<
         MaxSignatureLen,
         MaxSubmissionLen,
         MaxProofLen,
+        MaxAdditionalParamsLen,
     >
 {
     fn participants(&self) -> Option<Vec<AccountId32>> {
@@ -403,6 +444,7 @@ pub trait PalletSubmitter: Send + Sync + 'static {
             MaxSignatureLen,
             MaxDataLen,
             MaxProofLen,
+            MaxAdditionalParamsLen,
         >,
     ) -> Result<(), crate::Error>;
 }
@@ -437,6 +479,7 @@ where
             MaxSignatureLen,
             MaxDataLen,
             MaxProofLen,
+            MaxAdditionalParamsLen,
         >,
     ) -> Result<(), crate::Error> {
         let tx = tangle_subxt::tangle_runtime::api::tx()
@@ -481,12 +524,17 @@ where
                 .map_err(|err| crate::Error::ClientError {
                     err: format!("Failed to setup api: {err:?}"),
                 })?;
-        Ok(Self {
+        Ok(Self::with_client(subxt_client, signer, logger))
+    }
+
+    pub fn with_client(subxt_client: OnlineClient<C>, signer: S, logger: DebugLogger) -> Self {
+        Self {
             subxt_client,
             signer,
             logger,
-        })
+        }
     }
+
     async fn submit<Call: TxPayload>(&self, call: &Call) -> anyhow::Result<C::Hash> {
         if let Some(details) = call.validation_details() {
             self.logger.trace(format!(
diff --git a/gadget-common/src/config.rs b/common/src/config.rs
similarity index 100%
rename from gadget-common/src/config.rs
rename to common/src/config.rs
diff --git a/gadget-common/src/debug_logger.rs b/common/src/debug_logger.rs
similarity index 52%
rename from gadget-common/src/debug_logger.rs
rename to common/src/debug_logger.rs
index 43d09d243..bbf40b347 100644
--- a/gadget-common/src/debug_logger.rs
+++ b/common/src/debug_logger.rs
@@ -1,28 +1,48 @@
 use std::fmt::Display;
 
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub struct DebugLogger {
     pub peer_id: String,
 }
 
 impl DebugLogger {
     pub fn trace<T: Display>(&self, msg: T) {
+        if self.peer_id.is_empty() {
+            log::trace!(target: "gadget", "{msg}");
+            return;
+        }
         log::trace!(target: "gadget", "[{}] {msg}", &self.peer_id);
     }
 
     pub fn debug<T: Display>(&self, msg: T) {
+        if self.peer_id.is_empty() {
+            log::debug!(target: "gadget", "{msg}");
+            return;
+        }
         log::debug!(target: "gadget", "[{}] {msg}", &self.peer_id);
     }
 
     pub fn info<T: Display>(&self, msg: T) {
+        if self.peer_id.is_empty() {
+            log::info!(target: "gadget", "{msg}");
+            return;
+        }
         log::info!(target: "gadget", "[{}] {msg}", &self.peer_id);
     }
 
     pub fn warn<T: Display>(&self, msg: T) {
+        if self.peer_id.is_empty() {
+            log::warn!(target: "gadget", "{msg}");
+            return;
+        }
         log::warn!(target: "gadget", "[{}] {msg}", &self.peer_id);
     }
 
     pub fn error<T: Display>(&self, msg: T) {
+        if self.peer_id.is_empty() {
+            log::error!(target: "gadget", "{msg}");
+            return;
+        }
         log::error!(target: "gadget", "[{}] {msg}", &self.peer_id);
     }
 }
diff --git a/gadget-common/src/full_protocol.rs b/common/src/full_protocol.rs
similarity index 98%
rename from gadget-common/src/full_protocol.rs
rename to common/src/full_protocol.rs
index 7dc9e785e..bf1d3ad5c 100644
--- a/gadget-common/src/full_protocol.rs
+++ b/common/src/full_protocol.rs
@@ -19,7 +19,7 @@ use std::sync::Arc;
 use tangle_subxt::subxt::utils::AccountId32;
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_primitives::{jobs, roles};
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_testnet_runtime::{
-    MaxParticipants, MaxSubmissionLen,
+    MaxAdditionalParamsLen, MaxParticipants, MaxSubmissionLen,
 };
 use tokio::sync::mpsc::UnboundedReceiver;
 
@@ -79,7 +79,7 @@ pub trait FullProtocolConfig: Clone + Send + Sync + Sized + 'static {
 
     fn phase_filter(
         &self,
-        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
     ) -> bool;
 
     fn jobs_client(&self) -> &SharedOptional<JobsClient<Self::Client>>;
@@ -182,7 +182,7 @@ impl<T: FullProtocolConfig> GadgetProtocol<T::Client> for T {
 
     fn phase_filter(
         &self,
-        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
     ) -> bool {
         T::phase_filter(self, job)
     }
@@ -202,10 +202,6 @@ impl<T: FullProtocolConfig> GadgetProtocol<T::Client> for T {
 
 #[async_trait]
 impl<T: FullProtocolConfig> Network for T {
-    fn greatest_authority_id(&self) -> Option<Public> {
-        T::internal_network(self).greatest_authority_id()
-    }
-
     async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
         let mut message = T::internal_network(self).next_message().await?;
         if let Some(peer_public_key) = message.from_network_id {
@@ -255,6 +251,10 @@ impl<T: FullProtocolConfig> Network for T {
         crate::prometheus::BYTES_SENT.inc_by(message.payload.len() as u64);
         T::internal_network(self).send_message(message).await
     }
+
+    fn greatest_authority_id(&self) -> Option<Public> {
+        T::internal_network(self).greatest_authority_id()
+    }
 }
 
 /// Used for constructing an instance of a node. If there is both a keygen and a signing protocol, then,
diff --git a/gadget-common/src/gadget/message.rs b/common/src/gadget/message.rs
similarity index 100%
rename from gadget-common/src/gadget/message.rs
rename to common/src/gadget/message.rs
diff --git a/gadget-common/src/gadget/metrics.rs b/common/src/gadget/metrics.rs
similarity index 100%
rename from gadget-common/src/gadget/metrics.rs
rename to common/src/gadget/metrics.rs
diff --git a/gadget-common/src/gadget/mod.rs b/common/src/gadget/mod.rs
similarity index 95%
rename from gadget-common/src/gadget/mod.rs
rename to common/src/gadget/mod.rs
index 3b2d0676b..a5e2d2ef8 100644
--- a/gadget-common/src/gadget/mod.rs
+++ b/common/src/gadget/mod.rs
@@ -16,7 +16,7 @@ use std::time::Duration;
 use tangle_subxt::subxt::utils::AccountId32;
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_primitives::{jobs, roles};
 use tangle_subxt::tangle_runtime::api::runtime_types::tangle_testnet_runtime::{
-    MaxParticipants, MaxSubmissionLen,
+    MaxAdditionalParamsLen, MaxParticipants, MaxSubmissionLen,
 };
 
 pub mod message;
@@ -68,10 +68,13 @@ impl<C: ClientWithApi, N: Network, M: GadgetProtocol<C>> Module<C, N, M> {
 }
 
 pub struct JobInitMetadata {
-    pub job_type: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+    pub job_type:
+        jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
     pub role_type: roles::RoleType,
     /// This value only exists if this is a stage2 job
-    pub phase1_job: Option<jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>>,
+    pub phase1_job: Option<
+        jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
+    >,
     pub participants_role_ids: Vec<ecdsa::Public>,
     pub task_id: <WorkManager as WorkManagerInterface>::TaskID,
     pub retry_id: <WorkManager as WorkManagerInterface>::RetryID,
@@ -96,7 +99,7 @@ impl<C: ClientWithApi, N: Network, M: GadgetProtocol<C>> SubstrateGadgetModule f
     ) -> Result<(), Self::Error> {
         let now: u64 = notification.number;
         *self.clock.write() = Some(now);
-        self.protocol.logger().info(format!(
+        self.protocol.logger().trace(format!(
             "Processing finality notification at block number {now}",
         ));
 
@@ -158,7 +161,11 @@ impl<C: ClientWithApi, N: Network, M: GadgetProtocol<C>> SubstrateGadgetModule f
             let task_id = job_id.to_be_bytes();
             let task_id = keccak_256(&task_id);
             if self.job_manager.job_exists(&task_id) {
-                // log::warn!(target: "gadget", "The job requested for initialization is already running or enqueued, skipping submission");
+                self.protocol.logger().trace(format!(
+                    "[{}] The job {} is already running or enqueued, skipping submission",
+                    self.protocol.name(),
+                    job.job_id,
+                ));
                 continue;
             }
 
@@ -242,7 +249,7 @@ impl<C: ClientWithApi, N: Network, M: GadgetProtocol<C>> SubstrateGadgetModule f
         for relevant_job in relevant_jobs {
             let task_id = relevant_job.task_id;
             let retry_id = relevant_job.retry_id;
-            self.protocol.logger().trace(format!(
+            self.protocol.logger().info(format!(
                 "Creating job for task {task_id} with retry id {retry_id}",
                 task_id = hex::encode(task_id),
                 retry_id = retry_id
@@ -361,7 +368,7 @@ pub trait GadgetProtocol<C: ClientWithApi>: AsyncProtocol + Send + Sync {
     /// ```
     fn phase_filter(
         &self,
-        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+        job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
     ) -> bool;
     fn client(&self) -> JobsClient<C>;
     fn logger(&self) -> DebugLogger;
diff --git a/gadget-common/src/gadget/network/gossip.rs b/common/src/gadget/network/gossip.rs
similarity index 74%
rename from gadget-common/src/gadget/network/gossip.rs
rename to common/src/gadget/network/gossip.rs
index 4bedc8070..3a7dbe10a 100644
--- a/gadget-common/src/gadget/network/gossip.rs
+++ b/common/src/gadget/network/gossip.rs
@@ -1,4 +1,4 @@
-//! A Gossip Engine that uses [`sc_network::NetworkService`] as a backend.
+//! A Gossip Engine that uses [`libp2p_gadget::NetworkService`] as a backend.
 //!
 //! In a nutshell, it works as follows:
 //!
@@ -27,18 +27,16 @@ use crate::Error;
 use async_trait::async_trait;
 use futures::StreamExt;
 use gadget_core::job_manager::WorkManagerInterface;
+use libp2p_gadget::protocol::notifications::NotificationHandle;
+use libp2p_gadget::service::traits::{NotificationEvent, ValidationResult};
+use libp2p_gadget::{
+    config, error, multiaddr, Event, MessageSink, NetworkEventStream, NetworkNotification,
+    NetworkPeers, NetworkService, NetworkStateInfo, NotificationService, PeerId, ProtocolName,
+};
 use linked_hash_map::LinkedHashMap;
 use parking_lot::{Mutex, RwLock};
-use sc_network::{
-    config, error, multiaddr, Event, NetworkEventStream, NetworkNotification, NetworkPeers,
-    NetworkService, NetworkStateInfo, PeerId, ProtocolName,
-};
-use sc_network_sync::SyncEvent;
-use sc_network_sync::SyncEventStream;
-use sc_network_sync::SyncingService;
 use serde::{Deserialize, Serialize};
-use sp_core::{ecdsa, Pair};
-use sp_runtime::traits::Block;
+use sp_core::{ecdsa, keccak_256, Pair};
 use std::{
     collections::{HashMap, HashSet},
     hash::Hash,
@@ -80,6 +78,26 @@ pub struct HandshakeMessage {
     pub account_id: ecdsa::Public,
 }
 
+/// Returns the configuration of the set to put in the network configuration.
+pub fn set_config(
+    protocol_name: ProtocolName,
+) -> (config::NonDefaultSetConfig, NotificationHandle) {
+    let (non_default_set_config, notification_service) = config::NonDefaultSetConfig::new(
+        protocol_name,
+        Vec::new(), // fallback_names is empty as we accept only reserved nodes
+        MAX_MESSAGE_SIZE,
+        None, // handshake is None
+        config::SetConfig {
+            in_peers: 0,                                            // Zero slots for in_peers
+            out_peers: 0,                                           // Zero slots for out_peers
+            reserved_nodes: Vec::new(),                             // Accepts only reserved nodes
+            non_reserved_mode: config::NonReservedPeerMode::Accept, // Denies non-reserved nodes
+        },
+    );
+
+    (non_default_set_config, notification_service)
+}
+
 impl<KBE: KeystoreBackend> NetworkGossipEngineBuilder<KBE> {
     /// Create a new network gossip engine.
     pub fn new(protocol_name: ProtocolName, keystore: ECDSAKeyStore<KBE>) -> Self {
@@ -89,47 +107,26 @@ impl<KBE: KeystoreBackend> NetworkGossipEngineBuilder<KBE> {
         }
     }
 
-    /// Returns the configuration of the set to put in the network configuration.
-    pub fn set_config(protocol_name: ProtocolName) -> config::NonDefaultSetConfig {
-        let (non_default_set_config, _notification_service) = config::NonDefaultSetConfig::new(
-            protocol_name,
-            Vec::new(), // fallback_names is empty as we accept only reserved nodes
-            MAX_MESSAGE_SIZE,
-            None, // handshake is None
-            config::SetConfig {
-                in_peers: 0,                                          // Zero slots for in_peers
-                out_peers: 0,                                         // Zero slots for out_peers
-                reserved_nodes: Vec::new(),                           // Accepts only reserved nodes
-                non_reserved_mode: config::NonReservedPeerMode::Deny, // Denies non-reserved nodes
-            },
-        );
-
-        non_default_set_config
-    }
-
     /// Turns the builder into the actual handler. Returns a controller that allows controlling
     /// the behaviour of the handler while it's running.
     ///
-    /// Important: the gossip mechanism is initially disabled and doesn't gossip messages.
-    /// You must call [`GossipHandlerController::set_gossip_enabled`] to enable it.
     ///
     /// The returned values are:
     /// - a [`GossipHandler`] that is a simple background task that should run indefinitely, and
     /// - a [`GossipHandlerController`] that can be used to control that background task.
-    pub fn build<B: Block>(
+    pub fn build(
         self,
-        service: Arc<NetworkService<B, B::Hash>>,
-        sync_service: Arc<SyncingService<B>>,
+        service: Arc<NetworkService>,
         metrics: Option<Metrics>,
         logger: DebugLogger,
-    ) -> error::Result<(GossipHandler<B, KBE>, GossipHandlerController)> {
+    ) -> error::Result<(GossipHandler<KBE>, GossipHandlerController)> {
         // Here we need to create few channels to communicate back and forth between the
         // background task and the controller.
         // since we have two things here we will need two channels:
         // 1. a channel to send commands to the background task (Controller -> Background).
         let (handler_channel, handler_channel_rx) = tokio::sync::mpsc::unbounded_channel();
         let (message_channel_tx, message_channel_rx) = tokio::sync::mpsc::unbounded_channel();
-        let gossip_enabled = Arc::new(AtomicBool::new(false));
+        let gossip_enabled = Arc::new(AtomicBool::new(true));
         let authority_id_to_peer_id = Arc::new(RwLock::new(HashMap::new()));
         let handler = GossipHandler {
             keystore: self.keystore,
@@ -142,7 +139,6 @@ impl<KBE: KeystoreBackend> NetworkGossipEngineBuilder<KBE> {
             authority_id_to_peer_id: authority_id_to_peer_id.clone(),
             gossip_enabled: gossip_enabled.clone(),
             service,
-            sync_service,
             peers: Arc::new(RwLock::new(HashMap::new())),
             logger: logger.clone(),
             metrics: Arc::new(metrics),
@@ -167,7 +163,7 @@ impl<KBE: KeystoreBackend> NetworkGossipEngineBuilder<KBE> {
 
 #[allow(unused)]
 mod rep {
-    use sc_network::ReputationChange as Rep;
+    use libp2p_gadget::ReputationChange as Rep;
     /// Reputation change when a peer sends us a message that we didn't know about.
     pub const GOOD_MESSAGE: Rep = Rep::new(1 << 7, "Good message");
     /// We received an unexpected message packet.
@@ -276,7 +272,7 @@ impl GossipHandlerController {
 /// Handler for gossiping messages. Call [`GossipHandler::run`] to start the processing.
 ///
 /// This is a background task that handles all the messages.
-pub struct GossipHandler<B: Block + 'static, KBE: KeystoreBackend> {
+pub struct GossipHandler<KBE: KeystoreBackend> {
     /// The Protocol Name, should be unique.
     ///
     /// Used as an identifier for the gossip protocol.
@@ -292,8 +288,7 @@ pub struct GossipHandler<B: Block + 'static, KBE: KeystoreBackend> {
     /// multiple times concurrently.
     pending_messages_peers: Arc<RwLock<LruHashMap<Vec<u8>, HashSet<PeerId>>>>,
     /// Network service to use to send messages and manage peers.
-    service: Arc<NetworkService<B, B::Hash>>,
-    sync_service: Arc<SyncingService<B>>,
+    service: Arc<NetworkService>,
     // All connected peers
     peers: Arc<RwLock<HashMap<PeerId, Peer>>>,
     /// A mapping from authority id to peer id.
@@ -307,7 +302,7 @@ pub struct GossipHandler<B: Block + 'static, KBE: KeystoreBackend> {
     metrics: Arc<Option<Metrics>>,
 }
 
-impl<B: Block + 'static, KBE: KeystoreBackend> Clone for GossipHandler<B, KBE> {
+impl<KBE: KeystoreBackend> Clone for GossipHandler<KBE> {
     fn clone(&self) -> Self {
         Self {
             protocol_name: self.protocol_name.clone(),
@@ -316,7 +311,6 @@ impl<B: Block + 'static, KBE: KeystoreBackend> Clone for GossipHandler<B, KBE> {
             incoming_messages_stream: self.incoming_messages_stream.clone(),
             pending_messages_peers: self.pending_messages_peers.clone(),
             service: self.service.clone(),
-            sync_service: self.sync_service.clone(),
             peers: self.peers.clone(),
             authority_id_to_peer_id: self.authority_id_to_peer_id.clone(),
             gossip_enabled: self.gossip_enabled.clone(),
@@ -345,7 +339,7 @@ struct Peer {
     authority_id: Option<ecdsa::Public>,
 }
 
-impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
+impl<KBE: KeystoreBackend> GossipHandler<KBE> {
     /// Turns the [`GossipHandler`] into a future that should run forever and not be
     /// interrupted.
     pub async fn run(self) {
@@ -355,7 +349,6 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
             .take()
             .expect("incoming message stream already taken");
         let mut event_stream = self.service.event_stream("dkg-handler");
-        let mut sync_event_stream = self.sync_service.event_stream("dkg-handler");
 
         self.logger.debug("Starting the Gossip Handler");
 
@@ -397,13 +390,6 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
                 self2.handle_network_event(event).await;
             }
         });
-        // third task, handles the incoming events from the sync stream.
-        let mut self3 = self.clone();
-        let sync_events_task = tokio::spawn(async move {
-            while let Some(event) = sync_event_stream.next().await {
-                self3.handle_sync_event(event).await;
-            }
-        });
         // wait for the first task to finish or error out.
         //
         // The reason why we wait for the first one to finish, is that it is a critical error
@@ -420,19 +406,21 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
         // events   task should have finished as well.
         // 3. The timer task, however, will never finish, unless the node is shutting down, in which
         //  case the network events task should have finished as well.
-        let _result = futures::future::select_all(vec![
-            network_events_task,
-            incoming_messages_task,
-            sync_events_task,
-        ])
-        .await;
+        let _result =
+            futures::future::select_all(vec![network_events_task, incoming_messages_task]).await;
         self.logger
             .error("The Gossip Handler has finished!!".to_string());
     }
 
-    async fn handle_sync_event(&mut self, event: SyncEvent) {
+    async fn handle_network_event(&self, event: Event) {
         match event {
-            SyncEvent::PeerConnected(remote) => {
+            Event::Dht(_) => {}
+            Event::NotificationStreamOpened {
+                remote, protocol, ..
+            } if protocol == self.protocol_name => {
+                self.logger
+                    .debug(format!("Peer {remote} connected to gossip protocol"));
+
                 let addr = iter::once(multiaddr::Protocol::P2p(remote.into()))
                     .collect::<multiaddr::Multiaddr>();
                 let result = self
@@ -442,27 +430,6 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
                     self.logger
                         .error(format!("Add reserved peer failed: {err}"));
                 }
-            }
-            SyncEvent::PeerDisconnected(remote) => {
-                if let Err(err) = self.service.remove_peers_from_reserved_set(
-                    self.protocol_name.clone(),
-                    iter::once(remote).collect(),
-                ) {
-                    self.logger
-                        .error(format!("Remove reserved peer failed: {err}"));
-                }
-            }
-        }
-    }
-
-    async fn handle_network_event(&self, event: Event) {
-        match event {
-            Event::Dht(_) => {}
-            Event::NotificationStreamOpened {
-                remote, protocol, ..
-            } if protocol == self.protocol_name => {
-                self.logger
-                    .debug(format!("Peer {remote} connected to gossip protocol"));
                 // Send our Handshake message to that peer.
                 self.send_handshake_message(remote).await;
                 let mut lock = self.peers.write();
@@ -486,6 +453,13 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
             Event::NotificationStreamClosed { remote, protocol }
                 if protocol == self.protocol_name =>
             {
+                if let Err(err) = self.service.remove_peers_from_reserved_set(
+                    self.protocol_name.clone(),
+                    iter::once(remote).collect(),
+                ) {
+                    self.logger
+                        .error(format!("Remove reserved peer failed: {err}"));
+                }
                 let mut lock = self.peers.write();
                 let peer = lock.remove(&remote);
                 debug_assert!(peer.is_some());
@@ -537,8 +511,18 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
                     };
                 }
             }
-            Event::NotificationStreamOpened { .. } => {}
-            Event::NotificationStreamClosed { .. } => {}
+            Event::NotificationStreamOpened {
+                protocol, remote, ..
+            } => {
+                self.logger
+                    .debug(format!("NotificationStreamOpened {protocol} {remote}",));
+            }
+            Event::NotificationStreamClosed {
+                protocol, remote, ..
+            } => {
+                self.logger
+                    .debug(format!("NotificationStreamClosed {protocol} {remote}",));
+            }
         }
     }
 
@@ -702,8 +686,9 @@ impl<B: Block + 'static, KBE: KeystoreBackend> GossipHandler<B, KBE> {
         };
         if peer_ids.is_empty() {
             let message_hash = message.hash();
+            let h = hex::encode(message_hash);
             self.logger
-                .warn(format!("No peers to gossip message {message_hash:?}"));
+                .warn(format!("No peers to gossip message 0x{h}"));
             return;
         }
         for peer in peer_ids {
@@ -777,3 +762,192 @@ impl<T: Hash + Eq> LruHashSet<T> {
         self.set.insert(e, ())
     }
 }
+
+#[derive(Clone)]
+pub struct SingleReceiverNotificationHandle {
+    /// Important: While this implementation is cloneable, it is expected that there is only a single
+    /// callee that calls the `recv` function. In other words, only a single point in the program can
+    /// receive messages. All other instances of this struct should be used to send messages only.
+    receiver: Arc<tokio::sync::Mutex<UnboundedReceiver<Vec<u8>>>>,
+    mapping: Arc<tokio::sync::Mutex<HashMap<PeerId, Box<dyn MessageSink>>>>,
+    logger: DebugLogger,
+}
+
+impl SingleReceiverNotificationHandle {
+    pub fn new(mut handle: NotificationHandle, logger: DebugLogger) -> Self {
+        let (tx_inbound, rx_inbound) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
+        let mapping = Arc::new(tokio::sync::Mutex::new(HashMap::<
+            PeerId,
+            Box<dyn MessageSink>,
+        >::new()));
+
+        let mapping_background = mapping.clone();
+        let logger_background = logger.clone();
+
+        let task = async move {
+            let logger_background = &logger_background;
+            let mapping_background = &mapping_background;
+
+            let receive_task = async move {
+                while let Some(event) = handle.next_event().await {
+                    match event {
+                        NotificationEvent::ValidateInboundSubstream {
+                            peer, result_tx, ..
+                        } => {
+                            // We accept all inbound substreams.
+                            if let Err(e) = result_tx.send(ValidationResult::Accept) {
+                                logger_background
+                                    .error(format!("Failed to send validation result: {e:?}"));
+                            } else {
+                                logger_background
+                                    .debug(format!("Accepted inbound substream from peer {peer}"));
+                            }
+                        }
+                        NotificationEvent::NotificationStreamOpened { peer, .. } => {
+                            logger_background
+                                .debug(format!("Notification stream opened for peer {peer}"));
+                            if let Some(message_sink) = handle.message_sink(&peer) {
+                                mapping_background.lock().await.insert(peer, message_sink);
+                                logger_background
+                                    .debug(format!("Created message sink for peer {peer}"));
+                            } else {
+                                logger_background
+                                    .warn(format!("Failed to create message sink for peer {peer}"));
+                            }
+                        }
+                        NotificationEvent::NotificationStreamClosed { peer, .. } => {
+                            logger_background
+                                .warn(format!("Notification stream closed for peer {peer}"));
+                            mapping_background.lock().await.remove(&peer);
+                        }
+                        NotificationEvent::NotificationReceived {
+                            peer: _,
+                            notification,
+                        } => {
+                            tx_inbound
+                                .send(notification)
+                                .map_err(|err| Error::NetworkError {
+                                    err: format!(
+                                        "Failed to send message to receive handler: {err:?}"
+                                    ),
+                                })?;
+                        }
+                    }
+                }
+
+                Err::<(), _>(Error::NetworkError {
+                    err: "Notification handle rx has been dropped".to_string(),
+                })
+            };
+
+            if let Err(err) = receive_task.await {
+                logger_background.error(format!("Notification receive task failed: {err:?}"));
+            }
+        };
+
+        tokio::task::spawn(task);
+
+        Self {
+            receiver: Arc::new(tokio::sync::Mutex::new(rx_inbound)),
+            mapping,
+            logger,
+        }
+    }
+
+    pub async fn send(&self, recipient: PeerId, payload: Vec<u8>) -> Result<(), Error> {
+        if let Some(message_sink) = self.mapping.lock().await.get(&recipient) {
+            let hash = hex::encode(keccak_256(&payload));
+            self.logger.trace(format!(
+                "Sending notification (0x{hash}) to peer {recipient}"
+            ));
+            message_sink
+                .send_async_notification(payload)
+                .await
+                .map_err(|err| Error::NetworkError {
+                    err: format!("Failed to send notification: {err:?}"),
+                })
+        } else {
+            Err(Error::NetworkError {
+                err: format!("Peer does not exist for transmission: {recipient}"),
+            })
+        }
+    }
+
+    pub async fn n_peers(&self) -> usize {
+        self.mapping.lock().await.len()
+    }
+}
+
+#[async_trait]
+impl Network for SingleReceiverNotificationHandle {
+    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
+        if let Ok(mut lock) = self.receiver.try_lock() {
+            let message = lock.recv().await?;
+            if let Ok(deserialized) = bincode2::deserialize(&message) {
+                Some(deserialized)
+            } else {
+                self.logger
+                    .error("Failed to deserialize message".to_string());
+                self.next_message().await
+            }
+        } else {
+            self.logger.error(
+                "There can only be a single receiver for a multiplexed notification handle"
+                    .to_string(),
+            );
+            None
+        }
+    }
+
+    async fn send_message(
+        &self,
+        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
+    ) -> Result<(), Error> {
+        let maybe_recipient = message.to_network_id;
+        match maybe_recipient {
+            Some(recipient) => {
+                let pk_of_recipient_bytes = recipient.0;
+                let recipient = PeerId::from_bytes(&pk_of_recipient_bytes).map_err(|err| {
+                    Error::NetworkError {
+                        err: format!("Failed to convert public key to peer id: {err:?}"),
+                    }
+                })?;
+
+                self.send(
+                    recipient,
+                    bincode2::serialize(&message).expect("Failed to serialize message"),
+                )
+                .await
+            }
+            None => {
+                // broadcast message
+                let mapping = self.mapping.lock().await;
+                let peers = mapping.keys().cloned().collect::<Vec<_>>();
+                // drop the lock early, to avoid waiting for the send to finish.
+                drop(mapping);
+
+                if peers.is_empty() {
+                    let message_hash = message.hash();
+                    let h = hex::encode(message_hash);
+                    self.logger
+                        .warn(format!("No peers to gossip message 0x{h}"));
+                    return Ok(());
+                }
+                let mut errors = Vec::new();
+                let message = bincode2::serialize(&message).expect("Failed to serialize message");
+                for peer in peers.into_iter() {
+                    if let Err(err) = self.send(peer, message.clone()).await {
+                        errors.push((peer, err));
+                    }
+                }
+                if errors.is_empty() {
+                    Ok(())
+                } else {
+                    Err(Error::NetworkError {
+                        err: format!("Failed to send message to some peers: {errors:?}"),
+                    })
+                }
+            }
+        }
+    }
+}
diff --git a/gadget-common/src/gadget/network/mod.rs b/common/src/gadget/network/mod.rs
similarity index 100%
rename from gadget-common/src/gadget/network/mod.rs
rename to common/src/gadget/network/mod.rs
diff --git a/gadget-common/src/gadget/work_manager.rs b/common/src/gadget/work_manager.rs
similarity index 100%
rename from gadget-common/src/gadget/work_manager.rs
rename to common/src/gadget/work_manager.rs
diff --git a/gadget-common/src/helpers.rs b/common/src/helpers.rs
similarity index 100%
rename from gadget-common/src/helpers.rs
rename to common/src/helpers.rs
diff --git a/gadget-common/src/keystore.rs b/common/src/keystore.rs
similarity index 100%
rename from gadget-common/src/keystore.rs
rename to common/src/keystore.rs
diff --git a/gadget-common/src/lib.rs b/common/src/lib.rs
similarity index 93%
rename from gadget-common/src/lib.rs
rename to common/src/lib.rs
index 24dcfc52a..3d03f9d48 100644
--- a/gadget-common/src/lib.rs
+++ b/common/src/lib.rs
@@ -37,12 +37,14 @@ pub mod prelude {
     pub use gadget_core::job_manager::SendFuture;
     pub use parking_lot::Mutex;
     pub use protocol_macros::protocol;
+    pub use sc_network_sync::SyncingService;
+    pub use sp_runtime::traits::Block;
     pub use std::pin::Pin;
     pub use std::sync::Arc;
     pub use tangle_subxt::subxt::utils::AccountId32;
     pub use tangle_subxt::tangle_runtime::api::runtime_types::tangle_primitives::{jobs, roles};
     pub use tangle_subxt::tangle_runtime::api::runtime_types::tangle_testnet_runtime::{
-        MaxParticipants, MaxSubmissionLen,
+        MaxAdditionalParamsLen, MaxParticipants, MaxSubmissionLen,
     };
 
     pub use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
@@ -211,9 +213,9 @@ macro_rules! generate_setup_and_run_command {
         }
 
         pub async fn run<C: ClientWithApi + 'static, N: Network, KBE: $crate::keystore::KeystoreBackend>(
-            mut client: Vec<C>,
+            client: Vec<C>,
             pallet_tx: Arc<dyn PalletSubmitter>,
-            mut network: Vec<N>,
+            networks: Vec<N>,
             logger: DebugLogger,
             account_id: sp_core::sr25519::Public,
             key_store: ECDSAKeyStore<KBE>,
@@ -222,9 +224,11 @@ macro_rules! generate_setup_and_run_command {
         {
             use futures::TryStreamExt;
             let futures = futures::stream::FuturesUnordered::new();
+            let mut networks: std::collections::VecDeque<_> = networks.into_iter().collect();
+            let mut clients: std::collections::VecDeque<_> = client.into_iter().collect();
 
             $(
-                let config = crate::$config::new(client.pop().expect("Not enough clients"), pallet_tx.clone(), network.pop().expect("Not enough networks"), logger.clone(), account_id.clone(), key_store.clone(), prometheus_config.clone()).await?;
+                let config = crate::$config::new(clients.pop_front().expect("Not enough clients"), pallet_tx.clone(), networks.pop_front().expect("Not enough networks"), logger.clone(), account_id.clone(), key_store.clone(), prometheus_config.clone()).await?;
                 futures.push(Box::pin(config.execute()) as std::pin::Pin<Box<dyn SendFuture<'static, Result<(), $crate::Error>>>>);
             )*
 
@@ -275,8 +279,10 @@ macro_rules! generate_protocol {
                 key_store: ECDSAKeyStore<Self::KeystoreBackend>,
                 prometheus_config: $crate::prometheus::PrometheusConfig,
             ) -> Result<Self, Error> {
-                let logger = DebugLogger {
-                    peer_id: (logger.peer_id + " | " + stringify!($name)).replace("\"", "")
+                let logger = if logger.peer_id.is_empty() {
+                    DebugLogger { peer_id: stringify!($name).replace("\"", "").into() }
+                } else {
+                    DebugLogger { peer_id: (logger.peer_id + " | " + stringify!($name)).replace("\"", "") }
                 };
                 Ok(Self {
                     pallet_tx,
@@ -343,7 +349,7 @@ macro_rules! generate_protocol {
 
             fn phase_filter(
                 &self,
-                job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+                job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
             ) -> bool {
                 matches!(job, $phase_filter)
             }
diff --git a/gadget-common/src/locks.rs b/common/src/locks.rs
similarity index 100%
rename from gadget-common/src/locks.rs
rename to common/src/locks.rs
diff --git a/gadget-common/src/prometheus.rs b/common/src/prometheus.rs
similarity index 100%
rename from gadget-common/src/prometheus.rs
rename to common/src/prometheus.rs
diff --git a/gadget-common/src/protocol.rs b/common/src/protocol.rs
similarity index 100%
rename from gadget-common/src/protocol.rs
rename to common/src/protocol.rs
diff --git a/gadget-common/src/utils.rs b/common/src/utils.rs
similarity index 100%
rename from gadget-common/src/utils.rs
rename to common/src/utils.rs
diff --git a/gadget-core/Cargo.toml b/core/Cargo.toml
similarity index 100%
rename from gadget-core/Cargo.toml
rename to core/Cargo.toml
diff --git a/gadget-core/src/gadget/manager.rs b/core/src/gadget/manager.rs
similarity index 100%
rename from gadget-core/src/gadget/manager.rs
rename to core/src/gadget/manager.rs
diff --git a/gadget-core/src/gadget/mod.rs b/core/src/gadget/mod.rs
similarity index 100%
rename from gadget-core/src/gadget/mod.rs
rename to core/src/gadget/mod.rs
diff --git a/gadget-core/src/gadget/substrate/mod.rs b/core/src/gadget/substrate/mod.rs
similarity index 100%
rename from gadget-core/src/gadget/substrate/mod.rs
rename to core/src/gadget/substrate/mod.rs
diff --git a/gadget-core/src/job.rs b/core/src/job.rs
similarity index 100%
rename from gadget-core/src/job.rs
rename to core/src/job.rs
diff --git a/gadget-core/src/job_manager.rs b/core/src/job_manager.rs
similarity index 100%
rename from gadget-core/src/job_manager.rs
rename to core/src/job_manager.rs
diff --git a/gadget-core/src/lib.rs b/core/src/lib.rs
similarity index 100%
rename from gadget-core/src/lib.rs
rename to core/src/lib.rs
diff --git a/flake.lock b/flake.lock
index a42147d9a..5c3974be1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1709386671,
-        "narHash": "sha256-VPqfBnIJ+cfa78pd4Y5Cr6sOWVW8GYHRVucxJGmRf8Q=",
+        "lastModified": 1709780214,
+        "narHash": "sha256-p4iDKdveHMhfGAlpxmkCtfQO3WRzmlD11aIcThwPqhk=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "fa9a51752f1b5de583ad5213eb621be071806663",
+        "rev": "f945939fd679284d736112d3d5410eb867f3b31c",
         "type": "github"
       },
       "original": {
diff --git a/libp2p/.DS_Store b/libp2p/.DS_Store
new file mode 100644
index 000000000..5887c83fc
Binary files /dev/null and b/libp2p/.DS_Store differ
diff --git a/libp2p/Cargo.toml b/libp2p/Cargo.toml
new file mode 100644
index 000000000..0ed89c5b2
--- /dev/null
+++ b/libp2p/Cargo.toml
@@ -0,0 +1,71 @@
+[package]
+description = "Substrate network protocol"
+name = "libp2p-gadget"
+version = "0.34.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network"
+readme = "README.md"
+workspace = ".."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+array-bytes = "6.1"
+async-channel = "1.8.0"
+async-trait = "0.1"
+asynchronous-codec = "0.6"
+bytes = "1"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] }
+either = "1.5.3"
+fnv = "1.0.6"
+futures = "0.3.21"
+futures-timer = "3.0.2"
+ip_network = "0.4.1"
+libp2p = { version = "0.51.4", features = ["dns", "identify", "kad", "macros", "mdns", "noise", "ping", "request-response", "tcp", "tokio", "websocket", "yamux"] }
+linked_hash_set = "0.1.3"
+log = "0.4.17"
+mockall = "0.11.3"
+parking_lot = "0.12.1"
+partial_sort = "0.2.0"
+pin-project = "1.0.12"
+rand = "0.8.5"
+serde = { version = "1.0.195", features = ["derive"] }
+serde_json = "1.0.111"
+smallvec = "1.11.0"
+thiserror = "1.0"
+tokio = { version = "1.22.0", features = ["macros", "sync"] }
+tokio-stream = "0.1.7"
+unsigned-varint = { version = "0.7.1", features = ["asynchronous_codec", "futures"] }
+zeroize = "1.4.3"
+prometheus-endpoint = { workspace = true }
+sc-client-api = { workspace = true }
+sc-network-common = { path = "common" }
+sc-utils = { workspace = true }
+sp-arithmetic = { workspace = true }
+sp-blockchain = { workspace = true }
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+wasm-timer = "0.2"
+auto_impl = { workspace = true }
+
+[dev-dependencies]
+assert_matches = "1.3"
+mockall = "0.11.3"
+multistream-select = "0.12.1"
+rand = "0.8.5"
+tempfile = "3.1.0"
+tokio = { version = "1.22.0", features = ["macros"] }
+tokio-util = { version = "0.7.4", features = ["compat"] }
+tokio-test = "0.4.2"
+sc-network-light = { path = "light" }
+sc-network-sync = { path = "sync" }
+sp-test-primitives = { workspace = true }
+sp-tracing = { workspace = true }
+substrate-test-runtime = { workspace = true }
+substrate-test-runtime-client = { workspace = true }
+
+[features]
+default = []
diff --git a/libp2p/README.md b/libp2p/README.md
new file mode 100644
index 000000000..f4031fbd3
--- /dev/null
+++ b/libp2p/README.md
@@ -0,0 +1,287 @@
+Substrate-specific P2P networking.
+
+**Important**: This crate is unstable and the API and usage may change.
+
+# Node identities and addresses
+
+In a decentralized network, each node possesses a network private key and a network public key.
+In Substrate, the keys are based on the ed25519 curve.
+
+From a node's public key, we can derive its *identity*. In Substrate and libp2p, a node's
+identity is represented with the [`PeerId`] struct. All network communications between nodes on
+the network use encryption derived from both sides's keys, which means that **identities cannot
+be faked**.
+
+A node's identity uniquely identifies a machine on the network. If you start two or more
+clients using the same network key, large interferences will happen.
+
+# Substrate's network protocol
+
+Substrate's networking protocol is based upon libp2p. It is at the moment not possible and not
+planned to permit using something else than the libp2p network stack and the rust-libp2p
+library. However the libp2p framework is very flexible and the rust-libp2p library could be
+extended to support a wider range of protocols than what is offered by libp2p.
+
+## Discovery mechanisms
+
+In order for our node to join a peer-to-peer network, it has to know a list of nodes that are
+part of said network. This includes nodes identities and their address (how to reach them).
+Building such a list is called the **discovery** mechanism. There are three mechanisms that
+Substrate uses:
+
+- Bootstrap nodes. These are hard-coded node identities and addresses passed alongside with
+the network configuration.
+- mDNS. We perform a UDP broadcast on the local network. Nodes that listen may respond with
+their identity. More info [here](https://github.com/libp2p/specs/blob/master/discovery/mdns.md).
+mDNS can be disabled in the network configuration.
+- Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
+configured Kademlia DHTs (one per configured chain protocol) in order for nodes to propagate to
+us their view of the network. More information about Kademlia can be found [on
+Wikipedia](https://en.wikipedia.org/wiki/Kademlia).
+
+## Connection establishment
+
+When node Alice knows node Bob's identity and address, it can establish a connection with Bob.
+All connections must always use encryption and multiplexing. While some node addresses (eg.
+addresses using `/quic`) already imply which encryption and/or multiplexing to use, for others
+the **multistream-select** protocol is used in order to negotiate an encryption layer and/or a
+multiplexing layer.
+
+The connection establishment mechanism is called the **transport**.
+
+As of the writing of this documentation, the following base-layer protocols are supported by
+Substrate:
+
+- TCP/IP for addresses of the form `/ip4/1.2.3.4/tcp/5`. Once the TCP connection is open, an
+encryption and a multiplexing layer are negotiated on top.
+- WebSockets for addresses of the form `/ip4/1.2.3.4/tcp/5/ws`. A TCP/IP connection is open and
+the WebSockets protocol is negotiated on top. Communications then happen inside WebSockets data
+frames. Encryption and multiplexing are additionally negotiated again inside this channel.
+- DNS for addresses of the form `/dns/example.com/tcp/5` or `/dns/example.com/tcp/5/ws`. A
+node's address can contain a domain name.
+- (All of the above using IPv6 instead of IPv4.)
+
+On top of the base-layer protocol, the [Noise](https://noiseprotocol.org/) protocol is
+negotiated and applied. The exact handshake protocol is experimental and is subject to change.
+
+The following multiplexing protocols are supported:
+
+- [Yamux](https://github.com/hashicorp/yamux/blob/master/spec.md).
+
+## Substreams
+
+Once a connection has been established and uses multiplexing, substreams can be opened. When
+a substream is open, the **multistream-select** protocol is used to negotiate which protocol
+to use on that given substream.
+
+Protocols that are specific to a certain chain have a `<protocol-id>` in their name. This
+"protocol ID" is defined in the chain specifications. For example, the protocol ID of Polkadot
+is "dot". In the protocol names below, `<protocol-id>` must be replaced with the corresponding
+protocol ID.
+
+> **Note**: It is possible for the same connection to be used for multiple chains. For example,
+> one can use both the `/dot/sync/2` and `/sub/sync/2` protocols on the same
+> connection, provided that the remote supports them.
+
+Substrate uses the following standard libp2p protocols:
+
+- **`/ipfs/ping/1.0.0`**. We periodically open an ephemeral substream in order to ping the
+remote and check whether the connection is still alive. Failure for the remote to reply leads
+to a disconnection.
+- **[`/ipfs/id/1.0.0`](https://github.com/libp2p/specs/tree/master/identify)**. We
+periodically open an ephemeral substream in order to ask information from the remote.
+- **[`/<protocol_id>/kad`](https://github.com/libp2p/specs/pull/108)**. We periodically open
+ephemeral substreams for Kademlia random walk queries. Each Kademlia query is done in a
+separate substream.
+
+Additionally, Substrate uses the following non-libp2p-standard protocols:
+
+- **`/substrate/<protocol-id>/<version>`** (where `<protocol-id>` must be replaced with the
+protocol ID of the targeted chain, and `<version>` is a number between 2 and 6). For each
+connection we optionally keep an additional substream for all Substrate-based communications alive.
+This protocol is considered legacy, and is progressively being replaced with alternatives.
+This is designated as "The legacy Substrate substream" in this documentation. See below for
+more details.
+- **`/<protocol-id>/sync/2`** is a request-response protocol (see below) that lets one perform
+requests for information about blocks. Each request is the encoding of a `BlockRequest` and
+each response is the encoding of a `BlockResponse`, as defined in the `api.v1.proto` file in
+this source tree.
+- **`/<protocol-id>/light/2`** is a request-response protocol (see below) that lets one perform
+light-client-related requests for information about the state. Each request is the encoding of
+a `light::Request` and each response is the encoding of a `light::Response`, as defined in the
+`light.v1.proto` file in this source tree.
+- **`/<protocol-id>/transactions/1`** is a notifications protocol (see below) where
+transactions are pushed to other nodes. The handshake is empty on both sides. The message
+format is a SCALE-encoded list of transactions, where each transaction is an opaque list of
+bytes.
+- **`/<protocol-id>/block-announces/1`** is a notifications protocol (see below) where
+block announces are pushed to other nodes. The handshake is empty on both sides. The message
+format is a SCALE-encoded tuple containing a block header followed with an opaque list of
+bytes containing some data associated with this block announcement, e.g. a candidate message.
+- Notifications protocols that are registered using `NetworkConfiguration::notifications_protocols`.
+For example: `/paritytech/grandpa/1`. See below for more information.
+
+## The legacy Substrate substream
+
+Substrate uses a component named the **peerset manager (PSM)**. Through the discovery
+mechanism, the PSM is aware of the nodes that are part of the network and decides which nodes
+we should perform Substrate-based communications with. For these nodes, we open a connection
+if necessary and open a unique substream for Substrate-based communications. If the PSM decides
+that we should disconnect a node, then that substream is closed.
+
+For more information about the PSM, see the *sc-peerset* crate.
+
+Note that at the moment there is no mechanism in place to solve the issues that arise where the
+two sides of a connection open the unique substream simultaneously. In order to not run into
+issues, only the dialer of a connection is allowed to open the unique substream. When the
+substream is closed, the entire connection is closed as well. This is a bug that will be
+resolved by deprecating the protocol entirely.
+
+Within the unique Substrate substream, messages encoded using
+[`parity-scale-codec``](https://github.com/paritytech/parity-scale-codec) are exchanged.
+The detail of theses messages is not totally in place, but they can be found in the
+`message.rs` file.
+
+Once the substream is open, the first step is an exchange of a *status* message from both
+sides, containing information such as the chain root hash, head of chain, and so on.
+
+Communications within this substream include:
+
+- Syncing. Blocks are announced and requested from other nodes.
+- Light-client requests. When a light client requires information, a random node we have a
+substream open with is chosen, and the information is requested from it.
+- Gossiping. Used for example by grandpa.
+
+## Request-response protocols
+
+A so-called request-response protocol is defined as follow:
+
+- When a substream is opened, the opening side sends a message whose content is
+protocol-specific. The message must be prefixed with an
+[LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. After the
+message has been sent, the writing side is closed.
+- The remote sends back the response prefixed with a LEB128-encoded length, and closes its
+side as well.
+
+Each request is performed in a new separate substream.
+
+## Notifications protocols
+
+A so-called notifications protocol is defined as follow:
+
+- When a substream is opened, the opening side sends a handshake message whose content is
+protocol-specific. The handshake message must be prefixed with an
+[LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. The
+handshake message can be of length 0, in which case the sender has to send a single `0`.
+- The receiver then either immediately closes the substream, or answers with its own
+LEB128-prefixed protocol-specific handshake response. The message can be of length 0, in which
+case a single `0` has to be sent back.
+- Once the handshake has completed, the notifications protocol is unidirectional. Only the
+node which initiated the substream can push notifications. If the remote wants to send
+notifications as well, it has to open its own undirectional substream.
+- Each notification must be prefixed with an LEB128-encoded length. The encoding of the
+messages is specific to each protocol.
+- Either party can signal that it doesn't want a notifications substream anymore by closing
+its writing side. The other party should respond by closing its own writing side soon after.
+
+The API of `sc-network` allows one to register user-defined notification protocols.
+`sc-network` automatically tries to open a substream towards each node for which the legacy
+Substream substream is open. The handshake is then performed automatically.
+
+For example, the `sc-consensus-grandpa` crate registers the `/paritytech/grandpa/1`
+notifications protocol.
+
+At the moment, for backwards-compatibility, notification protocols are tied to the legacy
+Substrate substream. Additionally, the handshake message is hardcoded to be a single 8-bits
+integer representing the role of the node:
+
+- 1 for a full node.
+- 2 for a light node.
+- 4 for an authority.
+
+In the future, though, these restrictions will be removed.
+
+# Sync
+
+The crate implements a number of syncing algorithms. The main purpose of the syncing algorithm is
+get the chain to the latest state and keep it synced with the rest of the network by downloading and
+importing new data as soon as it becomes available. Once the node starts it catches up with the network
+with one of the initial sync methods listed below, and once it is completed uses a keep-up sync to
+download new blocks.
+
+## Full and light sync
+
+This is the default syncing method for the initial and keep-up sync. The algorithm starts with the
+current best block and downloads block data progressively from multiple peers if available. Once
+there's a sequence of blocks ready to be imported they are fed to the import queue. Full nodes download
+and execute full blocks, while light nodes only download and import headers. This continues until each peers
+has no more new blocks to give.
+
+For each peer the sync maintains the number of our common best block with that peer. This number is updates
+whenever peer announce new blocks or our best block advances. This allows to keep track of peers that have new
+block data and request new information as soon as it is announced. In keep-up mode, we also track peers that
+announce blocks on all branches and not just the best branch. The sync algorithm tries to be greedy and download
+all data that's announced.
+
+## Fast sync
+
+In this mode the initial downloads and verifies full header history. This allows to validate
+authority set transitions and arrive at a recent header. After header chain is verified and imported
+the node starts downloading a state snapshot using the state request protocol. Each `StateRequest`
+contains a starting storage key, which is empty for the first request.
+`StateResponse` contains a storage proof for a sequence of keys and values in the storage
+starting (but not including) from the key that is in the request. After iterating the proof trie against
+the storage root that is in the target header, the node issues The next `StateRequest` with set starting
+key set to the last key from the previous response. This continues until trie iteration reaches the end.
+The state is then imported into the database and the keep-up sync starts in normal full/light sync mode.
+
+## Warp sync
+
+This is similar to fast sync, but instead of downloading and verifying full header chain, the algorithm
+only downloads finalized authority set changes.
+
+### GRANDPA warp sync
+
+GRANDPA keeps justifications for each finalized authority set change. Each change is signed by the
+authorities from the previous set. By downloading and verifying these signed hand-offs starting from genesis,
+we arrive at a recent header faster than downloading full header chain. Each `WarpSyncRequest` contains a block
+hash to a to start collecting proofs from. `WarpSyncResponse` contains a sequence of block headers and
+justifications. The proof downloader checks the justifications and continues requesting proofs from the last
+header hash, until it arrives at some recent header.
+
+Once the finality chain is proved for a header, the state matching the header is downloaded much like during
+the fast sync. The state is verified to match the header storage root. After the state is imported into the
+database it is queried for the information that allows GRANDPA and BABE to continue operating from that state.
+This includes BABE epoch information and GRANDPA authority set id.
+
+### Background block download
+
+After the latest state has been imported the node is fully operational, but is still missing historic block
+data. I.e. it is unable to serve bock bodies and headers other than the most recent one. To make sure all
+nodes have block history available, a background sync process is started that downloads all the missing blocks.
+It is run in parallel with the keep-up sync and does not interfere with downloading of the recent blocks.
+During this download we also import GRANDPA justifications for blocks with authority set changes, so that
+the warp-synced node has all the data to serve for other nodes nodes that might want to sync from it with
+any method.
+
+# Usage
+
+Using the `sc-network` crate is done through the [`NetworkWorker`] struct. Create this
+struct by passing a [`config::Params`], then poll it as if it was a `Future`. You can extract an
+`Arc<NetworkService>` from the `NetworkWorker`, which can be shared amongst multiple places
+in order to give orders to the networking.
+
+See the [`config`] module for more information about how to configure the networking.
+
+After the `NetworkWorker` has been created, the important things to do are:
+
+- Calling `NetworkWorker::poll` in order to advance the network. This can be done by
+dispatching a background task with the [`NetworkWorker`].
+- Calling `on_block_import` whenever a block is added to the client.
+- Calling `on_block_finalized` whenever a block is finalized.
+- Calling `trigger_repropagate` when a transaction is added to the pool.
+
+More precise usage details are still being worked on and will likely change in the future.
+
+
+License: GPL-3.0-or-later WITH Classpath-exception-2.0
diff --git a/libp2p/bitswap/Cargo.toml b/libp2p/bitswap/Cargo.toml
new file mode 100644
index 000000000..7db8d1d87
--- /dev/null
+++ b/libp2p/bitswap/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+description = "Substrate bitswap protocol"
+name = "sc-network-bitswap"
+version = "0.33.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-bitswap"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[build-dependencies]
+prost-build = "0.11"
+
+[dependencies]
+async-channel = "1.8.0"
+cid = "0.9.0"
+futures = "0.3.21"
+libp2p-identity = { version = "0.1.3", features = ["peerid"] }
+log = "0.4.17"
+prost = "0.12"
+thiserror = "1.0"
+unsigned-varint = { version = "0.7.1", features = ["asynchronous_codec", "futures"] }
+sc-client-api = { workspace = true }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sp-blockchain = { workspace = true }
+sp-runtime = { workspace = true }
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["full"] }
+sc-block-builder = { workspace = true }
+sc-consensus = { workspace = true }
+sp-crypto-hashing = { workspace = true }
+sp-consensus = { workspace = true }
+substrate-test-runtime = { workspace = true }
+substrate-test-runtime-client = { workspace = true }
diff --git a/libp2p/bitswap/build.rs b/libp2p/bitswap/build.rs
new file mode 100644
index 000000000..2495a9c25
--- /dev/null
+++ b/libp2p/bitswap/build.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+const PROTOS: &[&str] = &["src/schema/bitswap.v1.2.0.proto"];
+
+fn main() {
+	prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
+}
diff --git a/libp2p/bitswap/src/lib.rs b/libp2p/bitswap/src/lib.rs
new file mode 100644
index 000000000..0586354d6
--- /dev/null
+++ b/libp2p/bitswap/src/lib.rs
@@ -0,0 +1,531 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Bitswap server for Substrate.
+//!
+//! Allows querying transactions by hash over standard bitswap protocol
+//! Only supports bitswap 1.2.0.
+//! CID is expected to reference 256-bit Blake2b transaction hash.
+
+use cid::{self, Version};
+use futures::StreamExt;
+use libp2p_identity::PeerId;
+use log::{debug, error, trace};
+use prost::Message;
+use sc_client_api::BlockBackend;
+use sc_network::{
+	request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
+	types::ProtocolName,
+};
+use schema::bitswap::{
+	message::{wantlist::WantType, Block as MessageBlock, BlockPresence, BlockPresenceType},
+	Message as BitswapMessage,
+};
+use sp_runtime::traits::Block as BlockT;
+use std::{io, sync::Arc, time::Duration};
+use unsigned_varint::encode as varint_encode;
+
+mod schema;
+
+const LOG_TARGET: &str = "bitswap";
+
+// Undocumented, but according to JS the bitswap messages have a max size of 512*1024 bytes
+// https://github.com/ipfs/js-ipfs-bitswap/blob/
+// d8f80408aadab94c962f6b88f343eb9f39fa0fcc/src/decision-engine/index.js#L16
+// We set it to the same value as max substrate protocol message
+const MAX_PACKET_SIZE: u64 = 16 * 1024 * 1024;
+
+/// Max number of queued responses before denying requests.
+const MAX_REQUEST_QUEUE: usize = 20;
+
+/// Max number of blocks per wantlist
+const MAX_WANTED_BLOCKS: usize = 16;
+
+/// Bitswap protocol name
+const PROTOCOL_NAME: &'static str = "/ipfs/bitswap/1.2.0";
+
+/// Prefix represents all metadata of a CID, without the actual content.
+#[derive(PartialEq, Eq, Clone, Debug)]
+struct Prefix {
+	/// The version of CID.
+	pub version: Version,
+	/// The codec of CID.
+	pub codec: u64,
+	/// The multihash type of CID.
+	pub mh_type: u64,
+	/// The multihash length of CID.
+	pub mh_len: u8,
+}
+
+impl Prefix {
+	/// Convert the prefix to encoded bytes.
+	pub fn to_bytes(&self) -> Vec<u8> {
+		let mut res = Vec::with_capacity(4);
+		let mut buf = varint_encode::u64_buffer();
+		let version = varint_encode::u64(self.version.into(), &mut buf);
+		res.extend_from_slice(version);
+		let mut buf = varint_encode::u64_buffer();
+		let codec = varint_encode::u64(self.codec, &mut buf);
+		res.extend_from_slice(codec);
+		let mut buf = varint_encode::u64_buffer();
+		let mh_type = varint_encode::u64(self.mh_type, &mut buf);
+		res.extend_from_slice(mh_type);
+		let mut buf = varint_encode::u64_buffer();
+		let mh_len = varint_encode::u64(self.mh_len as u64, &mut buf);
+		res.extend_from_slice(mh_len);
+		res
+	}
+}
+
+/// Bitswap request handler
+pub struct BitswapRequestHandler<B> {
+	client: Arc<dyn BlockBackend<B> + Send + Sync>,
+	request_receiver: async_channel::Receiver<IncomingRequest>,
+}
+
+impl<B: BlockT> BitswapRequestHandler<B> {
+	/// Create a new [`BitswapRequestHandler`].
+	pub fn new(client: Arc<dyn BlockBackend<B> + Send + Sync>) -> (Self, ProtocolConfig) {
+		let (tx, request_receiver) = async_channel::bounded(MAX_REQUEST_QUEUE);
+
+		let config = ProtocolConfig {
+			name: ProtocolName::from(PROTOCOL_NAME),
+			fallback_names: vec![],
+			max_request_size: MAX_PACKET_SIZE,
+			max_response_size: MAX_PACKET_SIZE,
+			request_timeout: Duration::from_secs(15),
+			inbound_queue: Some(tx),
+		};
+
+		(Self { client, request_receiver }, config)
+	}
+
+	/// Run [`BitswapRequestHandler`].
+	pub async fn run(mut self) {
+		while let Some(request) = self.request_receiver.next().await {
+			let IncomingRequest { peer, payload, pending_response } = request;
+
+			match self.handle_message(&peer, &payload) {
+				Ok(response) => {
+					let response = OutgoingResponse {
+						result: Ok(response),
+						reputation_changes: Vec::new(),
+						sent_feedback: None,
+					};
+
+					match pending_response.send(response) {
+						Ok(()) => {
+							trace!(target: LOG_TARGET, "Handled bitswap request from {peer}.",)
+						},
+						Err(_) => debug!(
+							target: LOG_TARGET,
+							"Failed to handle light client request from {peer}: {}",
+							BitswapError::SendResponse,
+						),
+					}
+				},
+				Err(err) => {
+					error!(target: LOG_TARGET, "Failed to process request from {peer}: {err}");
+
+					// TODO: adjust reputation?
+
+					let response = OutgoingResponse {
+						result: Err(()),
+						reputation_changes: vec![],
+						sent_feedback: None,
+					};
+
+					if pending_response.send(response).is_err() {
+						debug!(
+							target: LOG_TARGET,
+							"Failed to handle bitswap request from {peer}: {}",
+							BitswapError::SendResponse,
+						);
+					}
+				},
+			}
+		}
+	}
+
+	/// Handle received Bitswap request
+	fn handle_message(
+		&mut self,
+		peer: &PeerId,
+		payload: &Vec<u8>,
+	) -> Result<Vec<u8>, BitswapError> {
+		let request = schema::bitswap::Message::decode(&payload[..])?;
+
+		trace!(target: LOG_TARGET, "Received request: {:?} from {}", request, peer);
+
+		let mut response = BitswapMessage::default();
+
+		let wantlist = match request.wantlist {
+			Some(wantlist) => wantlist,
+			None => {
+				debug!(target: LOG_TARGET, "Unexpected bitswap message from {}", peer);
+				return Err(BitswapError::InvalidWantList)
+			},
+		};
+
+		if wantlist.entries.len() > MAX_WANTED_BLOCKS {
+			trace!(target: LOG_TARGET, "Ignored request: too many entries");
+			return Err(BitswapError::TooManyEntries)
+		}
+
+		for entry in wantlist.entries {
+			let cid = match cid::Cid::read_bytes(entry.block.as_slice()) {
+				Ok(cid) => cid,
+				Err(e) => {
+					trace!(target: LOG_TARGET, "Bad CID {:?}: {:?}", entry.block, e);
+					continue
+				},
+			};
+
+			if cid.version() != cid::Version::V1 ||
+				cid.hash().code() != u64::from(cid::multihash::Code::Blake2b256) ||
+				cid.hash().size() != 32
+			{
+				debug!(target: LOG_TARGET, "Ignoring unsupported CID {}: {}", peer, cid);
+				continue
+			}
+
+			let mut hash = B::Hash::default();
+			hash.as_mut().copy_from_slice(&cid.hash().digest()[0..32]);
+			let transaction = match self.client.indexed_transaction(hash) {
+				Ok(ex) => ex,
+				Err(e) => {
+					error!(target: LOG_TARGET, "Error retrieving transaction {}: {}", hash, e);
+					None
+				},
+			};
+
+			match transaction {
+				Some(transaction) => {
+					trace!(target: LOG_TARGET, "Found CID {:?}, hash {:?}", cid, hash);
+
+					if entry.want_type == WantType::Block as i32 {
+						let prefix = Prefix {
+							version: cid.version(),
+							codec: cid.codec(),
+							mh_type: cid.hash().code(),
+							mh_len: cid.hash().size(),
+						};
+						response
+							.payload
+							.push(MessageBlock { prefix: prefix.to_bytes(), data: transaction });
+					} else {
+						response.block_presences.push(BlockPresence {
+							r#type: BlockPresenceType::Have as i32,
+							cid: cid.to_bytes(),
+						});
+					}
+				},
+				None => {
+					trace!(target: LOG_TARGET, "Missing CID {:?}, hash {:?}", cid, hash);
+
+					if entry.send_dont_have {
+						response.block_presences.push(BlockPresence {
+							r#type: BlockPresenceType::DontHave as i32,
+							cid: cid.to_bytes(),
+						});
+					}
+				},
+			}
+		}
+
+		Ok(response.encode_to_vec())
+	}
+}
+
+/// Bitswap protocol error.
+#[derive(Debug, thiserror::Error)]
+pub enum BitswapError {
+	/// Protobuf decoding error.
+	#[error("Failed to decode request: {0}.")]
+	DecodeProto(#[from] prost::DecodeError),
+
+	/// Protobuf encoding error.
+	#[error("Failed to encode response: {0}.")]
+	EncodeProto(#[from] prost::EncodeError),
+
+	/// Client backend error.
+	#[error(transparent)]
+	Client(#[from] sp_blockchain::Error),
+
+	/// Error parsing CID
+	#[error(transparent)]
+	BadCid(#[from] cid::Error),
+
+	/// Packet read error.
+	#[error(transparent)]
+	Read(#[from] io::Error),
+
+	/// Error sending response.
+	#[error("Failed to send response.")]
+	SendResponse,
+
+	/// Message doesn't have a WANT list.
+	#[error("Invalid WANT list.")]
+	InvalidWantList,
+
+	/// Too many blocks requested.
+	#[error("Too many block entries in the request.")]
+	TooManyEntries,
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use futures::channel::oneshot;
+	use sc_block_builder::BlockBuilderBuilder;
+	use schema::bitswap::{
+		message::{wantlist::Entry, Wantlist},
+		Message as BitswapMessage,
+	};
+	use sp_consensus::BlockOrigin;
+	use sp_runtime::codec::Encode;
+	use substrate_test_runtime::ExtrinsicBuilder;
+	use substrate_test_runtime_client::{self, prelude::*, TestClientBuilder};
+
+	#[tokio::test]
+	async fn undecodeable_message() {
+		let client = substrate_test_runtime_client::new();
+		let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
+
+		tokio::spawn(async move { bitswap.run().await });
+
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: vec![0x13, 0x37, 0x13, 0x38],
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(result, Err(()));
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+		} else {
+			panic!("invalid event received");
+		}
+	}
+
+	#[tokio::test]
+	async fn empty_want_list() {
+		let client = substrate_test_runtime_client::new();
+		let (bitswap, mut config) = BitswapRequestHandler::new(Arc::new(client));
+
+		tokio::spawn(async move { bitswap.run().await });
+
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.as_mut()
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: BitswapMessage { wantlist: None, ..Default::default() }.encode_to_vec(),
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(result, Err(()));
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+		} else {
+			panic!("invalid event received");
+		}
+
+		// Empty WANT list should not cause an error
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: BitswapMessage {
+					wantlist: Some(Default::default()),
+					..Default::default()
+				}
+				.encode_to_vec(),
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(result, Ok(BitswapMessage::default().encode_to_vec()));
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+		} else {
+			panic!("invalid event received");
+		}
+	}
+
+	#[tokio::test]
+	async fn too_long_want_list() {
+		let client = substrate_test_runtime_client::new();
+		let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
+
+		tokio::spawn(async move { bitswap.run().await });
+
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: BitswapMessage {
+					wantlist: Some(Wantlist {
+						entries: (0..MAX_WANTED_BLOCKS + 1)
+							.map(|_| Entry::default())
+							.collect::<Vec<_>>(),
+						full: false,
+					}),
+					..Default::default()
+				}
+				.encode_to_vec(),
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(result, Err(()));
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+		} else {
+			panic!("invalid event received");
+		}
+	}
+
+	#[tokio::test]
+	async fn transaction_not_found() {
+		let client = TestClientBuilder::with_tx_storage(u32::MAX).build();
+
+		let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
+		tokio::spawn(async move { bitswap.run().await });
+
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: BitswapMessage {
+					wantlist: Some(Wantlist {
+						entries: vec![Entry {
+							block: cid::Cid::new_v1(
+								0x70,
+								cid::multihash::Multihash::wrap(
+									u64::from(cid::multihash::Code::Blake2b256),
+									&[0u8; 32],
+								)
+								.unwrap(),
+							)
+							.to_bytes(),
+							..Default::default()
+						}],
+						full: false,
+					}),
+					..Default::default()
+				}
+				.encode_to_vec(),
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(result, Ok(vec![]));
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+		} else {
+			panic!("invalid event received");
+		}
+	}
+
+	#[tokio::test]
+	async fn transaction_found() {
+		let mut client = TestClientBuilder::with_tx_storage(u32::MAX).build();
+		let mut block_builder = BlockBuilderBuilder::new(&client)
+			.on_parent_block(client.chain_info().genesis_hash)
+			.with_parent_block_number(0)
+			.build()
+			.unwrap();
+
+		// encoded extrinsic: [161, .. , 2, 6, 16, 19, 55, 19, 56]
+		let ext = ExtrinsicBuilder::new_indexed_call(vec![0x13, 0x37, 0x13, 0x38]).build();
+		let pattern_index = ext.encoded_size() - 4;
+
+		block_builder.push(ext.clone()).unwrap();
+		let block = block_builder.build().unwrap().block;
+
+		client.import(BlockOrigin::File, block).await.unwrap();
+
+		let (bitswap, config) = BitswapRequestHandler::new(Arc::new(client));
+
+		tokio::spawn(async move { bitswap.run().await });
+
+		let (tx, rx) = oneshot::channel();
+		config
+			.inbound_queue
+			.unwrap()
+			.send(IncomingRequest {
+				peer: PeerId::random(),
+				payload: BitswapMessage {
+					wantlist: Some(Wantlist {
+						entries: vec![Entry {
+							block: cid::Cid::new_v1(
+								0x70,
+								cid::multihash::Multihash::wrap(
+									u64::from(cid::multihash::Code::Blake2b256),
+									&sp_crypto_hashing::blake2_256(&ext.encode()[pattern_index..]),
+								)
+								.unwrap(),
+							)
+							.to_bytes(),
+							..Default::default()
+						}],
+						full: false,
+					}),
+					..Default::default()
+				}
+				.encode_to_vec(),
+				pending_response: tx,
+			})
+			.await
+			.unwrap();
+
+		if let Ok(OutgoingResponse { result, reputation_changes, sent_feedback }) = rx.await {
+			assert_eq!(reputation_changes, Vec::new());
+			assert!(sent_feedback.is_none());
+
+			let response =
+				schema::bitswap::Message::decode(&result.expect("fetch to succeed")[..]).unwrap();
+			assert_eq!(response.payload[0].data, vec![0x13, 0x37, 0x13, 0x38]);
+		} else {
+			panic!("invalid event received");
+		}
+	}
+}
diff --git a/libp2p/bitswap/src/schema.rs b/libp2p/bitswap/src/schema.rs
new file mode 100644
index 000000000..1d62847c2
--- /dev/null
+++ b/libp2p/bitswap/src/schema.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+//! Include sources generated from protobuf definitions.
+
+pub(crate) mod bitswap {
+	include!(concat!(env!("OUT_DIR"), "/bitswap.message.rs"));
+}
diff --git a/libp2p/bitswap/src/schema/bitswap.v1.2.0.proto b/libp2p/bitswap/src/schema/bitswap.v1.2.0.proto
new file mode 100644
index 000000000..a4138b516
--- /dev/null
+++ b/libp2p/bitswap/src/schema/bitswap.v1.2.0.proto
@@ -0,0 +1,43 @@
+syntax = "proto3";
+
+package bitswap.message;
+
+message Message {
+	message Wantlist {
+		enum WantType {
+			Block = 0;
+			Have = 1;
+		}
+
+		message Entry {
+			bytes block = 1; 	// the block cid (cidV0 in bitswap 1.0.0, cidV1 in bitswap 1.1.0)
+			int32 priority = 2;	// the priority (normalized). default to 1
+			bool cancel = 3;		// whether this revokes an entry
+			WantType wantType = 4; // Note: defaults to enum 0, ie Block
+			bool sendDontHave = 5; // Note: defaults to false
+		}
+
+		repeated Entry entries = 1; // a list of wantlist entries
+		bool full = 2;							// whether this is the full wantlist. default to false
+	}
+
+	message Block {
+		bytes prefix = 1;		// CID prefix (cid version, multicodec and multihash prefix (type + length)
+		bytes data = 2;
+	}
+
+	enum BlockPresenceType {
+		Have = 0;
+		DontHave = 1;
+	}
+	message BlockPresence {
+		bytes cid = 1;
+		BlockPresenceType type = 2;
+	}
+
+	Wantlist wantlist = 1;
+	repeated bytes blocks = 2;		// used to send Blocks in bitswap 1.0.0
+	repeated Block payload = 3;		// used to send Blocks in bitswap 1.1.0
+	repeated BlockPresence blockPresences = 4;
+	int32 pendingBytes = 5;
+}
diff --git a/libp2p/common/Cargo.toml b/libp2p/common/Cargo.toml
new file mode 100644
index 000000000..82fc883d5
--- /dev/null
+++ b/libp2p/common/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+description = "Substrate network common"
+name = "sc-network-common"
+version = "0.33.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-sync"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[build-dependencies]
+prost-build = "0.11"
+
+[dependencies]
+async-trait = "0.1.74"
+bitflags = "1.3.2"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = [
+	"derive",
+] }
+futures = { workspace = true }
+libp2p-identity = { version = "0.1.3", features = ["peerid"] }
+sc-consensus = { workspace = true }
+sp-consensus = { workspace = true }
+sp-consensus-grandpa = { workspace = true }
+sp-runtime = { workspace = true }
+
+[dev-dependencies]
+tempfile = "3.1.0"
diff --git a/libp2p/common/src/lib.rs b/libp2p/common/src/lib.rs
new file mode 100644
index 000000000..8149f5ea7
--- /dev/null
+++ b/libp2p/common/src/lib.rs
@@ -0,0 +1,30 @@
+// 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/>.
+
+//! Common data structures of the networking layer.
+
+pub mod message;
+pub mod role;
+pub mod sync;
+pub mod types;
+
+/// Minimum Requirements for a Hash within Networking
+pub trait ExHashT: std::hash::Hash + Eq + std::fmt::Debug + Clone + Send + Sync + 'static {}
+
+impl<T> ExHashT for T where T: std::hash::Hash + Eq + std::fmt::Debug + Clone + Send + Sync + 'static
+{}
diff --git a/libp2p/common/src/message.rs b/libp2p/common/src/message.rs
new file mode 100644
index 000000000..12d2a6800
--- /dev/null
+++ b/libp2p/common/src/message.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+//! Network packet message types. These get serialized and put into the lower level protocol
+//! payload.
+
+/// A unique ID of a request.
+pub type RequestId = u64;
diff --git a/libp2p/common/src/role.rs b/libp2p/common/src/role.rs
new file mode 100644
index 000000000..fb7c5cb69
--- /dev/null
+++ b/libp2p/common/src/role.rs
@@ -0,0 +1,137 @@
+// 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/>.
+
+// file-level lint whitelist to avoid problem with bitflags macro below
+// TODO: can be dropped after an update to bitflags 2.4
+#![allow(clippy::bad_bit_mask)]
+
+use codec::{self, Encode, EncodeLike, Input, Output};
+
+/// Role that the peer sent to us during the handshake, with the addition of what our local node
+/// knows about that peer.
+///
+/// > **Note**: This enum is different from the `Role` enum. The `Role` enum indicates what a
+/// >    node says about itself, while `ObservedRole` is a `Role` merged with the
+/// >    information known locally about that node.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ObservedRole {
+    /// Full node.
+    Full,
+    /// Light node.
+    Light,
+    /// Third-party authority.
+    Authority,
+}
+
+impl ObservedRole {
+    /// Returns `true` for `ObservedRole::Light`.
+    pub fn is_light(&self) -> bool {
+        matches!(self, Self::Light)
+    }
+}
+
+impl From<Roles> for ObservedRole {
+    fn from(roles: Roles) -> Self {
+        if roles.is_authority() {
+            ObservedRole::Authority
+        } else if roles.is_full() {
+            ObservedRole::Full
+        } else {
+            ObservedRole::Light
+        }
+    }
+}
+
+/// Role of the local node.
+#[derive(Debug, Clone)]
+pub enum Role {
+    /// Regular full node.
+    Full,
+    /// Actual authority.
+    Authority,
+}
+
+impl Role {
+    /// True for [`Role::Authority`].
+    pub fn is_authority(&self) -> bool {
+        matches!(self, Self::Authority)
+    }
+}
+
+impl std::fmt::Display for Role {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Full => write!(f, "FULL"),
+            Self::Authority => write!(f, "AUTHORITY"),
+        }
+    }
+}
+
+bitflags::bitflags! {
+    /// Bitmask of the roles that a node fulfills.
+    pub struct Roles: u8 {
+        /// No network.
+        const NONE = 0b00000000;
+        /// Full node, does not participate in consensus.
+        const FULL = 0b00000001;
+        /// Light client node.
+        const LIGHT = 0b00000010;
+        /// Act as an authority
+        const AUTHORITY = 0b00000100;
+    }
+}
+
+impl Roles {
+    /// Does this role represents a client that holds full chain data locally?
+    pub fn is_full(&self) -> bool {
+        self.intersects(Self::FULL | Self::AUTHORITY)
+    }
+
+    /// Does this role represents a client that does not participates in the consensus?
+    pub fn is_authority(&self) -> bool {
+        *self == Self::AUTHORITY
+    }
+
+    /// Does this role represents a client that does not hold full chain data locally?
+    pub fn is_light(&self) -> bool {
+        !self.is_full()
+    }
+}
+
+impl<'a> From<&'a Role> for Roles {
+    fn from(roles: &'a Role) -> Self {
+        match roles {
+            Role::Full => Self::FULL,
+            Role::Authority => Self::AUTHORITY,
+        }
+    }
+}
+
+impl Encode for Roles {
+    fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
+        dest.push_byte(self.bits())
+    }
+}
+
+impl EncodeLike for Roles {}
+
+impl codec::Decode for Roles {
+    fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
+        Self::from_bits(input.read_byte()?).ok_or_else(|| codec::Error::from("Invalid bytes"))
+    }
+}
diff --git a/libp2p/common/src/sync.rs b/libp2p/common/src/sync.rs
new file mode 100644
index 000000000..eb49347c7
--- /dev/null
+++ b/libp2p/common/src/sync.rs
@@ -0,0 +1,55 @@
+// 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/>.
+
+//! Abstract interfaces and data structures related to network sync.
+
+pub mod message;
+
+/// Sync operation mode.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum SyncMode {
+    /// Full block download and verification.
+    Full,
+    /// Download blocks and the latest state.
+    LightState {
+        /// Skip state proof download and verification.
+        skip_proofs: bool,
+        /// Download indexed transactions for recent blocks.
+        storage_chain_mode: bool,
+    },
+    /// Warp sync - verify authority set transitions and the latest state.
+    Warp,
+}
+
+impl SyncMode {
+    /// Returns `true` if `self` is [`Self::Warp`].
+    pub fn is_warp(&self) -> bool {
+        matches!(self, Self::Warp)
+    }
+
+    /// Returns `true` if `self` is [`Self::LightState`].
+    pub fn light_state(&self) -> bool {
+        matches!(self, Self::LightState { .. })
+    }
+}
+
+impl Default for SyncMode {
+    fn default() -> Self {
+        Self::Full
+    }
+}
diff --git a/libp2p/common/src/sync/message.rs b/libp2p/common/src/sync/message.rs
new file mode 100644
index 000000000..06eff8b66
--- /dev/null
+++ b/libp2p/common/src/sync/message.rs
@@ -0,0 +1,255 @@
+// 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/>.
+
+//! Network packet message types. These get serialized and put into the lower level protocol
+//! payload.
+
+use crate::role::Roles;
+
+use bitflags::bitflags;
+use codec::{Decode, Encode, Error, Input, Output};
+pub use generic::{BlockAnnounce, FromBlock};
+use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
+
+/// Type alias for using the block request type using block type parameters.
+pub type BlockRequest<B> =
+    generic::BlockRequest<<B as BlockT>::Hash, <<B as BlockT>::Header as HeaderT>::Number>;
+
+/// Type alias for using the BlockData type using block type parameters.
+pub type BlockData<B> =
+    generic::BlockData<<B as BlockT>::Header, <B as BlockT>::Hash, <B as BlockT>::Extrinsic>;
+
+/// Type alias for using the BlockResponse type using block type parameters.
+pub type BlockResponse<B> =
+    generic::BlockResponse<<B as BlockT>::Header, <B as BlockT>::Hash, <B as BlockT>::Extrinsic>;
+
+// Bits of block data and associated artifacts to request.
+bitflags! {
+    /// Node roles bitmask.
+    pub struct BlockAttributes: u8 {
+        /// Include block header.
+        const HEADER = 0b00000001;
+        /// Include block body.
+        const BODY = 0b00000010;
+        /// Include block receipt.
+        const RECEIPT = 0b00000100;
+        /// Include block message queue.
+        const MESSAGE_QUEUE = 0b00001000;
+        /// Include a justification for the block.
+        const JUSTIFICATION = 0b00010000;
+        /// Include indexed transactions for a block.
+        const INDEXED_BODY = 0b00100000;
+    }
+}
+
+impl BlockAttributes {
+    /// Encodes attributes as big endian u32, compatible with SCALE-encoding (i.e the
+    /// significant byte has zero index).
+    pub fn to_be_u32(&self) -> u32 {
+        u32::from_be_bytes([self.bits(), 0, 0, 0])
+    }
+
+    /// Decodes attributes, encoded with the `encode_to_be_u32()` call.
+    pub fn from_be_u32(encoded: u32) -> Result<Self, Error> {
+        Self::from_bits(encoded.to_be_bytes()[0])
+            .ok_or_else(|| Error::from("Invalid BlockAttributes"))
+    }
+}
+
+impl Encode for BlockAttributes {
+    fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
+        dest.push_byte(self.bits())
+    }
+}
+
+impl codec::EncodeLike for BlockAttributes {}
+
+impl Decode for BlockAttributes {
+    fn decode<I: Input>(input: &mut I) -> Result<Self, Error> {
+        Self::from_bits(input.read_byte()?).ok_or_else(|| Error::from("Invalid bytes"))
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Encode, Decode)]
+/// Block enumeration direction.
+pub enum Direction {
+    /// Enumerate in ascending order (from child to parent).
+    Ascending = 0,
+    /// Enumerate in descending order (from parent to canonical child).
+    Descending = 1,
+}
+
+/// Block state in the chain.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Encode, Decode)]
+pub enum BlockState {
+    /// Block is not part of the best chain.
+    Normal,
+    /// Latest best block.
+    Best,
+}
+
+/// Announcement summary used for debug logging.
+#[derive(Debug)]
+pub struct AnnouncementSummary<H: HeaderT> {
+    pub block_hash: H::Hash,
+    pub number: H::Number,
+    pub parent_hash: H::Hash,
+    pub state: Option<BlockState>,
+}
+
+impl<H: HeaderT> BlockAnnounce<H> {
+    pub fn summary(&self) -> AnnouncementSummary<H> {
+        AnnouncementSummary {
+            block_hash: self.header.hash(),
+            number: *self.header.number(),
+            parent_hash: *self.header.parent_hash(),
+            state: self.state,
+        }
+    }
+}
+
+/// Generic types.
+pub mod generic {
+    use super::{BlockAttributes, BlockState, Direction};
+    use crate::message::RequestId;
+    use codec::{Decode, Encode, Input, Output};
+    use sp_runtime::{EncodedJustification, Justifications};
+
+    /// Block data sent in the response.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub struct BlockData<Header, Hash, Extrinsic> {
+        /// Block header hash.
+        pub hash: Hash,
+        /// Block header if requested.
+        pub header: Option<Header>,
+        /// Block body if requested.
+        pub body: Option<Vec<Extrinsic>>,
+        /// Block body indexed transactions if requested.
+        pub indexed_body: Option<Vec<Vec<u8>>>,
+        /// Block receipt if requested.
+        pub receipt: Option<Vec<u8>>,
+        /// Block message queue if requested.
+        pub message_queue: Option<Vec<u8>>,
+        /// Justification if requested.
+        pub justification: Option<EncodedJustification>,
+        /// Justifications if requested.
+        pub justifications: Option<Justifications>,
+    }
+
+    /// Request block data from a peer.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub struct BlockRequest<Hash, Number> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Bits of block data to request.
+        pub fields: BlockAttributes,
+        /// Start from this block.
+        pub from: FromBlock<Hash, Number>,
+        /// Sequence direction.
+        pub direction: Direction,
+        /// Maximum number of blocks to return. An implementation defined maximum is used when
+        /// unspecified.
+        pub max: Option<u32>,
+    }
+
+    /// Identifies starting point of a block sequence.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub enum FromBlock<Hash, Number> {
+        /// Start with given hash.
+        Hash(Hash),
+        /// Start with given block number.
+        Number(Number),
+    }
+
+    /// Response to `BlockRequest`
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub struct BlockResponse<Header, Hash, Extrinsic> {
+        /// Id of a request this response was made for.
+        pub id: RequestId,
+        /// Block data for the requested sequence.
+        pub blocks: Vec<BlockData<Header, Hash, Extrinsic>>,
+    }
+
+    /// Announce a new complete block on the network.
+    #[derive(Debug, PartialEq, Eq, Clone)]
+    pub struct BlockAnnounce<H> {
+        /// New block header.
+        pub header: H,
+        /// Block state. TODO: Remove `Option` and custom encoding when v4 becomes common.
+        pub state: Option<BlockState>,
+        /// Data associated with this block announcement, e.g. a candidate message.
+        pub data: Option<Vec<u8>>,
+    }
+
+    // Custom Encode/Decode impl to maintain backwards compatibility with v3.
+    // This assumes that the packet contains nothing but the announcement message.
+    // TODO: Get rid of it once protocol v4 is common.
+    impl<H: Encode> Encode for BlockAnnounce<H> {
+        fn encode_to<T: Output + ?Sized>(&self, dest: &mut T) {
+            self.header.encode_to(dest);
+            if let Some(state) = &self.state {
+                state.encode_to(dest);
+            }
+            if let Some(data) = &self.data {
+                data.encode_to(dest)
+            }
+        }
+    }
+
+    impl<H: Decode> Decode for BlockAnnounce<H> {
+        fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
+            let header = H::decode(input)?;
+            let state = BlockState::decode(input).ok();
+            let data = Vec::decode(input).ok();
+            Ok(Self {
+                header,
+                state,
+                data,
+            })
+        }
+    }
+}
+
+/// Handshake sent when we open a block announces substream.
+#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+pub struct BlockAnnouncesHandshake<B: BlockT> {
+    /// Roles of the node.
+    pub roles: Roles,
+    /// Best block number.
+    pub best_number: NumberFor<B>,
+    /// Best block hash.
+    pub best_hash: B::Hash,
+    /// Genesis block hash.
+    pub genesis_hash: B::Hash,
+}
+
+impl<B: BlockT> BlockAnnouncesHandshake<B> {
+    pub fn build(
+        roles: Roles,
+        best_number: NumberFor<B>,
+        best_hash: B::Hash,
+        genesis_hash: B::Hash,
+    ) -> Self {
+        Self {
+            genesis_hash,
+            roles,
+            best_number,
+            best_hash,
+        }
+    }
+}
diff --git a/libp2p/common/src/types.rs b/libp2p/common/src/types.rs
new file mode 100644
index 000000000..92a0da001
--- /dev/null
+++ b/libp2p/common/src/types.rs
@@ -0,0 +1,41 @@
+// 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/>.
+
+/// Description of a reputation adjustment for a node.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ReputationChange {
+    /// Reputation delta.
+    pub value: i32,
+    /// Reason for reputation change.
+    pub reason: &'static str,
+}
+
+impl ReputationChange {
+    /// New reputation change with given delta and reason.
+    pub const fn new(value: i32, reason: &'static str) -> ReputationChange {
+        Self { value, reason }
+    }
+
+    /// New reputation change that forces minimum possible reputation.
+    pub const fn new_fatal(reason: &'static str) -> ReputationChange {
+        Self {
+            value: i32::MIN,
+            reason,
+        }
+    }
+}
diff --git a/libp2p/light/Cargo.toml b/libp2p/light/Cargo.toml
new file mode 100644
index 000000000..2fd96c4b0
--- /dev/null
+++ b/libp2p/light/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+description = "Substrate light network protocol"
+name = "sc-network-light"
+version = "0.33.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-light"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[build-dependencies]
+prost-build = "0.11"
+
+[dependencies]
+async-channel = "1.8.0"
+array-bytes = "6.1"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = [
+	"derive",
+] }
+futures = "0.3.21"
+libp2p-identity = { version = "0.1.3", features = ["peerid"] }
+log = "0.4.16"
+prost = "0.12"
+sp-blockchain = { workspace = true }
+sc-client-api = { workspace = true }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+thiserror = "1.0"
diff --git a/libp2p/light/build.rs b/libp2p/light/build.rs
new file mode 100644
index 000000000..c3037b702
--- /dev/null
+++ b/libp2p/light/build.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+const PROTOS: &[&str] = &["src/schema/light.v1.proto"];
+
+fn main() {
+    prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
+}
diff --git a/libp2p/light/src/lib.rs b/libp2p/light/src/lib.rs
new file mode 100644
index 000000000..48b170d4f
--- /dev/null
+++ b/libp2p/light/src/lib.rs
@@ -0,0 +1,23 @@
+#![allow(clippy::enum_variant_names)]
+// 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/>.
+
+//! Light client data structures of the networking layer.
+
+pub mod light_client_requests;
+mod schema;
diff --git a/libp2p/light/src/light_client_requests.rs b/libp2p/light/src/light_client_requests.rs
new file mode 100644
index 000000000..b66d6e3a9
--- /dev/null
+++ b/libp2p/light/src/light_client_requests.rs
@@ -0,0 +1,63 @@
+// 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/>.
+
+//! Helpers for outgoing and incoming light client requests.
+
+use sc_network::{config::ProtocolId, request_responses::ProtocolConfig};
+
+use std::time::Duration;
+
+/// For incoming light client requests.
+pub mod handler;
+
+/// Generate the light client protocol name from the genesis hash and fork id.
+fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
+    let genesis_hash = genesis_hash.as_ref();
+    if let Some(fork_id) = fork_id {
+        format!(
+            "/{}/{}/light/2",
+            array_bytes::bytes2hex("", genesis_hash),
+            fork_id
+        )
+    } else {
+        format!("/{}/light/2", array_bytes::bytes2hex("", genesis_hash))
+    }
+}
+
+/// Generate the legacy light client protocol name from chain specific protocol identifier.
+fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
+    format!("/{}/light/2", protocol_id.as_ref())
+}
+
+/// Generates a [`ProtocolConfig`] for the light client request protocol, refusing incoming
+/// requests.
+pub fn generate_protocol_config<Hash: AsRef<[u8]>>(
+    protocol_id: &ProtocolId,
+    genesis_hash: Hash,
+    fork_id: Option<&str>,
+) -> ProtocolConfig {
+    ProtocolConfig {
+        name: generate_protocol_name(genesis_hash, fork_id).into(),
+        fallback_names: std::iter::once(generate_legacy_protocol_name(protocol_id).into())
+            .collect(),
+        max_request_size: 1024 * 1024,
+        max_response_size: 16 * 1024 * 1024,
+        request_timeout: Duration::from_secs(15),
+        inbound_queue: None,
+    }
+}
diff --git a/libp2p/light/src/light_client_requests/handler.rs b/libp2p/light/src/light_client_requests/handler.rs
new file mode 100644
index 000000000..745471102
--- /dev/null
+++ b/libp2p/light/src/light_client_requests/handler.rs
@@ -0,0 +1,356 @@
+// 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/>.
+
+//! Helper for incoming light client requests.
+//!
+//! Handle (i.e. answer) incoming light client requests from a remote peer received via
+//! `crate::request_responses::RequestResponsesBehaviour` with
+//! [`LightClientRequestHandler`](handler::LightClientRequestHandler).
+
+use crate::schema;
+use codec::{self, Decode, Encode};
+use futures::prelude::*;
+use libp2p_identity::PeerId;
+use log::{debug, trace};
+use prost::Message;
+use sc_client_api::{BlockBackend, ProofProvider};
+use sc_network::{
+    config::ProtocolId,
+    request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
+    ReputationChange,
+};
+use sp_core::{
+    hexdisplay::HexDisplay,
+    storage::{ChildInfo, ChildType, PrefixedStorageKey},
+};
+use sp_runtime::traits::Block;
+use std::{marker::PhantomData, sync::Arc};
+
+const LOG_TARGET: &str = "light-client-request-handler";
+
+/// Incoming requests bounded queue size. For now due to lack of data on light client request
+/// handling in production systems, this value is chosen to match the block request limit.
+const MAX_LIGHT_REQUEST_QUEUE: usize = 20;
+
+/// Handler for incoming light client requests from a remote peer.
+pub struct LightClientRequestHandler<B, Client> {
+    request_receiver: async_channel::Receiver<IncomingRequest>,
+    /// Blockchain client.
+    client: Arc<Client>,
+    _block: PhantomData<B>,
+}
+
+impl<B, Client> LightClientRequestHandler<B, Client>
+where
+    B: Block,
+    Client: BlockBackend<B> + ProofProvider<B> + Send + Sync + 'static,
+{
+    /// Create a new [`LightClientRequestHandler`].
+    pub fn new(
+        protocol_id: &ProtocolId,
+        fork_id: Option<&str>,
+        client: Arc<Client>,
+    ) -> (Self, ProtocolConfig) {
+        let (tx, request_receiver) = async_channel::bounded(MAX_LIGHT_REQUEST_QUEUE);
+
+        let mut protocol_config = super::generate_protocol_config(
+            protocol_id,
+            client
+                .block_hash(0u32.into())
+                .ok()
+                .flatten()
+                .expect("Genesis block exists; qed"),
+            fork_id,
+        );
+        protocol_config.inbound_queue = Some(tx);
+
+        (
+            Self {
+                client,
+                request_receiver,
+                _block: PhantomData,
+            },
+            protocol_config,
+        )
+    }
+
+    /// Run [`LightClientRequestHandler`].
+    pub async fn run(mut self) {
+        while let Some(request) = self.request_receiver.next().await {
+            let IncomingRequest {
+                peer,
+                payload,
+                pending_response,
+            } = request;
+
+            match self.handle_request(peer, payload) {
+                Ok(response_data) => {
+                    let response = OutgoingResponse {
+                        result: Ok(response_data),
+                        reputation_changes: Vec::new(),
+                        sent_feedback: None,
+                    };
+
+                    match pending_response.send(response) {
+                        Ok(()) => trace!(
+                            target: LOG_TARGET,
+                            "Handled light client request from {}.",
+                            peer,
+                        ),
+                        Err(_) => debug!(
+                            target: LOG_TARGET,
+                            "Failed to handle light client request from {}: {}",
+                            peer,
+                            HandleRequestError::SendResponse,
+                        ),
+                    };
+                }
+                Err(e) => {
+                    debug!(
+                        target: LOG_TARGET,
+                        "Failed to handle light client request from {}: {}", peer, e,
+                    );
+
+                    let reputation_changes = match e {
+                        HandleRequestError::BadRequest(_) => {
+                            vec![ReputationChange::new(-(1 << 12), "bad request")]
+                        }
+                        _ => Vec::new(),
+                    };
+
+                    let response = OutgoingResponse {
+                        result: Err(()),
+                        reputation_changes,
+                        sent_feedback: None,
+                    };
+
+                    if pending_response.send(response).is_err() {
+                        debug!(
+                            target: LOG_TARGET,
+                            "Failed to handle light client request from {}: {}",
+                            peer,
+                            HandleRequestError::SendResponse,
+                        );
+                    };
+                }
+            }
+        }
+    }
+
+    fn handle_request(
+        &mut self,
+        peer: PeerId,
+        payload: Vec<u8>,
+    ) -> Result<Vec<u8>, HandleRequestError> {
+        let request = schema::v1::light::Request::decode(&payload[..])?;
+
+        let response = match &request.request {
+            Some(schema::v1::light::request::Request::RemoteCallRequest(r)) => {
+                self.on_remote_call_request(&peer, r)?
+            }
+            Some(schema::v1::light::request::Request::RemoteReadRequest(r)) => {
+                self.on_remote_read_request(&peer, r)?
+            }
+            Some(schema::v1::light::request::Request::RemoteReadChildRequest(r)) => {
+                self.on_remote_read_child_request(&peer, r)?
+            }
+            None => {
+                return Err(HandleRequestError::BadRequest(
+                    "Remote request without request data.",
+                ))
+            }
+        };
+
+        let mut data = Vec::new();
+        response.encode(&mut data)?;
+
+        Ok(data)
+    }
+
+    fn on_remote_call_request(
+        &mut self,
+        peer: &PeerId,
+        request: &schema::v1::light::RemoteCallRequest,
+    ) -> Result<schema::v1::light::Response, HandleRequestError> {
+        trace!(
+            "Remote call request from {} ({} at {:?}).",
+            peer,
+            request.method,
+            request.block,
+        );
+
+        let block = Decode::decode(&mut request.block.as_ref())?;
+
+        let response = match self
+            .client
+            .execution_proof(block, &request.method, &request.data)
+        {
+            Ok((_, proof)) => schema::v1::light::RemoteCallResponse {
+                proof: Some(proof.encode()),
+            },
+            Err(e) => {
+                trace!(
+                    "remote call request from {} ({} at {:?}) failed with: {}",
+                    peer,
+                    request.method,
+                    request.block,
+                    e,
+                );
+                schema::v1::light::RemoteCallResponse { proof: None }
+            }
+        };
+
+        Ok(schema::v1::light::Response {
+            response: Some(schema::v1::light::response::Response::RemoteCallResponse(
+                response,
+            )),
+        })
+    }
+
+    fn on_remote_read_request(
+        &mut self,
+        peer: &PeerId,
+        request: &schema::v1::light::RemoteReadRequest,
+    ) -> Result<schema::v1::light::Response, HandleRequestError> {
+        if request.keys.is_empty() {
+            debug!("Invalid remote read request sent by {}.", peer);
+            return Err(HandleRequestError::BadRequest(
+                "Remote read request without keys.",
+            ));
+        }
+
+        trace!(
+            "Remote read request from {} ({} at {:?}).",
+            peer,
+            fmt_keys(request.keys.first(), request.keys.last()),
+            request.block,
+        );
+
+        let block = Decode::decode(&mut request.block.as_ref())?;
+
+        let response = match self
+            .client
+            .read_proof(block, &mut request.keys.iter().map(AsRef::as_ref))
+        {
+            Ok(proof) => schema::v1::light::RemoteReadResponse {
+                proof: Some(proof.encode()),
+            },
+            Err(error) => {
+                trace!(
+                    "remote read request from {} ({} at {:?}) failed with: {}",
+                    peer,
+                    fmt_keys(request.keys.first(), request.keys.last()),
+                    request.block,
+                    error,
+                );
+                schema::v1::light::RemoteReadResponse { proof: None }
+            }
+        };
+
+        Ok(schema::v1::light::Response {
+            response: Some(schema::v1::light::response::Response::RemoteReadResponse(
+                response,
+            )),
+        })
+    }
+
+    fn on_remote_read_child_request(
+        &mut self,
+        peer: &PeerId,
+        request: &schema::v1::light::RemoteReadChildRequest,
+    ) -> Result<schema::v1::light::Response, HandleRequestError> {
+        if request.keys.is_empty() {
+            debug!("Invalid remote child read request sent by {}.", peer);
+            return Err(HandleRequestError::BadRequest(
+                "Remove read child request without keys.",
+            ));
+        }
+
+        trace!(
+            "Remote read child request from {} ({} {} at {:?}).",
+            peer,
+            HexDisplay::from(&request.storage_key),
+            fmt_keys(request.keys.first(), request.keys.last()),
+            request.block,
+        );
+
+        let block = Decode::decode(&mut request.block.as_ref())?;
+
+        let prefixed_key = PrefixedStorageKey::new_ref(&request.storage_key);
+        let child_info = match ChildType::from_prefixed_key(prefixed_key) {
+            Some((ChildType::ParentKeyId, storage_key)) => Ok(ChildInfo::new_default(storage_key)),
+            None => Err(sp_blockchain::Error::InvalidChildStorageKey),
+        };
+        let response = match child_info.and_then(|child_info| {
+            self.client.read_child_proof(
+                block,
+                &child_info,
+                &mut request.keys.iter().map(AsRef::as_ref),
+            )
+        }) {
+            Ok(proof) => schema::v1::light::RemoteReadResponse {
+                proof: Some(proof.encode()),
+            },
+            Err(error) => {
+                trace!(
+                    "remote read child request from {} ({} {} at {:?}) failed with: {}",
+                    peer,
+                    HexDisplay::from(&request.storage_key),
+                    fmt_keys(request.keys.first(), request.keys.last()),
+                    request.block,
+                    error,
+                );
+                schema::v1::light::RemoteReadResponse { proof: None }
+            }
+        };
+
+        Ok(schema::v1::light::Response {
+            response: Some(schema::v1::light::response::Response::RemoteReadResponse(
+                response,
+            )),
+        })
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum HandleRequestError {
+    #[error("Failed to decode request: {0}.")]
+    DecodeProto(#[from] prost::DecodeError),
+    #[error("Failed to encode response: {0}.")]
+    EncodeProto(#[from] prost::EncodeError),
+    #[error("Failed to send response.")]
+    SendResponse,
+    /// A bad request has been received.
+    #[error("bad request: {0}")]
+    BadRequest(&'static str),
+    /// Encoding or decoding of some data failed.
+    #[error("codec error: {0}")]
+    Codec(#[from] codec::Error),
+}
+
+fn fmt_keys(first: Option<&Vec<u8>>, last: Option<&Vec<u8>>) -> String {
+    if let (Some(first), Some(last)) = (first, last) {
+        if first == last {
+            HexDisplay::from(first).to_string()
+        } else {
+            format!("{}..{}", HexDisplay::from(first), HexDisplay::from(last))
+        }
+    } else {
+        String::from("n/a")
+    }
+}
diff --git a/libp2p/light/src/schema.rs b/libp2p/light/src/schema.rs
new file mode 100644
index 000000000..3b5a63514
--- /dev/null
+++ b/libp2p/light/src/schema.rs
@@ -0,0 +1,73 @@
+// 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/>.
+
+//! Include sources generated from protobuf definitions.
+
+pub(crate) mod v1 {
+    pub(crate) mod light {
+        include!(concat!(env!("OUT_DIR"), "/api.v1.light.rs"));
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use prost::Message as _;
+
+    #[test]
+    fn empty_proof_encodes_correctly() {
+        let encoded = super::v1::light::Response {
+            response: Some(super::v1::light::response::Response::RemoteReadResponse(
+                super::v1::light::RemoteReadResponse {
+                    proof: Some(Vec::new()),
+                },
+            )),
+        }
+        .encode_to_vec();
+
+        // Make sure that the response contains one field of number 2 and wire type 2 (message),
+        // then another field of number 2 and wire type 2 (bytes), then a length of 0.
+        assert_eq!(encoded, vec![(2 << 3) | 2, 2, (2 << 3) | 2, 0]);
+    }
+
+    #[test]
+    fn no_proof_encodes_correctly() {
+        let encoded = super::v1::light::Response {
+            response: Some(super::v1::light::response::Response::RemoteReadResponse(
+                super::v1::light::RemoteReadResponse { proof: None },
+            )),
+        }
+        .encode_to_vec();
+
+        // Make sure that the response contains one field of number 2 and wire type 2 (message).
+        assert_eq!(encoded, vec![(2 << 3) | 2, 0]);
+    }
+
+    #[test]
+    fn proof_encodes_correctly() {
+        let encoded = super::v1::light::Response {
+            response: Some(super::v1::light::response::Response::RemoteReadResponse(
+                super::v1::light::RemoteReadResponse {
+                    proof: Some(vec![1, 2, 3, 4]),
+                },
+            )),
+        }
+        .encode_to_vec();
+
+        assert_eq!(encoded, vec![(2 << 3) | 2, 6, (2 << 3) | 2, 4, 1, 2, 3, 4]);
+    }
+}
diff --git a/libp2p/light/src/schema/light.v1.proto b/libp2p/light/src/schema/light.v1.proto
new file mode 100644
index 000000000..a269ea73c
--- /dev/null
+++ b/libp2p/light/src/schema/light.v1.proto
@@ -0,0 +1,67 @@
+// Schema definition for light client messages.
+
+syntax = "proto2";
+
+package api.v1.light;
+
+// Enumerate all possible light client request messages.
+message Request {
+	oneof request {
+		RemoteCallRequest remote_call_request = 1;
+		RemoteReadRequest remote_read_request = 2;
+		RemoteReadChildRequest remote_read_child_request = 4;
+		// Note: ids 3 and 5 were used in the past. It would be preferable to not re-use them.
+	}
+}
+
+// Enumerate all possible light client response messages.
+message Response {
+	oneof response {
+		RemoteCallResponse remote_call_response = 1;
+		RemoteReadResponse remote_read_response = 2;
+		// Note: ids 3 and 4 were used in the past. It would be preferable to not re-use them.
+	}
+}
+
+// Remote call request.
+message RemoteCallRequest {
+	// Block at which to perform call.
+	required bytes block = 2;
+	// Method name.
+	required string method = 3;
+	// Call data.
+	required bytes data = 4;
+}
+
+// Remote call response.
+message RemoteCallResponse {
+	// Execution proof. If missing, indicates that the remote couldn't answer, for example because
+	// the block is pruned.
+	optional bytes proof = 2;
+}
+
+// Remote storage read request.
+message RemoteReadRequest {
+	// Block at which to perform call.
+	required bytes block = 2;
+	// Storage keys.
+	repeated bytes keys = 3;
+}
+
+// Remote read response.
+message RemoteReadResponse {
+	// Read proof. If missing, indicates that the remote couldn't answer, for example because
+	// the block is pruned.
+	optional bytes proof = 2;
+}
+
+// Remote storage read child request.
+message RemoteReadChildRequest {
+	// Block at which to perform call.
+	required bytes block = 2;
+	// Child Storage key, this is relative
+	// to the child type storage location.
+	required bytes storage_key = 3;
+	// Storage keys.
+	repeated bytes keys = 6;
+}
diff --git a/libp2p/src/.DS_Store b/libp2p/src/.DS_Store
new file mode 100644
index 000000000..9eea434de
Binary files /dev/null and b/libp2p/src/.DS_Store differ
diff --git a/libp2p/src/behaviour.rs b/libp2p/src/behaviour.rs
new file mode 100644
index 000000000..df71f16f8
--- /dev/null
+++ b/libp2p/src/behaviour.rs
@@ -0,0 +1,394 @@
+// 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::{
+    discovery::{DiscoveryBehaviour, DiscoveryConfig, DiscoveryOut},
+    event::DhtEvent,
+    peer_info,
+    peer_store::PeerStoreHandle,
+    protocol::{CustomMessageOutcome, NotificationsSink, Protocol},
+    protocol_controller::SetId,
+    request_responses::{self, IfDisconnected, ProtocolConfig, RequestFailure},
+    service::traits::Direction,
+    types::ProtocolName,
+    ReputationChange,
+};
+
+use futures::channel::oneshot;
+use libp2p::{
+    core::Multiaddr, identify::Info as IdentifyInfo, identity::PublicKey, kad::RecordKey,
+    swarm::NetworkBehaviour, PeerId,
+};
+
+use parking_lot::Mutex;
+use std::{collections::HashSet, sync::Arc, time::Duration};
+
+pub use crate::request_responses::{InboundFailure, OutboundFailure, ResponseFailure};
+
+/// General behaviour of the network. Combines all protocols together.
+#[derive(NetworkBehaviour)]
+#[behaviour(out_event = "BehaviourOut")]
+pub struct Behaviour {
+    /// All the substrate-specific protocols.
+    substrate: Protocol,
+    /// Periodically pings and identifies the nodes we are connected to, and store information in a
+    /// cache.
+    peer_info: peer_info::PeerInfoBehaviour,
+    /// Discovers nodes of the network.
+    discovery: DiscoveryBehaviour,
+    /// Generic request-response protocols.
+    request_responses: request_responses::RequestResponsesBehaviour,
+}
+
+/// Event generated by `Behaviour`.
+#[allow(clippy::large_enum_variant)]
+pub enum BehaviourOut {
+    /// Started a random iterative Kademlia discovery query.
+    RandomKademliaStarted,
+
+    /// We have received a request from a peer and answered it.
+    ///
+    /// This event is generated for statistics purposes.
+    InboundRequest {
+        /// Peer which sent us a request.
+        peer: PeerId,
+        /// Protocol name of the request.
+        protocol: ProtocolName,
+        /// If `Ok`, contains the time elapsed between when we received the request and when we
+        /// sent back the response. If `Err`, the error that happened.
+        result: Result<Duration, ResponseFailure>,
+    },
+
+    /// A request has succeeded or failed.
+    ///
+    /// This event is generated for statistics purposes.
+    RequestFinished {
+        /// Peer that we send a request to.
+        peer: PeerId,
+        /// Name of the protocol in question.
+        protocol: ProtocolName,
+        /// Duration the request took.
+        duration: Duration,
+        /// Result of the request.
+        result: Result<(), RequestFailure>,
+    },
+
+    /// A request protocol handler issued reputation changes for the given peer.
+    ReputationChanges {
+        peer: PeerId,
+        changes: Vec<ReputationChange>,
+    },
+
+    /// Opened a substream with the given node with the given notifications protocol.
+    ///
+    /// The protocol is always one of the notification protocols that have been registered.
+    NotificationStreamOpened {
+        /// Node we opened the substream with.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+        /// Direction of the stream.
+        direction: Direction,
+        /// If the negotiation didn't use the main name of the protocol (the one in
+        /// `notifications_protocol`), then this field contains which name has actually been
+        /// used.
+        /// See also [`crate::Event::NotificationStreamOpened`].
+        negotiated_fallback: Option<ProtocolName>,
+        /// Object that permits sending notifications to the peer.
+        notifications_sink: NotificationsSink,
+        /// Received handshake.
+        received_handshake: Vec<u8>,
+    },
+
+    /// The [`NotificationsSink`] object used to send notifications with the given peer must be
+    /// replaced with a new one.
+    ///
+    /// This event is typically emitted when a transport-level connection is closed and we fall
+    /// back to a secondary connection.
+    NotificationStreamReplaced {
+        /// Id of the peer we are connected to.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+        /// Replacement for the previous [`NotificationsSink`].
+        notifications_sink: NotificationsSink,
+    },
+
+    /// Closed a substream with the given node. Always matches a corresponding previous
+    /// `NotificationStreamOpened` message.
+    NotificationStreamClosed {
+        /// Node we closed the substream with.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+    },
+
+    /// Received one or more messages from the given node using the given protocol.
+    NotificationsReceived {
+        /// Node we received the message from.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+        /// Concerned protocol and associated message.
+        notification: Vec<u8>,
+    },
+
+    /// We have obtained identity information from a peer, including the addresses it is listening
+    /// on.
+    PeerIdentify {
+        /// Id of the peer that has been identified.
+        peer_id: PeerId,
+        /// Information about the peer.
+        info: IdentifyInfo,
+    },
+
+    /// We have learned about the existence of a node on the default set.
+    Discovered(PeerId),
+
+    /// Events generated by a DHT as a response to get_value or put_value requests as well as the
+    /// request duration.
+    Dht(DhtEvent, Duration),
+
+    /// Ignored event generated by lower layers.
+    None,
+}
+
+impl Behaviour {
+    /// Builds a new `Behaviour`.
+    pub fn new(
+        substrate: Protocol,
+        user_agent: String,
+        local_public_key: PublicKey,
+        disco_config: DiscoveryConfig,
+        request_response_protocols: Vec<ProtocolConfig>,
+        peer_store_handle: PeerStoreHandle,
+        external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+    ) -> Result<Self, request_responses::RegisterError> {
+        Ok(Self {
+            substrate,
+            peer_info: peer_info::PeerInfoBehaviour::new(
+                user_agent,
+                local_public_key,
+                external_addresses,
+            ),
+            discovery: disco_config.finish(),
+            request_responses: request_responses::RequestResponsesBehaviour::new(
+                request_response_protocols.into_iter(),
+                Box::new(peer_store_handle),
+            )?,
+        })
+    }
+
+    /// Returns the list of nodes that we know exist in the network.
+    pub fn known_peers(&mut self) -> HashSet<PeerId> {
+        self.discovery.known_peers()
+    }
+
+    /// Adds a hard-coded address for the given peer, that never expires.
+    pub fn add_known_address(&mut self, peer_id: PeerId, addr: Multiaddr) {
+        self.discovery.add_known_address(peer_id, addr)
+    }
+
+    /// Returns the number of nodes in each Kademlia kbucket.
+    ///
+    /// Identifies kbuckets by the base 2 logarithm of their lower bound.
+    pub fn num_entries_per_kbucket(&mut self) -> Option<Vec<(u32, usize)>> {
+        self.discovery.num_entries_per_kbucket()
+    }
+
+    /// Returns the number of records in the Kademlia record stores.
+    pub fn num_kademlia_records(&mut self) -> Option<usize> {
+        self.discovery.num_kademlia_records()
+    }
+
+    /// Returns the total size in bytes of all the records in the Kademlia record stores.
+    pub fn kademlia_records_total_size(&mut self) -> Option<usize> {
+        self.discovery.kademlia_records_total_size()
+    }
+
+    /// Borrows `self` and returns a struct giving access to the information about a node.
+    ///
+    /// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
+    /// we're connected to, meaning that if `None` is returned then we're not connected to that
+    /// node.
+    pub fn node(&self, peer_id: &PeerId) -> Option<peer_info::Node> {
+        self.peer_info.node(peer_id)
+    }
+
+    /// Initiates sending a request.
+    pub fn send_request(
+        &mut self,
+        target: &PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        self.request_responses.send_request(
+            target,
+            protocol,
+            request,
+            fallback_request,
+            pending_response,
+            connect,
+        )
+    }
+
+    /// Returns a shared reference to the user protocol.
+    pub fn user_protocol(&self) -> &Protocol {
+        &self.substrate
+    }
+
+    /// Returns a mutable reference to the user protocol.
+    pub fn user_protocol_mut(&mut self) -> &mut Protocol {
+        &mut self.substrate
+    }
+
+    /// Add a self-reported address of a remote peer to the k-buckets of the supported
+    /// DHTs (`supported_protocols`).
+    pub fn add_self_reported_address_to_dht(
+        &mut self,
+        peer_id: &PeerId,
+        supported_protocols: &[impl AsRef<[u8]>],
+        addr: Multiaddr,
+    ) {
+        self.discovery
+            .add_self_reported_address(peer_id, supported_protocols, addr);
+    }
+
+    /// Start querying a record from the DHT. Will later produce either a `ValueFound` or a
+    /// `ValueNotFound` event.
+    pub fn get_value(&mut self, key: RecordKey) {
+        self.discovery.get_value(key);
+    }
+
+    /// Starts putting a record into DHT. Will later produce either a `ValuePut` or a
+    /// `ValuePutFailed` event.
+    pub fn put_value(&mut self, key: RecordKey, value: Vec<u8>) {
+        self.discovery.put_value(key, value);
+    }
+}
+
+impl From<CustomMessageOutcome> for BehaviourOut {
+    fn from(event: CustomMessageOutcome) -> Self {
+        match event {
+            CustomMessageOutcome::NotificationStreamOpened {
+                remote,
+                set_id,
+                direction,
+                negotiated_fallback,
+                received_handshake,
+                notifications_sink,
+            } => BehaviourOut::NotificationStreamOpened {
+                remote,
+                set_id,
+                direction,
+                negotiated_fallback,
+                received_handshake,
+                notifications_sink,
+            },
+            CustomMessageOutcome::NotificationStreamReplaced {
+                remote,
+                set_id,
+                notifications_sink,
+            } => BehaviourOut::NotificationStreamReplaced {
+                remote,
+                set_id,
+                notifications_sink,
+            },
+            CustomMessageOutcome::NotificationStreamClosed { remote, set_id } => {
+                BehaviourOut::NotificationStreamClosed { remote, set_id }
+            }
+            CustomMessageOutcome::NotificationsReceived {
+                remote,
+                set_id,
+                notification,
+            } => BehaviourOut::NotificationsReceived {
+                remote,
+                set_id,
+                notification,
+            },
+        }
+    }
+}
+
+impl From<request_responses::Event> for BehaviourOut {
+    fn from(event: request_responses::Event) -> Self {
+        match event {
+            request_responses::Event::InboundRequest {
+                peer,
+                protocol,
+                result,
+            } => BehaviourOut::InboundRequest {
+                peer,
+                protocol,
+                result,
+            },
+            request_responses::Event::RequestFinished {
+                peer,
+                protocol,
+                duration,
+                result,
+            } => BehaviourOut::RequestFinished {
+                peer,
+                protocol,
+                duration,
+                result,
+            },
+            request_responses::Event::ReputationChanges { peer, changes } => {
+                BehaviourOut::ReputationChanges { peer, changes }
+            }
+        }
+    }
+}
+
+impl From<peer_info::PeerInfoEvent> for BehaviourOut {
+    fn from(event: peer_info::PeerInfoEvent) -> Self {
+        let peer_info::PeerInfoEvent::Identified { peer_id, info } = event;
+        BehaviourOut::PeerIdentify { peer_id, info }
+    }
+}
+
+impl From<DiscoveryOut> for BehaviourOut {
+    fn from(event: DiscoveryOut) -> Self {
+        match event {
+            DiscoveryOut::UnroutablePeer(_peer_id) => {
+                // Obtaining and reporting listen addresses for unroutable peers back
+                // to Kademlia is handled by the `Identify` protocol, part of the
+                // `PeerInfoBehaviour`. See the `From<peer_info::PeerInfoEvent>`
+                // implementation.
+                BehaviourOut::None
+            }
+            DiscoveryOut::Discovered(peer_id) => BehaviourOut::Discovered(peer_id),
+            DiscoveryOut::ValueFound(results, duration) => {
+                BehaviourOut::Dht(DhtEvent::ValueFound(results), duration)
+            }
+            DiscoveryOut::ValueNotFound(key, duration) => {
+                BehaviourOut::Dht(DhtEvent::ValueNotFound(key), duration)
+            }
+            DiscoveryOut::ValuePut(key, duration) => {
+                BehaviourOut::Dht(DhtEvent::ValuePut(key), duration)
+            }
+            DiscoveryOut::ValuePutFailed(key, duration) => {
+                BehaviourOut::Dht(DhtEvent::ValuePutFailed(key), duration)
+            }
+            DiscoveryOut::RandomKademliaStarted => BehaviourOut::RandomKademliaStarted,
+        }
+    }
+}
diff --git a/libp2p/src/config.rs b/libp2p/src/config.rs
new file mode 100644
index 000000000..4e08a7e8e
--- /dev/null
+++ b/libp2p/src/config.rs
@@ -0,0 +1,833 @@
+// 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/>.
+
+//! Configuration of the networking layer.
+//!
+//! The [`Params`] struct is the struct that must be passed in order to initialize the networking.
+//! See the documentation of [`Params`].
+
+pub use crate::{
+    discovery::DEFAULT_KADEMLIA_REPLICATION_FACTOR,
+    protocol::{notification_service, NotificationsSink, ProtocolHandlePair},
+    request_responses::{
+        IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig,
+    },
+    service::traits::NotificationService,
+    types::ProtocolName,
+};
+
+pub use libp2p::{
+    build_multiaddr,
+    identity::{self, ed25519, Keypair},
+    multiaddr, Multiaddr, PeerId,
+};
+
+use crate::peer_store::PeerStoreHandle;
+use codec::Encode;
+use prometheus_endpoint::Registry;
+use zeroize::Zeroize;
+
+pub use sc_network_common::{
+    role::{Role, Roles},
+    sync::SyncMode,
+    ExHashT,
+};
+
+use crate::protocol::notifications::service::NotificationHandle;
+use std::{
+    error::Error,
+    fmt, fs,
+    future::Future,
+    io::{self, Write},
+    iter,
+    net::Ipv4Addr,
+    num::NonZeroUsize,
+    path::{Path, PathBuf},
+    pin::Pin,
+    str::{self, FromStr},
+};
+
+/// Protocol name prefix, transmitted on the wire for legacy protocol names.
+/// I.e., `dot` in `/dot/sync/2`. Should be unique for each chain. Always UTF-8.
+/// Deprecated in favour of genesis hash & fork ID based protocol names.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct ProtocolId(smallvec::SmallVec<[u8; 6]>);
+
+impl<'a> From<&'a str> for ProtocolId {
+    fn from(bytes: &'a str) -> ProtocolId {
+        Self(bytes.as_bytes().into())
+    }
+}
+
+impl AsRef<str> for ProtocolId {
+    fn as_ref(&self) -> &str {
+        str::from_utf8(&self.0[..])
+            .expect("the only way to build a ProtocolId is through a UTF-8 String; qed")
+    }
+}
+
+impl fmt::Debug for ProtocolId {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Debug::fmt(self.as_ref(), f)
+    }
+}
+
+/// Parses a string address and splits it into Multiaddress and PeerId, if
+/// valid.
+pub fn parse_str_addr(addr_str: &str) -> Result<(PeerId, Multiaddr), ParseErr> {
+    let addr: Multiaddr = addr_str.parse()?;
+    parse_addr(addr)
+}
+
+/// Splits a Multiaddress into a Multiaddress and PeerId.
+pub fn parse_addr(mut addr: Multiaddr) -> Result<(PeerId, Multiaddr), ParseErr> {
+    let who = match addr.pop() {
+        Some(multiaddr::Protocol::P2p(key)) => {
+            PeerId::from_multihash(key).map_err(|_| ParseErr::InvalidPeerId)?
+        }
+        _ => return Err(ParseErr::PeerIdMissing),
+    };
+
+    Ok((who, addr))
+}
+
+/// Address of a node, including its identity.
+///
+/// This struct represents a decoded version of a multiaddress that ends with `/p2p/<peerid>`.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
+#[serde(try_from = "String", into = "String")]
+pub struct MultiaddrWithPeerId {
+    /// Address of the node.
+    pub multiaddr: Multiaddr,
+    /// Its identity.
+    pub peer_id: PeerId,
+}
+
+impl MultiaddrWithPeerId {
+    /// Concatenates the multiaddress and peer ID into one multiaddress containing both.
+    pub fn concat(&self) -> Multiaddr {
+        let proto = multiaddr::Protocol::P2p(From::from(self.peer_id));
+        self.multiaddr.clone().with(proto)
+    }
+}
+
+impl fmt::Display for MultiaddrWithPeerId {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        fmt::Display::fmt(&self.concat(), f)
+    }
+}
+
+impl FromStr for MultiaddrWithPeerId {
+    type Err = ParseErr;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (peer_id, multiaddr) = parse_str_addr(s)?;
+        Ok(Self { peer_id, multiaddr })
+    }
+}
+
+impl From<MultiaddrWithPeerId> for String {
+    fn from(ma: MultiaddrWithPeerId) -> String {
+        format!("{}", ma)
+    }
+}
+
+impl TryFrom<String> for MultiaddrWithPeerId {
+    type Error = ParseErr;
+    fn try_from(string: String) -> Result<Self, Self::Error> {
+        string.parse()
+    }
+}
+
+/// Error that can be generated by `parse_str_addr`.
+#[derive(Debug)]
+pub enum ParseErr {
+    /// Error while parsing the multiaddress.
+    MultiaddrParse(multiaddr::Error),
+    /// Multihash of the peer ID is invalid.
+    InvalidPeerId,
+    /// The peer ID is missing from the address.
+    PeerIdMissing,
+}
+
+impl fmt::Display for ParseErr {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::MultiaddrParse(err) => write!(f, "{}", err),
+            Self::InvalidPeerId => write!(f, "Peer id at the end of the address is invalid"),
+            Self::PeerIdMissing => write!(f, "Peer id is missing from the address"),
+        }
+    }
+}
+
+impl std::error::Error for ParseErr {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        match self {
+            Self::MultiaddrParse(err) => Some(err),
+            Self::InvalidPeerId => None,
+            Self::PeerIdMissing => None,
+        }
+    }
+}
+
+impl From<multiaddr::Error> for ParseErr {
+    fn from(err: multiaddr::Error) -> ParseErr {
+        Self::MultiaddrParse(err)
+    }
+}
+
+/// Custom handshake for the notification protocol
+#[derive(Debug, Clone)]
+pub struct NotificationHandshake(Vec<u8>);
+
+impl NotificationHandshake {
+    /// Create new `NotificationHandshake` from an object that implements `Encode`
+    pub fn new<H: Encode>(handshake: H) -> Self {
+        Self(handshake.encode())
+    }
+
+    /// Create new `NotificationHandshake` from raw bytes
+    pub fn from_bytes(bytes: Vec<u8>) -> Self {
+        Self(bytes)
+    }
+}
+
+impl std::ops::Deref for NotificationHandshake {
+    type Target = Vec<u8>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+/// Configuration for the transport layer.
+#[derive(Clone, Debug)]
+pub enum TransportConfig {
+    /// Normal transport mode.
+    Normal {
+        /// If true, the network will use mDNS to discover other libp2p nodes on the local network
+        /// and connect to them if they support the same chain.
+        enable_mdns: bool,
+
+        /// If true, allow connecting to private IPv4/IPv6 addresses (as defined in
+        /// [RFC1918](https://tools.ietf.org/html/rfc1918)). Irrelevant for addresses that have
+        /// been passed in `::sc_network::config::NetworkConfiguration::boot_nodes`.
+        allow_private_ip: bool,
+    },
+
+    /// Only allow connections within the same process.
+    /// Only addresses of the form `/memory/...` will be supported.
+    MemoryOnly,
+}
+
+/// The policy for connections to non-reserved peers.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum NonReservedPeerMode {
+    /// Accept them. This is the default.
+    Accept,
+    /// Deny them.
+    Deny,
+}
+
+impl NonReservedPeerMode {
+    /// Attempt to parse the peer mode from a string.
+    pub fn parse(s: &str) -> Option<Self> {
+        match s {
+            "accept" => Some(Self::Accept),
+            "deny" => Some(Self::Deny),
+            _ => None,
+        }
+    }
+
+    /// If we are in "reserved-only" peer mode.
+    pub fn is_reserved_only(&self) -> bool {
+        matches!(self, NonReservedPeerMode::Deny)
+    }
+}
+
+/// The configuration of a node's secret key, describing the type of key
+/// and how it is obtained. A node's identity keypair is the result of
+/// the evaluation of the node key configuration.
+#[derive(Clone, Debug)]
+pub enum NodeKeyConfig {
+    /// A Ed25519 secret key configuration.
+    Ed25519(Secret<ed25519::SecretKey>),
+}
+
+impl Default for NodeKeyConfig {
+    fn default() -> NodeKeyConfig {
+        Self::Ed25519(Secret::New)
+    }
+}
+
+/// The options for obtaining a Ed25519 secret key.
+pub type Ed25519Secret = Secret<ed25519::SecretKey>;
+
+/// The configuration options for obtaining a secret key `K`.
+#[derive(Clone)]
+pub enum Secret<K> {
+    /// Use the given secret key `K`.
+    Input(K),
+    /// Read the secret key from a file. If the file does not exist,
+    /// it is created with a newly generated secret key `K`. The format
+    /// of the file is determined by `K`:
+    ///
+    ///   * `ed25519::SecretKey`: An unencoded 32 bytes Ed25519 secret key.
+    File(PathBuf),
+    /// Always generate a new secret key `K`.
+    New,
+}
+
+impl<K> fmt::Debug for Secret<K> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Input(_) => f.debug_tuple("Secret::Input").finish(),
+            Self::File(path) => f.debug_tuple("Secret::File").field(path).finish(),
+            Self::New => f.debug_tuple("Secret::New").finish(),
+        }
+    }
+}
+
+impl NodeKeyConfig {
+    /// Evaluate a `NodeKeyConfig` to obtain an identity `Keypair`:
+    ///
+    ///  * If the secret is configured as input, the corresponding keypair is returned.
+    ///
+    ///  * If the secret is configured as a file, it is read from that file, if it exists. Otherwise
+    ///    a new secret is generated and stored. In either case, the keypair obtained from the
+    ///    secret is returned.
+    ///
+    ///  * If the secret is configured to be new, it is generated and the corresponding keypair is
+    ///    returned.
+    pub fn into_keypair(self) -> io::Result<Keypair> {
+        use NodeKeyConfig::*;
+        match self {
+            Ed25519(Secret::New) => Ok(Keypair::generate_ed25519()),
+
+            Ed25519(Secret::Input(k)) => Ok(ed25519::Keypair::from(k).into()),
+
+            Ed25519(Secret::File(f)) => get_secret(
+                f,
+                |mut b| match String::from_utf8(b.to_vec()).ok().and_then(|s| {
+                    if s.len() == 64 {
+                        array_bytes::hex2bytes(&s).ok()
+                    } else {
+                        None
+                    }
+                }) {
+                    Some(s) => ed25519::SecretKey::try_from_bytes(s),
+                    _ => ed25519::SecretKey::try_from_bytes(&mut b),
+                },
+                ed25519::SecretKey::generate,
+                |b| b.as_ref().to_vec(),
+            )
+            .map(ed25519::Keypair::from)
+            .map(Keypair::from),
+        }
+    }
+}
+
+/// Load a secret key from a file, if it exists, or generate a
+/// new secret key and write it to that file. In either case,
+/// the secret key is returned.
+fn get_secret<P, F, G, E, W, K>(file: P, parse: F, generate: G, serialize: W) -> io::Result<K>
+where
+    P: AsRef<Path>,
+    F: for<'r> FnOnce(&'r mut [u8]) -> Result<K, E>,
+    G: FnOnce() -> K,
+    E: Error + Send + Sync + 'static,
+    W: Fn(&K) -> Vec<u8>,
+{
+    std::fs::read(&file)
+        .and_then(|mut sk_bytes| {
+            parse(&mut sk_bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
+        })
+        .or_else(|e| {
+            if e.kind() == io::ErrorKind::NotFound {
+                file.as_ref().parent().map_or(Ok(()), fs::create_dir_all)?;
+                let sk = generate();
+                let mut sk_vec = serialize(&sk);
+                write_secret_file(file, &sk_vec)?;
+                sk_vec.zeroize();
+                Ok(sk)
+            } else {
+                Err(e)
+            }
+        })
+}
+
+/// Write secret bytes to a file.
+fn write_secret_file<P>(path: P, sk_bytes: &[u8]) -> io::Result<()>
+where
+    P: AsRef<Path>,
+{
+    let mut file = open_secret_file(&path)?;
+    file.write_all(sk_bytes)
+}
+
+/// Opens a file containing a secret key in write mode.
+#[cfg(unix)]
+fn open_secret_file<P>(path: P) -> io::Result<fs::File>
+where
+    P: AsRef<Path>,
+{
+    use std::os::unix::fs::OpenOptionsExt;
+    fs::OpenOptions::new()
+        .write(true)
+        .create_new(true)
+        .mode(0o600)
+        .open(path)
+}
+
+/// Opens a file containing a secret key in write mode.
+#[cfg(not(unix))]
+fn open_secret_file<P>(path: P) -> Result<fs::File, io::Error>
+where
+    P: AsRef<Path>,
+{
+    fs::OpenOptions::new()
+        .write(true)
+        .create_new(true)
+        .open(path)
+}
+
+/// Configuration for a set of nodes.
+#[derive(Clone, Debug)]
+pub struct SetConfig {
+    /// Maximum allowed number of incoming substreams related to this set.
+    pub in_peers: u32,
+
+    /// Number of outgoing substreams related to this set that we're trying to maintain.
+    pub out_peers: u32,
+
+    /// List of reserved node addresses.
+    pub reserved_nodes: Vec<MultiaddrWithPeerId>,
+
+    /// Whether nodes that aren't in [`SetConfig::reserved_nodes`] are accepted or automatically
+    /// refused.
+    pub non_reserved_mode: NonReservedPeerMode,
+}
+
+impl Default for SetConfig {
+    fn default() -> Self {
+        Self {
+            in_peers: 25,
+            out_peers: 75,
+            reserved_nodes: Vec::new(),
+            non_reserved_mode: NonReservedPeerMode::Accept,
+        }
+    }
+}
+
+/// Extension to [`SetConfig`] for sets that aren't the default set.
+///
+/// > **Note**: As new fields might be added in the future, please consider using the `new` method
+/// >        and modifiers instead of creating this struct manually.
+#[derive(Debug)]
+pub struct NonDefaultSetConfig {
+    /// Name of the notifications protocols of this set. A substream on this set will be
+    /// considered established once this protocol is open.
+    ///
+    /// > **Note**: This field isn't present for the default set, as this is handled internally
+    /// > by the networking code.
+    protocol_name: ProtocolName,
+
+    /// If the remote reports that it doesn't support the protocol indicated in the
+    /// `notifications_protocol` field, then each of these fallback names will be tried one by
+    /// one.
+    ///
+    /// If a fallback is used, it will be reported in
+    /// `sc_network::protocol::event::Event::NotificationStreamOpened::negotiated_fallback`
+    fallback_names: Vec<ProtocolName>,
+
+    /// Handshake of the protocol
+    ///
+    /// NOTE: Currently custom handshakes are not fully supported. See issue #5685 for more
+    /// details. This field is temporarily used to allow moving the hardcoded block announcement
+    /// protocol out of `protocol.rs`.
+    handshake: Option<NotificationHandshake>,
+
+    /// Maximum allowed size of single notifications.
+    max_notification_size: u64,
+
+    /// Base configuration.
+    set_config: SetConfig,
+
+    /// Notification handle.
+    ///
+    /// Notification handle is created during `NonDefaultSetConfig` creation and its other half,
+    /// `Box<dyn NotificationService>` is given to the protocol created the config and
+    /// `ProtocolHandle` is given to `Notifications` when it initializes itself. This handle allows
+    /// `Notifications ` to communicate with the protocol directly without relaying events through
+    /// `sc-network.`
+    protocol_handle_pair: ProtocolHandlePair,
+}
+
+impl NonDefaultSetConfig {
+    /// Creates a new [`NonDefaultSetConfig`]. Zero slots and accepts only reserved nodes.
+    /// Also returns an object which allows the protocol to communicate with `Notifications`.
+    pub fn new(
+        protocol_name: ProtocolName,
+        fallback_names: Vec<ProtocolName>,
+        max_notification_size: u64,
+        handshake: Option<NotificationHandshake>,
+        set_config: SetConfig,
+    ) -> (Self, NotificationHandle) {
+        let (protocol_handle_pair, notification_service) =
+            notification_service(protocol_name.clone());
+        (
+            Self {
+                protocol_name,
+                max_notification_size,
+                fallback_names,
+                handshake,
+                set_config,
+                protocol_handle_pair,
+            },
+            notification_service,
+        )
+    }
+
+    /// Get reference to protocol name.
+    pub fn protocol_name(&self) -> &ProtocolName {
+        &self.protocol_name
+    }
+
+    /// Get reference to fallback protocol names.
+    pub fn fallback_names(&self) -> impl Iterator<Item = &ProtocolName> {
+        self.fallback_names.iter()
+    }
+
+    /// Get reference to handshake.
+    pub fn handshake(&self) -> &Option<NotificationHandshake> {
+        &self.handshake
+    }
+
+    /// Get maximum notification size.
+    pub fn max_notification_size(&self) -> u64 {
+        self.max_notification_size
+    }
+
+    /// Get reference to `SetConfig`.
+    pub fn set_config(&self) -> &SetConfig {
+        &self.set_config
+    }
+
+    /// Take `ProtocolHandlePair` from `NonDefaultSetConfig`
+    pub fn take_protocol_handle(self) -> ProtocolHandlePair {
+        self.protocol_handle_pair
+    }
+
+    /// Modifies the configuration to allow non-reserved nodes.
+    pub fn allow_non_reserved(&mut self, in_peers: u32, out_peers: u32) {
+        self.set_config.in_peers = in_peers;
+        self.set_config.out_peers = out_peers;
+        self.set_config.non_reserved_mode = NonReservedPeerMode::Accept;
+    }
+
+    /// Add a node to the list of reserved nodes.
+    pub fn add_reserved(&mut self, peer: MultiaddrWithPeerId) {
+        self.set_config.reserved_nodes.push(peer);
+    }
+
+    /// Add a list of protocol names used for backward compatibility.
+    ///
+    /// See the explanations in [`NonDefaultSetConfig::fallback_names`].
+    pub fn add_fallback_names(&mut self, fallback_names: Vec<ProtocolName>) {
+        self.fallback_names.extend(fallback_names);
+    }
+}
+
+/// Network service configuration.
+#[derive(Clone, Debug)]
+pub struct NetworkConfiguration {
+    /// Directory path to store network-specific configuration. None means nothing will be saved.
+    pub net_config_path: Option<PathBuf>,
+
+    /// Multiaddresses to listen for incoming connections.
+    pub listen_addresses: Vec<Multiaddr>,
+
+    /// Multiaddresses to advertise. Detected automatically if empty.
+    pub public_addresses: Vec<Multiaddr>,
+
+    /// List of initial node addresses
+    pub boot_nodes: Vec<MultiaddrWithPeerId>,
+
+    /// The node key configuration, which determines the node's network identity keypair.
+    pub node_key: NodeKeyConfig,
+
+    /// Configuration for the default set of nodes used for block syncing and transactions.
+    pub default_peers_set: SetConfig,
+
+    /// Number of substreams to reserve for full nodes for block syncing and transactions.
+    /// Any other slot will be dedicated to light nodes.
+    ///
+    /// This value is implicitly capped to `default_set.out_peers + default_set.in_peers`.
+    pub default_peers_set_num_full: u32,
+
+    /// Client identifier. Sent over the wire for debugging purposes.
+    pub client_version: String,
+
+    /// Name of the node. Sent over the wire for debugging purposes.
+    pub node_name: String,
+
+    /// Configuration for the transport layer.
+    pub transport: TransportConfig,
+
+    /// Maximum number of peers to ask the same blocks in parallel.
+    pub max_parallel_downloads: u32,
+
+    /// Maximum number of blocks per request.
+    pub max_blocks_per_request: u32,
+
+    /// Initial syncing mode.
+    pub sync_mode: SyncMode,
+
+    /// True if Kademlia random discovery should be enabled.
+    ///
+    /// If true, the node will automatically randomly walk the DHT in order to find new peers.
+    pub enable_dht_random_walk: bool,
+
+    /// Should we insert non-global addresses into the DHT?
+    pub allow_non_globals_in_dht: bool,
+
+    /// Require iterative Kademlia DHT queries to use disjoint paths for increased resiliency in
+    /// the presence of potentially adversarial nodes.
+    pub kademlia_disjoint_query_paths: bool,
+
+    /// Kademlia replication factor determines to how many closest peers a record is replicated to.
+    ///
+    /// Discovery mechanism requires successful replication to all
+    /// `kademlia_replication_factor` peers to consider record successfully put.
+    pub kademlia_replication_factor: NonZeroUsize,
+
+    /// Enable serving block data over IPFS bitswap.
+    pub ipfs_server: bool,
+
+    /// Size of Yamux receive window of all substreams. `None` for the default (256kiB).
+    /// Any value less than 256kiB is invalid.
+    ///
+    /// # Context
+    ///
+    /// By design, notifications substreams on top of Yamux connections only allow up to `N` bytes
+    /// to be transferred at a time, where `N` is the Yamux receive window size configurable here.
+    /// This means, in practice, that every `N` bytes must be acknowledged by the receiver before
+    /// the sender can send more data. The maximum bandwidth of each notifications substream is
+    /// therefore `N / round_trip_time`.
+    ///
+    /// It is recommended to leave this to `None`, and use a request-response protocol instead if
+    /// a large amount of data must be transferred. The reason why the value is configurable is
+    /// that some Substrate users mis-use notification protocols to send large amounts of data.
+    /// As such, this option isn't designed to stay and will likely get removed in the future.
+    ///
+    /// Note that configuring a value here isn't a modification of the Yamux protocol, but rather
+    /// a modification of the way the implementation works. Different nodes with different
+    /// configured values remain compatible with each other.
+    pub yamux_window_size: Option<u32>,
+}
+
+impl NetworkConfiguration {
+    /// Create new default configuration
+    pub fn new<SN: Into<String>, SV: Into<String>>(
+        node_name: SN,
+        client_version: SV,
+        node_key: NodeKeyConfig,
+        net_config_path: Option<PathBuf>,
+    ) -> Self {
+        let default_peers_set = SetConfig::default();
+        Self {
+            net_config_path,
+            listen_addresses: Vec::new(),
+            public_addresses: Vec::new(),
+            boot_nodes: Vec::new(),
+            node_key,
+            default_peers_set_num_full: default_peers_set.in_peers + default_peers_set.out_peers,
+            default_peers_set,
+            client_version: client_version.into(),
+            node_name: node_name.into(),
+            transport: TransportConfig::Normal {
+                enable_mdns: false,
+                allow_private_ip: true,
+            },
+            max_parallel_downloads: 5,
+            max_blocks_per_request: 64,
+            sync_mode: SyncMode::Full,
+            enable_dht_random_walk: true,
+            allow_non_globals_in_dht: false,
+            kademlia_disjoint_query_paths: false,
+            kademlia_replication_factor: NonZeroUsize::new(DEFAULT_KADEMLIA_REPLICATION_FACTOR)
+                .expect("value is a constant; constant is non-zero; qed."),
+            yamux_window_size: None,
+            ipfs_server: false,
+        }
+    }
+
+    /// Create new default configuration for localhost-only connection with random port (useful for
+    /// testing)
+    pub fn new_local() -> NetworkConfiguration {
+        let mut config =
+            NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
+
+        config.listen_addresses =
+            vec![
+                iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
+                    .chain(iter::once(multiaddr::Protocol::Tcp(0)))
+                    .collect(),
+            ];
+
+        config.allow_non_globals_in_dht = true;
+        config
+    }
+
+    /// Create new default configuration for localhost-only connection with random port (useful for
+    /// testing)
+    pub fn new_memory() -> NetworkConfiguration {
+        let mut config =
+            NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
+
+        config.listen_addresses =
+            vec![
+                iter::once(multiaddr::Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1)))
+                    .chain(iter::once(multiaddr::Protocol::Tcp(0)))
+                    .collect(),
+            ];
+
+        config.allow_non_globals_in_dht = true;
+        config
+    }
+}
+
+/// Network initialization parameters.
+pub struct Params {
+    /// Assigned role for our node (full, light, ...).
+    pub role: Role,
+
+    /// How to spawn background tasks.
+    pub executor: Box<ExecutorT>,
+
+    /// Network layer configuration.
+    pub network_config: FullNetworkConfiguration,
+
+    /// Peer store with known nodes, peer reputations, etc.
+    pub peer_store: PeerStoreHandle,
+
+    /// Legacy name of the protocol to use on the wire. Should be different for each chain.
+    pub protocol_id: ProtocolId,
+
+    /// Genesis hash of the chain
+    pub genesis_hash: [u8; 32],
+
+    /// Fork ID to distinguish protocols of different hard forks. Part of the standard protocol
+    /// name on the wire.
+    pub fork_id: Option<String>,
+
+    /// Registry for recording prometheus metrics to.
+    pub metrics_registry: Option<Registry>,
+}
+
+/// Alias for the execution of a future
+pub type ExecutorT = dyn Fn(Pin<Box<dyn Future<Output = ()> + Send>>) + Send;
+
+/// Full network configuration.
+pub struct FullNetworkConfiguration {
+    /// Installed notification protocols.
+    pub(crate) notification_protocols: Vec<NonDefaultSetConfig>,
+
+    /// List of request-response protocols that the node supports.
+    pub(crate) request_response_protocols: Vec<RequestResponseConfig>,
+
+    /// Network configuration.
+    pub network_config: NetworkConfiguration,
+}
+
+impl FullNetworkConfiguration {
+    /// Create new [`FullNetworkConfiguration`].
+    pub fn new(network_config: &NetworkConfiguration) -> Self {
+        Self {
+            notification_protocols: Vec::new(),
+            request_response_protocols: Vec::new(),
+            network_config: network_config.clone(),
+        }
+    }
+
+    /// Add a notification protocol.
+    pub fn add_notification_protocol(&mut self, config: NonDefaultSetConfig) {
+        self.notification_protocols.push(config);
+    }
+
+    /// Get reference to installed notification protocols.
+    pub fn notification_protocols(&self) -> &Vec<NonDefaultSetConfig> {
+        &self.notification_protocols
+    }
+
+    /// Add a request-response protocol.
+    pub fn add_request_response_protocol(&mut self, config: RequestResponseConfig) {
+        self.request_response_protocols.push(config);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::TempDir;
+
+    fn tempdir_with_prefix(prefix: &str) -> TempDir {
+        tempfile::Builder::new().prefix(prefix).tempdir().unwrap()
+    }
+
+    fn secret_bytes(kp: Keypair) -> Vec<u8> {
+        kp.try_into_ed25519()
+            .expect("ed25519 keypair")
+            .secret()
+            .as_ref()
+            .to_vec()
+    }
+
+    #[test]
+    fn test_secret_file() {
+        let tmp = tempdir_with_prefix("x");
+        std::fs::remove_dir(tmp.path()).unwrap(); // should be recreated
+        let file = tmp.path().join("x").to_path_buf();
+        let kp1 = NodeKeyConfig::Ed25519(Secret::File(file.clone()))
+            .into_keypair()
+            .unwrap();
+        let kp2 = NodeKeyConfig::Ed25519(Secret::File(file.clone()))
+            .into_keypair()
+            .unwrap();
+        assert!(file.is_file() && secret_bytes(kp1) == secret_bytes(kp2))
+    }
+
+    #[test]
+    fn test_secret_input() {
+        let sk = ed25519::SecretKey::generate();
+        let kp1 = NodeKeyConfig::Ed25519(Secret::Input(sk.clone()))
+            .into_keypair()
+            .unwrap();
+        let kp2 = NodeKeyConfig::Ed25519(Secret::Input(sk))
+            .into_keypair()
+            .unwrap();
+        assert!(secret_bytes(kp1) == secret_bytes(kp2));
+    }
+
+    #[test]
+    fn test_secret_new() {
+        let kp1 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
+        let kp2 = NodeKeyConfig::Ed25519(Secret::New).into_keypair().unwrap();
+        assert!(secret_bytes(kp1) != secret_bytes(kp2));
+    }
+}
diff --git a/libp2p/src/discovery.rs b/libp2p/src/discovery.rs
new file mode 100644
index 000000000..706c3e685
--- /dev/null
+++ b/libp2p/src/discovery.rs
@@ -0,0 +1,1244 @@
+// 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/>.
+
+//! Discovery mechanisms of Substrate.
+//!
+//! The `DiscoveryBehaviour` struct implements the `NetworkBehaviour` trait of libp2p and is
+//! responsible for discovering other nodes that are part of the network.
+//!
+//! Substrate uses the following mechanisms in order to discover nodes that are part of the network:
+//!
+//! - Bootstrap nodes. These are hard-coded node identities and addresses passed in the constructor
+//! of the `DiscoveryBehaviour`. You can also call `add_known_address` later to add an entry.
+//!
+//! - mDNS. Discovers nodes on the local network by broadcasting UDP packets.
+//!
+//! - Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
+//! configured Kademlia DHTs in order for nodes to propagate to us their view of the network. This
+//! is performed automatically by the `DiscoveryBehaviour`.
+//!
+//! Additionally, the `DiscoveryBehaviour` is also capable of storing and loading value in the
+//! configured DHTs.
+//!
+//! ## Usage
+//!
+//! The `DiscoveryBehaviour` generates events of type `DiscoveryOut`, most notably
+//! `DiscoveryOut::Discovered` that is generated whenever we discover a node.
+//! Only the identity of the node is returned. The node's addresses are stored within the
+//! `DiscoveryBehaviour` and can be queried through the `NetworkBehaviour` trait.
+//!
+//! **Important**: In order for the discovery mechanism to work properly, there needs to be an
+//! active mechanism that asks nodes for the addresses they are listening on. Whenever we learn
+//! of a node's address, you must call `add_self_reported_address`.
+
+use crate::{config::ProtocolId, utils::LruHashSet};
+
+use array_bytes::bytes2hex;
+use futures::prelude::*;
+use futures_timer::Delay;
+use ip_network::IpNetwork;
+use libp2p::{
+    core::{Endpoint, Multiaddr},
+    kad::{
+        handler::KademliaHandler,
+        record::store::{MemoryStore, RecordStore},
+        GetClosestPeersError, GetRecordOk, Kademlia, KademliaBucketInserts, KademliaConfig,
+        KademliaEvent, QueryId, QueryResult, Quorum, Record, RecordKey,
+    },
+    mdns::{self, tokio::Behaviour as TokioMdns},
+    multiaddr::Protocol,
+    swarm::{
+        behaviour::{
+            toggle::{Toggle, ToggleConnectionHandler},
+            DialFailure, FromSwarm, NewExternalAddr,
+        },
+        ConnectionDenied, ConnectionId, DialError, NetworkBehaviour, PollParameters, THandler,
+        THandlerInEvent, THandlerOutEvent, ToSwarm,
+    },
+    PeerId,
+};
+use log::{debug, info, trace, warn};
+use sp_core::hexdisplay::HexDisplay;
+use std::{
+    cmp,
+    collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
+    num::NonZeroUsize,
+    task::{Context, Poll},
+    time::Duration,
+};
+
+/// Maximum number of known external addresses that we will cache.
+/// This only affects whether we will log whenever we (re-)discover
+/// a given address.
+const MAX_KNOWN_EXTERNAL_ADDRESSES: usize = 32;
+
+/// Default value for Kademlia replication factor which  determines to how many closest peers a
+/// record is replicated to.
+pub const DEFAULT_KADEMLIA_REPLICATION_FACTOR: usize = 20;
+
+/// `DiscoveryBehaviour` configuration.
+///
+/// Note: In order to discover nodes or load and store values via Kademlia one has to add
+///       Kademlia protocol via [`DiscoveryConfig::with_kademlia`].
+pub struct DiscoveryConfig {
+    local_peer_id: PeerId,
+    permanent_addresses: Vec<(PeerId, Multiaddr)>,
+    dht_random_walk: bool,
+    allow_private_ip: bool,
+    allow_non_globals_in_dht: bool,
+    discovery_only_if_under_num: u64,
+    enable_mdns: bool,
+    kademlia_disjoint_query_paths: bool,
+    kademlia_protocols: Vec<Vec<u8>>,
+    kademlia_replication_factor: NonZeroUsize,
+}
+
+impl DiscoveryConfig {
+    /// Create a default configuration with the given public key.
+    pub fn new(local_peer_id: PeerId) -> Self {
+        Self {
+            local_peer_id,
+            permanent_addresses: Vec::new(),
+            dht_random_walk: true,
+            allow_private_ip: true,
+            allow_non_globals_in_dht: false,
+            discovery_only_if_under_num: std::u64::MAX,
+            enable_mdns: false,
+            kademlia_disjoint_query_paths: false,
+            kademlia_protocols: Vec::new(),
+            kademlia_replication_factor: NonZeroUsize::new(DEFAULT_KADEMLIA_REPLICATION_FACTOR)
+                .expect("value is a constant; constant is non-zero; qed."),
+        }
+    }
+
+    /// Set the number of active connections at which we pause discovery.
+    pub fn discovery_limit(&mut self, limit: u64) -> &mut Self {
+        self.discovery_only_if_under_num = limit;
+        self
+    }
+
+    /// Set custom nodes which never expire, e.g. bootstrap or reserved nodes.
+    pub fn with_permanent_addresses<I>(&mut self, permanent_addresses: I) -> &mut Self
+    where
+        I: IntoIterator<Item = (PeerId, Multiaddr)>,
+    {
+        self.permanent_addresses.extend(permanent_addresses);
+        self
+    }
+
+    /// Whether the discovery behaviour should periodically perform a random
+    /// walk on the DHT to discover peers.
+    pub fn with_dht_random_walk(&mut self, value: bool) -> &mut Self {
+        self.dht_random_walk = value;
+        self
+    }
+
+    /// Should private IPv4/IPv6 addresses be reported?
+    pub fn allow_private_ip(&mut self, value: bool) -> &mut Self {
+        self.allow_private_ip = value;
+        self
+    }
+
+    /// Should non-global addresses be inserted to the DHT?
+    pub fn allow_non_globals_in_dht(&mut self, value: bool) -> &mut Self {
+        self.allow_non_globals_in_dht = value;
+        self
+    }
+
+    /// Should MDNS discovery be supported?
+    pub fn with_mdns(&mut self, value: bool) -> &mut Self {
+        self.enable_mdns = value;
+        self
+    }
+
+    /// Add discovery via Kademlia for the given protocol.
+    ///
+    /// Currently accepts `protocol_id`. This should be removed once all the nodes
+    /// are upgraded to genesis hash- and fork ID-based Kademlia protocol name.
+    pub fn with_kademlia<Hash: AsRef<[u8]>>(
+        &mut self,
+        genesis_hash: Hash,
+        fork_id: Option<&str>,
+        protocol_id: &ProtocolId,
+    ) -> &mut Self {
+        self.kademlia_protocols = Vec::new();
+        self.kademlia_protocols
+            .push(kademlia_protocol_name(genesis_hash, fork_id));
+        self.kademlia_protocols
+            .push(legacy_kademlia_protocol_name(protocol_id));
+        self
+    }
+
+    /// Require iterative Kademlia DHT queries to use disjoint paths for increased resiliency in the
+    /// presence of potentially adversarial nodes.
+    pub fn use_kademlia_disjoint_query_paths(&mut self, value: bool) -> &mut Self {
+        self.kademlia_disjoint_query_paths = value;
+        self
+    }
+
+    /// Sets Kademlia replication factor.
+    pub fn with_kademlia_replication_factor(&mut self, value: NonZeroUsize) -> &mut Self {
+        self.kademlia_replication_factor = value;
+        self
+    }
+
+    /// Create a `DiscoveryBehaviour` from this config.
+    pub fn finish(self) -> DiscoveryBehaviour {
+        let Self {
+            local_peer_id,
+            permanent_addresses,
+            dht_random_walk,
+            allow_private_ip,
+            allow_non_globals_in_dht,
+            discovery_only_if_under_num,
+            enable_mdns,
+            kademlia_disjoint_query_paths,
+            kademlia_protocols,
+            kademlia_replication_factor,
+        } = self;
+
+        let kademlia = if !kademlia_protocols.is_empty() {
+            let mut config = KademliaConfig::default();
+
+            config.set_replication_factor(kademlia_replication_factor);
+            config.set_protocol_names(kademlia_protocols.into_iter().map(Into::into).collect());
+            // By default Kademlia attempts to insert all peers into its routing table once a
+            // dialing attempt succeeds. In order to control which peer is added, disable the
+            // auto-insertion and instead add peers manually.
+            config.set_kbucket_inserts(KademliaBucketInserts::Manual);
+            config.disjoint_query_paths(kademlia_disjoint_query_paths);
+
+            let store = MemoryStore::new(local_peer_id);
+            let mut kad = Kademlia::with_config(local_peer_id, store, config);
+
+            for (peer_id, addr) in &permanent_addresses {
+                kad.add_address(peer_id, addr.clone());
+            }
+
+            Some(kad)
+        } else {
+            None
+        };
+
+        DiscoveryBehaviour {
+            permanent_addresses,
+            ephemeral_addresses: HashMap::new(),
+            kademlia: Toggle::from(kademlia),
+            next_kad_random_query: if dht_random_walk {
+                Some(Delay::new(Duration::new(0, 0)))
+            } else {
+                None
+            },
+            duration_to_next_kad: Duration::from_secs(1),
+            pending_events: VecDeque::new(),
+            local_peer_id,
+            num_connections: 0,
+            allow_private_ip,
+            discovery_only_if_under_num,
+            mdns: if enable_mdns {
+                match TokioMdns::new(mdns::Config::default(), local_peer_id) {
+                    Ok(mdns) => Toggle::from(Some(mdns)),
+                    Err(err) => {
+                        warn!(target: "sub-libp2p", "Failed to initialize mDNS: {:?}", err);
+                        Toggle::from(None)
+                    }
+                }
+            } else {
+                Toggle::from(None)
+            },
+            allow_non_globals_in_dht,
+            known_external_addresses: LruHashSet::new(
+                NonZeroUsize::new(MAX_KNOWN_EXTERNAL_ADDRESSES)
+                    .expect("value is a constant; constant is non-zero; qed."),
+            ),
+            records_to_publish: Default::default(),
+        }
+    }
+}
+
+/// Implementation of `NetworkBehaviour` that discovers the nodes on the network.
+pub struct DiscoveryBehaviour {
+    /// User-defined list of nodes and their addresses. Typically includes bootstrap nodes and
+    /// reserved nodes.
+    permanent_addresses: Vec<(PeerId, Multiaddr)>,
+    /// Same as `permanent_addresses`, except that addresses that fail to reach a peer are
+    /// removed.
+    ephemeral_addresses: HashMap<PeerId, Vec<Multiaddr>>,
+    /// Kademlia requests and answers. Even though it's wrapped in `Toggle`, currently
+    /// it's always enabled in `NetworkWorker::new()`.
+    kademlia: Toggle<Kademlia<MemoryStore>>,
+    /// Discovers nodes on the local network.
+    mdns: Toggle<TokioMdns>,
+    /// Stream that fires when we need to perform the next random Kademlia query. `None` if
+    /// random walking is disabled.
+    next_kad_random_query: Option<Delay>,
+    /// After `next_kad_random_query` triggers, the next one triggers after this duration.
+    duration_to_next_kad: Duration,
+    /// Events to return in priority when polled.
+    pending_events: VecDeque<DiscoveryOut>,
+    /// Identity of our local node.
+    local_peer_id: PeerId,
+    /// Number of nodes we're currently connected to.
+    num_connections: u64,
+    /// If false, `addresses_of_peer` won't return any private IPv4/IPv6 address, except for the
+    /// ones stored in `permanent_addresses` or `ephemeral_addresses`.
+    allow_private_ip: bool,
+    /// Number of active connections over which we interrupt the discovery process.
+    discovery_only_if_under_num: u64,
+    /// Should non-global addresses be added to the DHT?
+    allow_non_globals_in_dht: bool,
+    /// A cache of discovered external addresses. Only used for logging purposes.
+    known_external_addresses: LruHashSet<Multiaddr>,
+    /// Records to publish per QueryId.
+    ///
+    /// After finishing a Kademlia query, libp2p will return us a list of the closest peers that
+    /// did not return the record(in `FinishedWithNoAdditionalRecord`). We will then put the record
+    /// to these peers.
+    records_to_publish: HashMap<QueryId, Record>,
+}
+
+impl DiscoveryBehaviour {
+    /// Returns the list of nodes that we know exist in the network.
+    pub fn known_peers(&mut self) -> HashSet<PeerId> {
+        let mut peers = HashSet::new();
+        if let Some(k) = self.kademlia.as_mut() {
+            for b in k.kbuckets() {
+                for e in b.iter() {
+                    if !peers.contains(e.node.key.preimage()) {
+                        peers.insert(*e.node.key.preimage());
+                    }
+                }
+            }
+        }
+        peers
+    }
+
+    /// Adds a hard-coded address for the given peer, that never expires.
+    ///
+    /// This adds an entry to the parameter that was passed to `new`.
+    ///
+    /// If we didn't know this address before, also generates a `Discovered` event.
+    pub fn add_known_address(&mut self, peer_id: PeerId, addr: Multiaddr) {
+        let addrs_list = self.ephemeral_addresses.entry(peer_id).or_default();
+        if addrs_list.contains(&addr) {
+            return;
+        }
+
+        if let Some(k) = self.kademlia.as_mut() {
+            k.add_address(&peer_id, addr.clone());
+        }
+
+        self.pending_events
+            .push_back(DiscoveryOut::Discovered(peer_id));
+        addrs_list.push(addr);
+    }
+
+    /// Add a self-reported address of a remote peer to the k-buckets of the DHT
+    /// if it has compatible `supported_protocols`.
+    ///
+    /// **Note**: It is important that you call this method. The discovery mechanism will not
+    /// automatically add connecting peers to the Kademlia k-buckets.
+    pub fn add_self_reported_address(
+        &mut self,
+        peer_id: &PeerId,
+        supported_protocols: &[impl AsRef<[u8]>],
+        addr: Multiaddr,
+    ) {
+        if let Some(kademlia) = self.kademlia.as_mut() {
+            if !self.allow_non_globals_in_dht && !Self::can_add_to_dht(&addr) {
+                trace!(
+                    target: "sub-libp2p",
+                    "Ignoring self-reported non-global address {} from {}.", addr, peer_id
+                );
+                return;
+            }
+
+            if let Some(matching_protocol) = supported_protocols.iter().find(|p| {
+                kademlia
+                    .protocol_names()
+                    .iter()
+                    .any(|k| k.as_ref() == p.as_ref())
+            }) {
+                trace!(
+                    target: "sub-libp2p",
+                    "Adding self-reported address {} from {} to Kademlia DHT {}.",
+                    addr, peer_id, String::from_utf8_lossy(matching_protocol.as_ref()),
+                );
+                kademlia.add_address(peer_id, addr.clone());
+            } else {
+                trace!(
+                    target: "sub-libp2p",
+                    "Ignoring self-reported address {} from {} as remote node is not part of the \
+                     Kademlia DHT supported by the local node.", addr, peer_id,
+                );
+            }
+        }
+    }
+
+    /// Start fetching a record from the DHT.
+    ///
+    /// A corresponding `ValueFound` or `ValueNotFound` event will later be generated.
+    pub fn get_value(&mut self, key: RecordKey) {
+        if let Some(k) = self.kademlia.as_mut() {
+            k.get_record(key.clone());
+        }
+    }
+
+    /// Start putting a record into the DHT. Other nodes can later fetch that value with
+    /// `get_value`.
+    ///
+    /// A corresponding `ValuePut` or `ValuePutFailed` event will later be generated.
+    pub fn put_value(&mut self, key: RecordKey, value: Vec<u8>) {
+        if let Some(k) = self.kademlia.as_mut() {
+            if let Err(e) = k.put_record(Record::new(key.clone(), value.clone()), Quorum::All) {
+                warn!(target: "sub-libp2p", "Libp2p => Failed to put record: {:?}", e);
+                self.pending_events.push_back(DiscoveryOut::ValuePutFailed(
+                    key.clone(),
+                    Duration::from_secs(0),
+                ));
+            }
+        }
+    }
+
+    /// Returns the number of nodes in each Kademlia kbucket for each Kademlia instance.
+    ///
+    /// Identifies Kademlia instances by their [`ProtocolId`] and kbuckets by the base 2 logarithm
+    /// of their lower bound.
+    pub fn num_entries_per_kbucket(&mut self) -> Option<Vec<(u32, usize)>> {
+        self.kademlia.as_mut().map(|kad| {
+            kad.kbuckets()
+                .map(|bucket| (bucket.range().0.ilog2().unwrap_or(0), bucket.iter().count()))
+                .collect()
+        })
+    }
+
+    /// Returns the number of records in the Kademlia record stores.
+    pub fn num_kademlia_records(&mut self) -> Option<usize> {
+        // Note that this code is ok only because we use a `MemoryStore`.
+        self.kademlia
+            .as_mut()
+            .map(|kad| kad.store_mut().records().count())
+    }
+
+    /// Returns the total size in bytes of all the records in the Kademlia record stores.
+    pub fn kademlia_records_total_size(&mut self) -> Option<usize> {
+        // Note that this code is ok only because we use a `MemoryStore`. If the records were
+        // for example stored on disk, this would load every single one of them every single time.
+        self.kademlia.as_mut().map(|kad| {
+            kad.store_mut()
+                .records()
+                .fold(0, |tot, rec| tot + rec.value.len())
+        })
+    }
+
+    /// Can the given `Multiaddr` be put into the DHT?
+    ///
+    /// This test is successful only for global IP addresses and DNS names.
+    // NB: Currently all DNS names are allowed and no check for TLD suffixes is done
+    // because the set of valid domains is highly dynamic and would require frequent
+    // updates, for example by utilising publicsuffix.org or IANA.
+    pub fn can_add_to_dht(addr: &Multiaddr) -> bool {
+        let ip = match addr.iter().next() {
+            Some(Protocol::Ip4(ip)) => IpNetwork::from(ip),
+            Some(Protocol::Ip6(ip)) => IpNetwork::from(ip),
+            Some(Protocol::Dns(_)) | Some(Protocol::Dns4(_)) | Some(Protocol::Dns6(_)) => {
+                return true
+            }
+            _ => return false,
+        };
+        ip.is_global()
+    }
+}
+
+/// Event generated by the `DiscoveryBehaviour`.
+#[derive(Debug)]
+pub enum DiscoveryOut {
+    /// A connection to a peer has been established but the peer has not been
+    /// added to the routing table because [`KademliaBucketInserts::Manual`] is
+    /// configured. If the peer is to be included in the routing table, it must
+    /// be explicitly added via
+    /// [`DiscoveryBehaviour::add_self_reported_address`].
+    Discovered(PeerId),
+
+    /// A peer connected to this node for whom no listen address is known.
+    ///
+    /// In order for the peer to be added to the Kademlia routing table, a known
+    /// listen address must be added via
+    /// [`DiscoveryBehaviour::add_self_reported_address`], e.g. obtained through
+    /// the `identify` protocol.
+    UnroutablePeer(PeerId),
+
+    /// The DHT yielded results for the record request.
+    ///
+    /// Returning the result grouped in (key, value) pairs as well as the request duration.
+    ValueFound(Vec<(RecordKey, Vec<u8>)>, Duration),
+
+    /// The record requested was not found in the DHT.
+    ///
+    /// Returning the corresponding key as well as the request duration.
+    ValueNotFound(RecordKey, Duration),
+
+    /// The record with a given key was successfully inserted into the DHT.
+    ///
+    /// Returning the corresponding key as well as the request duration.
+    ValuePut(RecordKey, Duration),
+
+    /// Inserting a value into the DHT failed.
+    ///
+    /// Returning the corresponding key as well as the request duration.
+    ValuePutFailed(RecordKey, Duration),
+
+    /// Started a random Kademlia query.
+    ///
+    /// Only happens if [`DiscoveryConfig::with_dht_random_walk`] has been configured to `true`.
+    RandomKademliaStarted,
+}
+
+impl NetworkBehaviour for DiscoveryBehaviour {
+    type ConnectionHandler = ToggleConnectionHandler<KademliaHandler<QueryId>>;
+    type OutEvent = DiscoveryOut;
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.kademlia.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.kademlia.handle_established_outbound_connection(
+            connection_id,
+            peer,
+            addr,
+            role_override,
+        )
+    }
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        self.kademlia
+            .handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        maybe_peer: Option<PeerId>,
+        addresses: &[Multiaddr],
+        effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        let Some(peer_id) = maybe_peer else {
+            return Ok(Vec::new());
+        };
+
+        let mut list = self
+            .permanent_addresses
+            .iter()
+            .filter_map(|(p, a)| (*p == peer_id).then_some(a.clone()))
+            .collect::<Vec<_>>();
+
+        if let Some(ephemeral_addresses) = self.ephemeral_addresses.get(&peer_id) {
+            list.extend(ephemeral_addresses.clone());
+        }
+
+        {
+            let mut list_to_filter = self.kademlia.handle_pending_outbound_connection(
+                connection_id,
+                maybe_peer,
+                addresses,
+                effective_role,
+            )?;
+
+            list_to_filter.extend(self.mdns.handle_pending_outbound_connection(
+                connection_id,
+                maybe_peer,
+                addresses,
+                effective_role,
+            )?);
+
+            if !self.allow_private_ip {
+                list_to_filter.retain(|addr| match addr.iter().next() {
+                    Some(Protocol::Ip4(addr)) if !IpNetwork::from(addr).is_global() => false,
+                    Some(Protocol::Ip6(addr)) if !IpNetwork::from(addr).is_global() => false,
+                    _ => true,
+                });
+            }
+
+            list.extend(list_to_filter);
+        }
+
+        trace!(target: "sub-libp2p", "Addresses of {:?}: {:?}", peer_id, list);
+
+        Ok(list)
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        match event {
+            FromSwarm::ConnectionEstablished(e) => {
+                self.num_connections += 1;
+                self.kademlia
+                    .on_swarm_event(FromSwarm::ConnectionEstablished(e));
+            }
+            FromSwarm::ConnectionClosed(e) => {
+                self.num_connections -= 1;
+                self.kademlia.on_swarm_event(FromSwarm::ConnectionClosed(e));
+            }
+            FromSwarm::DialFailure(e @ DialFailure { peer_id, error, .. }) => {
+                if let Some(peer_id) = peer_id {
+                    if let DialError::Transport(errors) = error {
+                        if let Entry::Occupied(mut entry) = self.ephemeral_addresses.entry(peer_id)
+                        {
+                            for (addr, _error) in errors {
+                                entry.get_mut().retain(|a| a != addr);
+                            }
+                            if entry.get().is_empty() {
+                                entry.remove();
+                            }
+                        }
+                    }
+                }
+
+                self.kademlia.on_swarm_event(FromSwarm::DialFailure(e));
+            }
+            FromSwarm::ListenerClosed(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::ListenerClosed(e));
+            }
+            FromSwarm::ListenFailure(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::ListenFailure(e));
+            }
+            FromSwarm::ListenerError(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::ListenerError(e));
+            }
+            FromSwarm::ExpiredExternalAddr(e) => {
+                // We intentionally don't remove the element from `known_external_addresses` in
+                // order to not print the log line again.
+
+                self.kademlia
+                    .on_swarm_event(FromSwarm::ExpiredExternalAddr(e));
+            }
+            FromSwarm::NewListener(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::NewListener(e));
+            }
+            FromSwarm::ExpiredListenAddr(e) => {
+                self.kademlia
+                    .on_swarm_event(FromSwarm::ExpiredListenAddr(e));
+            }
+            FromSwarm::NewExternalAddr(e @ NewExternalAddr { addr }) => {
+                let new_addr = addr.clone().with(Protocol::P2p(self.local_peer_id.into()));
+
+                if Self::can_add_to_dht(addr) {
+                    // NOTE: we might re-discover the same address multiple times
+                    // in which case we just want to refrain from logging.
+                    if self.known_external_addresses.insert(new_addr.clone()) {
+                        info!(
+                          target: "sub-libp2p",
+                          "🔍 Discovered new external address for our node: {}",
+                          new_addr,
+                        );
+                    }
+                }
+
+                self.kademlia.on_swarm_event(FromSwarm::NewExternalAddr(e));
+            }
+            FromSwarm::AddressChange(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::AddressChange(e));
+            }
+            FromSwarm::NewListenAddr(e) => {
+                self.kademlia.on_swarm_event(FromSwarm::NewListenAddr(e));
+                self.mdns.on_swarm_event(FromSwarm::NewListenAddr(e));
+            }
+        }
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        self.kademlia
+            .on_connection_handler_event(peer_id, connection_id, event);
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+        params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        // Immediately process the content of `discovered`.
+        if let Some(ev) = self.pending_events.pop_front() {
+            return Poll::Ready(ToSwarm::GenerateEvent(ev));
+        }
+
+        // Poll the stream that fires when we need to start a random Kademlia query.
+        if let Some(kademlia) = self.kademlia.as_mut() {
+            if let Some(next_kad_random_query) = self.next_kad_random_query.as_mut() {
+                while next_kad_random_query.poll_unpin(cx).is_ready() {
+                    let actually_started =
+                        if self.num_connections < self.discovery_only_if_under_num {
+                            let random_peer_id = PeerId::random();
+                            debug!(
+                                target: "sub-libp2p",
+                                "Libp2p <= Starting random Kademlia request for {:?}",
+                                random_peer_id,
+                            );
+                            kademlia.get_closest_peers(random_peer_id);
+                            true
+                        } else {
+                            debug!(
+                                target: "sub-libp2p",
+                                "Kademlia paused due to high number of connections ({})",
+                                self.num_connections
+                            );
+                            false
+                        };
+
+                    // Schedule the next random query with exponentially increasing delay,
+                    // capped at 60 seconds.
+                    *next_kad_random_query = Delay::new(self.duration_to_next_kad);
+                    self.duration_to_next_kad =
+                        cmp::min(self.duration_to_next_kad * 2, Duration::from_secs(60));
+
+                    if actually_started {
+                        let ev = DiscoveryOut::RandomKademliaStarted;
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                }
+            }
+        }
+
+        while let Poll::Ready(ev) = self.kademlia.poll(cx, params) {
+            match ev {
+                ToSwarm::GenerateEvent(ev) => match ev {
+                    KademliaEvent::RoutingUpdated { peer, .. } => {
+                        let ev = DiscoveryOut::Discovered(peer);
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                    KademliaEvent::UnroutablePeer { peer, .. } => {
+                        let ev = DiscoveryOut::UnroutablePeer(peer);
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                    KademliaEvent::RoutablePeer { peer, .. } => {
+                        let ev = DiscoveryOut::Discovered(peer);
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                    KademliaEvent::PendingRoutablePeer { .. }
+                    | KademliaEvent::InboundRequest { .. } => {
+                        // We are not interested in this event at the moment.
+                    }
+                    KademliaEvent::OutboundQueryProgressed {
+                        result: QueryResult::GetClosestPeers(res),
+                        ..
+                    } => match res {
+                        Err(GetClosestPeersError::Timeout { key, peers }) => {
+                            debug!(
+                                target: "sub-libp2p",
+                                "Libp2p => Query for {:?} timed out with {} results",
+                                HexDisplay::from(&key), peers.len(),
+                            );
+                        }
+                        Ok(ok) => {
+                            trace!(
+                                target: "sub-libp2p",
+                                "Libp2p => Query for {:?} yielded {:?} results",
+                                HexDisplay::from(&ok.key), ok.peers.len(),
+                            );
+                            if ok.peers.is_empty() && self.num_connections != 0 {
+                                debug!(
+                                    target: "sub-libp2p",
+                                    "Libp2p => Random Kademlia query has yielded empty results",
+                                );
+                            }
+                        }
+                    },
+                    KademliaEvent::OutboundQueryProgressed {
+                        result: QueryResult::GetRecord(res),
+                        stats,
+                        id,
+                        ..
+                    } => {
+                        let ev = match res {
+                            Ok(GetRecordOk::FoundRecord(r)) => {
+                                debug!(
+                                    target: "sub-libp2p",
+                                    "Libp2p => Found record ({:?}) with value: {:?}",
+                                    r.record.key,
+                                    r.record.value,
+                                );
+
+                                // Let's directly finish the query, as we are only interested in a
+                                // quorum of 1.
+                                if let Some(kad) = self.kademlia.as_mut() {
+                                    if let Some(mut query) = kad.query_mut(&id) {
+                                        query.finish();
+                                    }
+                                }
+
+                                // Will be removed below when we receive
+                                // `FinishedWithNoAdditionalRecord`.
+                                self.records_to_publish.insert(id, r.record.clone());
+
+                                DiscoveryOut::ValueFound(
+                                    vec![(r.record.key, r.record.value)],
+                                    stats.duration().unwrap_or_default(),
+                                )
+                            }
+                            Ok(GetRecordOk::FinishedWithNoAdditionalRecord {
+                                cache_candidates,
+                            }) => {
+                                // We always need to remove the record to not leak any data!
+                                if let Some(record) = self.records_to_publish.remove(&id) {
+                                    if cache_candidates.is_empty() {
+                                        continue;
+                                    }
+
+                                    // Put the record to the `cache_candidates` that are nearest to
+                                    // the record key from our point of view of the network.
+                                    if let Some(kad) = self.kademlia.as_mut() {
+                                        kad.put_record_to(
+                                            record,
+                                            cache_candidates.into_iter().map(|v| v.1),
+                                            Quorum::One,
+                                        );
+                                    }
+                                }
+
+                                continue;
+                            }
+                            Err(e @ libp2p::kad::GetRecordError::NotFound { .. }) => {
+                                trace!(
+                                    target: "sub-libp2p",
+                                    "Libp2p => Failed to get record: {:?}",
+                                    e,
+                                );
+                                DiscoveryOut::ValueNotFound(
+                                    e.into_key(),
+                                    stats.duration().unwrap_or_default(),
+                                )
+                            }
+                            Err(e) => {
+                                debug!(
+                                    target: "sub-libp2p",
+                                    "Libp2p => Failed to get record: {:?}",
+                                    e,
+                                );
+                                DiscoveryOut::ValueNotFound(
+                                    e.into_key(),
+                                    stats.duration().unwrap_or_default(),
+                                )
+                            }
+                        };
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                    KademliaEvent::OutboundQueryProgressed {
+                        result: QueryResult::PutRecord(res),
+                        stats,
+                        ..
+                    } => {
+                        let ev = match res {
+                            Ok(ok) => {
+                                DiscoveryOut::ValuePut(ok.key, stats.duration().unwrap_or_default())
+                            }
+                            Err(e) => {
+                                debug!(
+                                    target: "sub-libp2p",
+                                    "Libp2p => Failed to put record: {:?}",
+                                    e,
+                                );
+                                DiscoveryOut::ValuePutFailed(
+                                    e.into_key(),
+                                    stats.duration().unwrap_or_default(),
+                                )
+                            }
+                        };
+                        return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                    }
+                    KademliaEvent::OutboundQueryProgressed {
+                        result: QueryResult::RepublishRecord(res),
+                        ..
+                    } => match res {
+                        Ok(ok) => debug!(
+                            target: "sub-libp2p",
+                            "Libp2p => Record republished: {:?}",
+                            ok.key,
+                        ),
+                        Err(e) => debug!(
+                            target: "sub-libp2p",
+                            "Libp2p => Republishing of record {:?} failed with: {:?}",
+                            e.key(), e,
+                        ),
+                    },
+                    // We never start any other type of query.
+                    KademliaEvent::OutboundQueryProgressed { result: e, .. } => {
+                        warn!(target: "sub-libp2p", "Libp2p => Unhandled Kademlia event: {:?}", e)
+                    }
+                },
+                ToSwarm::Dial { opts } => return Poll::Ready(ToSwarm::Dial { opts }),
+                ToSwarm::NotifyHandler {
+                    peer_id,
+                    handler,
+                    event,
+                } => {
+                    return Poll::Ready(ToSwarm::NotifyHandler {
+                        peer_id,
+                        handler,
+                        event,
+                    })
+                }
+                ToSwarm::ReportObservedAddr { address, score } => {
+                    return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+                }
+                ToSwarm::CloseConnection {
+                    peer_id,
+                    connection,
+                } => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+            }
+        }
+
+        // Poll mDNS.
+        while let Poll::Ready(ev) = self.mdns.poll(cx, params) {
+            match ev {
+                ToSwarm::GenerateEvent(event) => match event {
+                    mdns::Event::Discovered(list) => {
+                        if self.num_connections >= self.discovery_only_if_under_num {
+                            continue;
+                        }
+
+                        self.pending_events
+                            .extend(list.map(|(peer_id, _)| DiscoveryOut::Discovered(peer_id)));
+                        if let Some(ev) = self.pending_events.pop_front() {
+                            return Poll::Ready(ToSwarm::GenerateEvent(ev));
+                        }
+                    }
+                    mdns::Event::Expired(_) => {}
+                },
+                ToSwarm::Dial { .. } => {
+                    unreachable!("mDNS never dials!");
+                }
+                ToSwarm::NotifyHandler { event, .. } => match event {}, /* `event` is an */
+                // enum with no
+                // variant
+                ToSwarm::ReportObservedAddr { address, score } => {
+                    return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+                }
+                ToSwarm::CloseConnection {
+                    peer_id,
+                    connection,
+                } => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+            }
+        }
+
+        Poll::Pending
+    }
+}
+
+/// Legacy (fallback) Kademlia protocol name based on `protocol_id`.
+fn legacy_kademlia_protocol_name(id: &ProtocolId) -> Vec<u8> {
+    let mut v = vec![b'/'];
+    v.extend_from_slice(id.as_ref().as_bytes());
+    v.extend_from_slice(b"/kad");
+    v
+}
+
+/// Kademlia protocol name based on `genesis_hash` and `fork_id`.
+fn kademlia_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> Vec<u8> {
+    let genesis_hash_hex = bytes2hex("", genesis_hash.as_ref());
+    if let Some(fork_id) = fork_id {
+        format!("/{}/{}/kad", genesis_hash_hex, fork_id)
+            .as_bytes()
+            .into()
+    } else {
+        format!("/{}/kad", genesis_hash_hex).as_bytes().into()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{
+        kademlia_protocol_name, legacy_kademlia_protocol_name, DiscoveryConfig, DiscoveryOut,
+    };
+    use crate::config::ProtocolId;
+    use futures::prelude::*;
+    use libp2p::{
+        core::{
+            transport::{MemoryTransport, Transport},
+            upgrade,
+        },
+        identity::Keypair,
+        noise,
+        swarm::{Executor, Swarm, SwarmBuilder, SwarmEvent},
+        yamux, Multiaddr,
+    };
+    use sp_core::hash::H256;
+    use std::{collections::HashSet, pin::Pin, task::Poll};
+
+    struct TokioExecutor(tokio::runtime::Runtime);
+    impl Executor for TokioExecutor {
+        fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) {
+            let res = self.0.spawn(f);
+            drop(res);
+        }
+    }
+
+    #[test]
+    #[allow(clippy::single_match)]
+    fn discovery_working() {
+        let mut first_swarm_peer_id_and_addr = None;
+
+        let genesis_hash = H256::from_low_u64_be(1);
+        let fork_id = Some("test-fork-id");
+        let protocol_id = ProtocolId::from("dot");
+
+        // Build swarms whose behaviour is `DiscoveryBehaviour`, each aware of
+        // the first swarm via `with_permanent_addresses`.
+        let mut swarms = (0..25)
+            .map(|i| {
+                let keypair = Keypair::generate_ed25519();
+
+                let transport = MemoryTransport::new()
+                    .upgrade(upgrade::Version::V1)
+                    .authenticate(noise::Config::new(&keypair).unwrap())
+                    .multiplex(yamux::Config::default())
+                    .boxed();
+
+                let behaviour = {
+                    let mut config = DiscoveryConfig::new(keypair.public().to_peer_id());
+                    config
+                        .with_permanent_addresses(first_swarm_peer_id_and_addr.clone())
+                        .allow_private_ip(true)
+                        .allow_non_globals_in_dht(true)
+                        .discovery_limit(50)
+                        .with_kademlia(genesis_hash, fork_id, &protocol_id);
+
+                    config.finish()
+                };
+
+                let runtime = tokio::runtime::Runtime::new().unwrap();
+                let mut swarm = SwarmBuilder::with_executor(
+                    transport,
+                    behaviour,
+                    keypair.public().to_peer_id(),
+                    TokioExecutor(runtime),
+                )
+                .build();
+
+                let listen_addr: Multiaddr = format!("/memory/{}", rand::random::<u64>())
+                    .parse()
+                    .unwrap();
+
+                if i == 0 {
+                    first_swarm_peer_id_and_addr =
+                        Some((keypair.public().to_peer_id(), listen_addr.clone()))
+                }
+
+                swarm.listen_on(listen_addr.clone()).unwrap();
+                (swarm, listen_addr)
+            })
+            .collect::<Vec<_>>();
+
+        // Build a `Vec<HashSet<PeerId>>` with the list of nodes remaining to be discovered.
+        let mut to_discover = (0..swarms.len())
+            .map(|n| {
+                (0..swarms.len())
+                    // Skip the first swarm as all other swarms already know it.
+                    .skip(1)
+                    .filter(|p| *p != n)
+                    .map(|p| *Swarm::local_peer_id(&swarms[p].0))
+                    .collect::<HashSet<_>>()
+            })
+            .collect::<Vec<_>>();
+
+        let fut = futures::future::poll_fn(move |cx| {
+            'polling: loop {
+                for swarm_n in 0..swarms.len() {
+                    match swarms[swarm_n].0.poll_next_unpin(cx) {
+                        Poll::Ready(Some(e)) => {
+                            match e {
+                                SwarmEvent::Behaviour(behavior) => {
+                                    match behavior {
+                                        DiscoveryOut::UnroutablePeer(other)
+                                        | DiscoveryOut::Discovered(other) => {
+                                            // Call `add_self_reported_address` to simulate identify
+                                            // happening.
+                                            let addr = swarms
+                                                .iter()
+                                                .find_map(|(s, a)| {
+                                                    if s.behaviour().local_peer_id == other {
+                                                        Some(a.clone())
+                                                    } else {
+                                                        None
+                                                    }
+                                                })
+                                                .unwrap();
+                                            // Test both genesis hash-based and legacy
+                                            // protocol names.
+                                            let protocol_name = if swarm_n % 2 == 0 {
+                                                kademlia_protocol_name(genesis_hash, fork_id)
+                                            } else {
+                                                legacy_kademlia_protocol_name(&protocol_id)
+                                            };
+                                            swarms[swarm_n]
+                                                .0
+                                                .behaviour_mut()
+                                                .add_self_reported_address(
+                                                    &other,
+                                                    &[protocol_name],
+                                                    addr,
+                                                );
+
+                                            to_discover[swarm_n].remove(&other);
+                                        }
+                                        DiscoveryOut::RandomKademliaStarted => {}
+                                        e => {
+                                            panic!("Unexpected event: {:?}", e)
+                                        }
+                                    }
+                                }
+                                // ignore non Behaviour events
+                                _ => {}
+                            }
+                            continue 'polling;
+                        }
+                        _ => {}
+                    }
+                }
+                break;
+            }
+
+            if to_discover.iter().all(|l| l.is_empty()) {
+                Poll::Ready(())
+            } else {
+                Poll::Pending
+            }
+        });
+
+        futures::executor::block_on(fut);
+    }
+
+    #[test]
+    fn discovery_ignores_peers_with_unknown_protocols() {
+        let supported_genesis_hash = H256::from_low_u64_be(1);
+        let unsupported_genesis_hash = H256::from_low_u64_be(2);
+        let supported_protocol_id = ProtocolId::from("a");
+        let unsupported_protocol_id = ProtocolId::from("b");
+
+        let mut discovery = {
+            let keypair = Keypair::generate_ed25519();
+            let mut config = DiscoveryConfig::new(keypair.public().to_peer_id());
+            config
+                .allow_private_ip(true)
+                .allow_non_globals_in_dht(true)
+                .discovery_limit(50)
+                .with_kademlia(supported_genesis_hash, None, &supported_protocol_id);
+            config.finish()
+        };
+
+        let predictable_peer_id = |bytes: &[u8; 32]| {
+            Keypair::ed25519_from_bytes(bytes.to_owned())
+                .unwrap()
+                .public()
+                .to_peer_id()
+        };
+
+        let remote_peer_id = predictable_peer_id(b"00000000000000000000000000000001");
+        let remote_addr: Multiaddr = "/memory/1".parse().unwrap();
+        let another_peer_id = predictable_peer_id(b"00000000000000000000000000000002");
+        let another_addr: Multiaddr = "/memory/2".parse().unwrap();
+
+        // Try adding remote peers with unsupported protocols.
+        discovery.add_self_reported_address(
+            &remote_peer_id,
+            &[kademlia_protocol_name(unsupported_genesis_hash, None)],
+            remote_addr.clone(),
+        );
+        discovery.add_self_reported_address(
+            &another_peer_id,
+            &[legacy_kademlia_protocol_name(&unsupported_protocol_id)],
+            another_addr.clone(),
+        );
+
+        {
+            let kademlia = discovery.kademlia.as_mut().unwrap();
+            assert!(
+                kademlia
+                    .kbucket(remote_peer_id)
+                    .expect("Remote peer id not to be equal to local peer id.")
+                    .is_empty(),
+                "Expect peer with unsupported protocol not to be added."
+            );
+            assert!(
+                kademlia
+                    .kbucket(another_peer_id)
+                    .expect("Remote peer id not to be equal to local peer id.")
+                    .is_empty(),
+                "Expect peer with unsupported protocol not to be added."
+            );
+        }
+
+        // Add remote peers with supported protocols.
+        discovery.add_self_reported_address(
+            &remote_peer_id,
+            &[kademlia_protocol_name(supported_genesis_hash, None)],
+            remote_addr.clone(),
+        );
+        discovery.add_self_reported_address(
+            &another_peer_id,
+            &[legacy_kademlia_protocol_name(&supported_protocol_id)],
+            another_addr.clone(),
+        );
+
+        {
+            let kademlia = discovery.kademlia.as_mut().unwrap();
+            assert_eq!(
+                2,
+                kademlia
+                    .kbuckets()
+                    .fold(0, |acc, bucket| acc + bucket.num_entries()),
+                "Expect peers with supported protocol to be added."
+            );
+        }
+    }
+}
diff --git a/libp2p/src/error.rs b/libp2p/src/error.rs
new file mode 100644
index 000000000..7deaff069
--- /dev/null
+++ b/libp2p/src/error.rs
@@ -0,0 +1,88 @@
+// 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/>.
+
+//! Substrate network possible errors.
+
+use crate::{config::TransportConfig, types::ProtocolName};
+
+use libp2p::{Multiaddr, PeerId};
+
+use std::fmt;
+
+/// Result type alias for the network.
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// Error type for the network.
+#[allow(clippy::large_enum_variant)]
+#[derive(thiserror::Error)]
+pub enum Error {
+    /// Io error
+    #[error(transparent)]
+    Io(#[from] std::io::Error),
+
+    /// Client error
+    #[error(transparent)]
+    Client(#[from] Box<sp_blockchain::Error>),
+    /// The same bootnode (based on address) is registered with two different peer ids.
+    #[error(
+		"The same bootnode (`{address}`) is registered with two different peer ids: `{first_id}` and `{second_id}`"
+	)]
+    DuplicateBootnode {
+        /// The address of the bootnode.
+        address: Multiaddr,
+        /// The first peer id that was found for the bootnode.
+        first_id: PeerId,
+        /// The second peer id that was found for the bootnode.
+        second_id: PeerId,
+    },
+    /// Prometheus metrics error.
+    #[error(transparent)]
+    Prometheus(#[from] prometheus_endpoint::PrometheusError),
+    /// The network addresses are invalid because they don't match the transport.
+    #[error(
+        "The following addresses are invalid because they don't match the transport: {addresses:?}"
+    )]
+    AddressesForAnotherTransport {
+        /// Transport used.
+        transport: TransportConfig,
+        /// The invalid addresses.
+        addresses: Vec<Multiaddr>,
+    },
+    /// The same request-response protocol has been registered multiple times.
+    #[error("Request-response protocol registered multiple times: {protocol}")]
+    DuplicateRequestResponseProtocol {
+        /// Name of the protocol registered multiple times.
+        protocol: ProtocolName,
+    },
+    /// Peer does not exist.
+    #[error("Peer `{0}` does not exist.")]
+    PeerDoesntExist(PeerId),
+    /// Channel closed.
+    #[error("Channel closed")]
+    ChannelClosed,
+    /// Connection closed.
+    #[error("Connection closed")]
+    ConnectionClosed,
+}
+
+// Make `Debug` use the `Display` implementation.
+impl fmt::Debug for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
diff --git a/libp2p/src/event.rs b/libp2p/src/event.rs
new file mode 100644
index 000000000..4cd93e632
--- /dev/null
+++ b/libp2p/src/event.rs
@@ -0,0 +1,92 @@
+// 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/>.
+
+//! Network event types. These are are not the part of the protocol, but rather
+//! events that happen on the network like DHT get/put results received.
+
+use crate::types::ProtocolName;
+
+use bytes::Bytes;
+use libp2p::{kad::record::Key, PeerId};
+
+use sc_network_common::role::ObservedRole;
+
+/// Events generated by DHT as a response to get_value and put_value requests.
+#[derive(Debug, Clone)]
+#[must_use]
+pub enum DhtEvent {
+    /// The value was found.
+    ValueFound(Vec<(Key, Vec<u8>)>),
+
+    /// The requested record has not been found in the DHT.
+    ValueNotFound(Key),
+
+    /// The record has been successfully inserted into the DHT.
+    ValuePut(Key),
+
+    /// An error has occurred while putting a record into the DHT.
+    ValuePutFailed(Key),
+}
+
+/// Type for events generated by networking layer.
+#[derive(Debug, Clone)]
+#[must_use]
+pub enum Event {
+    /// Event generated by a DHT.
+    Dht(DhtEvent),
+
+    /// Opened a substream with the given node with the given notifications protocol.
+    ///
+    /// The protocol is always one of the notification protocols that have been registered.
+    NotificationStreamOpened {
+        /// Node we opened the substream with.
+        remote: PeerId,
+        /// The concerned protocol. Each protocol uses a different substream.
+        /// This is always equal to the value of
+        /// `sc_network::config::NonDefaultSetConfig::notifications_protocol` of one of the
+        /// configured sets.
+        protocol: ProtocolName,
+        /// If the negotiation didn't use the main name of the protocol (the one in
+        /// `notifications_protocol`), then this field contains which name has actually been
+        /// used.
+        /// Always contains a value equal to the value in
+        /// `sc_network::config::NonDefaultSetConfig::fallback_names`.
+        negotiated_fallback: Option<ProtocolName>,
+        /// Role of the remote.
+        role: ObservedRole,
+        /// Received handshake.
+        received_handshake: Vec<u8>,
+    },
+
+    /// Closed a substream with the given node. Always matches a corresponding previous
+    /// `NotificationStreamOpened` message.
+    NotificationStreamClosed {
+        /// Node we closed the substream with.
+        remote: PeerId,
+        /// The concerned protocol. Each protocol uses a different substream.
+        protocol: ProtocolName,
+    },
+
+    /// Received one or more messages from the given node using the given protocol.
+    NotificationsReceived {
+        /// Node we received the message from.
+        remote: PeerId,
+        /// Concerned protocol and associated message.
+        messages: Vec<(ProtocolName, Bytes)>,
+    },
+}
diff --git a/libp2p/src/lib.rs b/libp2p/src/lib.rs
new file mode 100644
index 000000000..6265d7584
--- /dev/null
+++ b/libp2p/src/lib.rs
@@ -0,0 +1,298 @@
+// 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/>.
+
+#![warn(unused_extern_crates)]
+
+//! Substrate-specific P2P networking.
+//!
+//! **Important**: This crate is unstable and the API and usage may change.
+//!
+//! # Node identities and addresses
+//!
+//! In a decentralized network, each node possesses a network private key and a network public key.
+//! In Substrate, the keys are based on the ed25519 curve.
+//!
+//! From a node's public key, we can derive its *identity*. In Substrate and libp2p, a node's
+//! identity is represented with the [`PeerId`] struct. All network communications between nodes on
+//! the network use encryption derived from both sides's keys, which means that **identities cannot
+//! be faked**.
+//!
+//! A node's identity uniquely identifies a machine on the network. If you start two or more
+//! clients using the same network key, large interferences will happen.
+//!
+//! # Substrate's network protocol
+//!
+//! Substrate's networking protocol is based upon libp2p. It is at the moment not possible and not
+//! planned to permit using something else than the libp2p network stack and the rust-libp2p
+//! library. However the libp2p framework is very flexible and the rust-libp2p library could be
+//! extended to support a wider range of protocols than what is offered by libp2p.
+//!
+//! ## Discovery mechanisms
+//!
+//! In order for our node to join a peer-to-peer network, it has to know a list of nodes that are
+//! part of said network. This includes nodes identities and their address (how to reach them).
+//! Building such a list is called the **discovery** mechanism. There are three mechanisms that
+//! Substrate uses:
+//!
+//! - Bootstrap nodes. These are hard-coded node identities and addresses passed alongside with
+//! the network configuration.
+//! - mDNS. We perform a UDP broadcast on the local network. Nodes that listen may respond with
+//! their identity. More info [here](https://github.com/libp2p/specs/blob/master/discovery/mdns.md).
+//! mDNS can be disabled in the network configuration.
+//! - Kademlia random walk. Once connected, we perform random Kademlia `FIND_NODE` requests on the
+//! configured Kademlia DHTs (one per configured chain protocol) in order for nodes to propagate to
+//! us their view of the network. More information about Kademlia can be found [on
+//! Wikipedia](https://en.wikipedia.org/wiki/Kademlia).
+//!
+//! ## Connection establishment
+//!
+//! When node Alice knows node Bob's identity and address, it can establish a connection with Bob.
+//! All connections must always use encryption and multiplexing. While some node addresses (eg.
+//! addresses using `/quic`) already imply which encryption and/or multiplexing to use, for others
+//! the **multistream-select** protocol is used in order to negotiate an encryption layer and/or a
+//! multiplexing layer.
+//!
+//! The connection establishment mechanism is called the **transport**.
+//!
+//! As of the writing of this documentation, the following base-layer protocols are supported by
+//! Substrate:
+//!
+//! - TCP/IP for addresses of the form `/ip4/1.2.3.4/tcp/5`. Once the TCP connection is open, an
+//! encryption and a multiplexing layer are negotiated on top.
+//! - WebSockets for addresses of the form `/ip4/1.2.3.4/tcp/5/ws`. A TCP/IP connection is open and
+//! the WebSockets protocol is negotiated on top. Communications then happen inside WebSockets data
+//! frames. Encryption and multiplexing are additionally negotiated again inside this channel.
+//! - DNS for addresses of the form `/dns/example.com/tcp/5` or `/dns/example.com/tcp/5/ws`. A
+//! node's address can contain a domain name.
+//! - (All of the above using IPv6 instead of IPv4.)
+//!
+//! On top of the base-layer protocol, the [Noise](https://noiseprotocol.org/) protocol is
+//! negotiated and applied. The exact handshake protocol is experimental and is subject to change.
+//!
+//! The following multiplexing protocols are supported:
+//!
+//! - [Yamux](https://github.com/hashicorp/yamux/blob/master/spec.md).
+//!
+//! ## Substreams
+//!
+//! Once a connection has been established and uses multiplexing, substreams can be opened. When
+//! a substream is open, the **multistream-select** protocol is used to negotiate which protocol
+//! to use on that given substream.
+//!
+//! Protocols that are specific to a certain chain have a `<protocol-id>` in their name. This
+//! "protocol ID" is defined in the chain specifications. For example, the protocol ID of Polkadot
+//! is "dot". In the protocol names below, `<protocol-id>` must be replaced with the corresponding
+//! protocol ID.
+//!
+//! > **Note**: It is possible for the same connection to be used for multiple chains. For example,
+//! > one can use both the `/dot/sync/2` and `/sub/sync/2` protocols on the same
+//! > connection, provided that the remote supports them.
+//!
+//! Substrate uses the following standard libp2p protocols:
+//!
+//! - **`/ipfs/ping/1.0.0`**. We periodically open an ephemeral substream in order to ping the
+//! remote and check whether the connection is still alive. Failure for the remote to reply leads
+//! to a disconnection.
+//! - **[`/ipfs/id/1.0.0`](https://github.com/libp2p/specs/tree/master/identify)**. We
+//! periodically open an ephemeral substream in order to ask information from the remote.
+//! - **[`/<protocol_id>/kad`](https://github.com/libp2p/specs/pull/108)**. We periodically open
+//! ephemeral substreams for Kademlia random walk queries. Each Kademlia query is done in a
+//! separate substream.
+//!
+//! Additionally, Substrate uses the following non-libp2p-standard protocols:
+//!
+//! - **`/substrate/<protocol-id>/<version>`** (where `<protocol-id>` must be replaced with the
+//! protocol ID of the targeted chain, and `<version>` is a number between 2 and 6). For each
+//! connection we optionally keep an additional substream for all Substrate-based communications
+//! alive. This protocol is considered legacy, and is progressively being replaced with
+//! alternatives. This is designated as "The legacy Substrate substream" in this documentation. See
+//! below for more details.
+//! - **`/<protocol-id>/sync/2`** is a request-response protocol (see below) that lets one perform
+//! requests for information about blocks. Each request is the encoding of a `BlockRequest` and
+//! each response is the encoding of a `BlockResponse`, as defined in the `api.v1.proto` file in
+//! this source tree.
+//! - **`/<protocol-id>/light/2`** is a request-response protocol (see below) that lets one perform
+//! light-client-related requests for information about the state. Each request is the encoding of
+//! a `light::Request` and each response is the encoding of a `light::Response`, as defined in the
+//! `light.v1.proto` file in this source tree.
+//! - **`/<protocol-id>/transactions/1`** is a notifications protocol (see below) where
+//! transactions are pushed to other nodes. The handshake is empty on both sides. The message
+//! format is a SCALE-encoded list of transactions, where each transaction is an opaque list of
+//! bytes.
+//! - **`/<protocol-id>/block-announces/1`** is a notifications protocol (see below) where
+//! block announces are pushed to other nodes. The handshake is empty on both sides. The message
+//! format is a SCALE-encoded tuple containing a block header followed with an opaque list of
+//! bytes containing some data associated with this block announcement, e.g. a candidate message.
+//! - Notifications protocols that are registered using
+//! `NetworkConfiguration::notifications_protocols`. For example: `/paritytech/grandpa/1`. See
+//! below for more information.
+//!
+//! ## The legacy Substrate substream
+//!
+//! Substrate uses a component named the **peerset manager (PSM)**. Through the discovery
+//! mechanism, the PSM is aware of the nodes that are part of the network and decides which nodes
+//! we should perform Substrate-based communications with. For these nodes, we open a connection
+//! if necessary and open a unique substream for Substrate-based communications. If the PSM decides
+//! that we should disconnect a node, then that substream is closed.
+//!
+//! For more information about the PSM, see the *sc-peerset* crate.
+//!
+//! Note that at the moment there is no mechanism in place to solve the issues that arise where the
+//! two sides of a connection open the unique substream simultaneously. In order to not run into
+//! issues, only the dialer of a connection is allowed to open the unique substream. When the
+//! substream is closed, the entire connection is closed as well. This is a bug that will be
+//! resolved by deprecating the protocol entirely.
+//!
+//! Within the unique Substrate substream, messages encoded using
+//! [*parity-scale-codec*](https://github.com/paritytech/parity-scale-codec) are exchanged.
+//! The detail of theses messages is not totally in place, but they can be found in the
+//! `message.rs` file.
+//!
+//! Once the substream is open, the first step is an exchange of a *status* message from both
+//! sides, containing information such as the chain root hash, head of chain, and so on.
+//!
+//! Communications within this substream include:
+//!
+//! - Syncing. Blocks are announced and requested from other nodes.
+//! - Light-client requests. When a light client requires information, a random node we have a
+//! substream open with is chosen, and the information is requested from it.
+//! - Gossiping. Used for example by grandpa.
+//!
+//! ## Request-response protocols
+//!
+//! A so-called request-response protocol is defined as follow:
+//!
+//! - When a substream is opened, the opening side sends a message whose content is
+//! protocol-specific. The message must be prefixed with an
+//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. After the
+//! message has been sent, the writing side is closed.
+//! - The remote sends back the response prefixed with a LEB128-encoded length, and closes its
+//! side as well.
+//!
+//! Each request is performed in a new separate substream.
+//!
+//! ## Notifications protocols
+//!
+//! A so-called notifications protocol is defined as follow:
+//!
+//! - When a substream is opened, the opening side sends a handshake message whose content is
+//! protocol-specific. The handshake message must be prefixed with an
+//! [LEB128-encoded number](https://en.wikipedia.org/wiki/LEB128) indicating its length. The
+//! handshake message can be of length 0, in which case the sender has to send a single `0`.
+//! - The receiver then either immediately closes the substream, or answers with its own
+//! LEB128-prefixed protocol-specific handshake response. The message can be of length 0, in which
+//! case a single `0` has to be sent back.
+//! - Once the handshake has completed, the notifications protocol is unidirectional. Only the
+//! node which initiated the substream can push notifications. If the remote wants to send
+//! notifications as well, it has to open its own undirectional substream.
+//! - Each notification must be prefixed with an LEB128-encoded length. The encoding of the
+//! messages is specific to each protocol.
+//! - Either party can signal that it doesn't want a notifications substream anymore by closing
+//! its writing side. The other party should respond by closing its own writing side soon after.
+//!
+//! The API of `sc-network` allows one to register user-defined notification protocols.
+//! `sc-network` automatically tries to open a substream towards each node for which the legacy
+//! Substream substream is open. The handshake is then performed automatically.
+//!
+//! For example, the `sc-consensus-grandpa` crate registers the `/paritytech/grandpa/1`
+//! notifications protocol.
+//!
+//! At the moment, for backwards-compatibility, notification protocols are tied to the legacy
+//! Substrate substream. Additionally, the handshake message is hardcoded to be a single 8-bits
+//! integer representing the role of the node:
+//!
+//! - 1 for a full node.
+//! - 2 for a light node.
+//! - 4 for an authority.
+//!
+//! In the future, though, these restrictions will be removed.
+//!
+//! # Usage
+//!
+//! Using the `sc-network` crate is done through the [`NetworkWorker`] struct. Create this
+//! struct by passing a [`config::Params`], then poll it as if it was a `Future`. You can extract an
+//! `Arc<NetworkService>` from the `NetworkWorker`, which can be shared amongst multiple places
+//! in order to give orders to the networking.
+//!
+//! See the [`config`] module for more information about how to configure the networking.
+//!
+//! After the `NetworkWorker` has been created, the important things to do are:
+//!
+//! - Calling `NetworkWorker::poll` in order to advance the network. This can be done by
+//! dispatching a background task with the [`NetworkWorker`].
+//! - Calling `on_block_import` whenever a block is added to the client.
+//! - Calling `on_block_finalized` whenever a block is finalized.
+//! - Calling `trigger_repropagate` when a transaction is added to the pool.
+//!
+//! More precise usage details are still being worked on and will likely change in the future.
+
+mod behaviour;
+pub mod protocol;
+
+#[cfg(test)]
+mod mock;
+
+pub mod config;
+pub mod discovery;
+pub mod error;
+pub mod event;
+pub mod network_state;
+pub mod peer_info;
+pub mod peer_store;
+pub mod protocol_controller;
+pub mod request_responses;
+pub mod service;
+pub mod transport;
+pub mod types;
+pub mod utils;
+
+pub use libp2p::identity::PublicKey as LibP2PPublicKey;
+
+pub use event::{DhtEvent, Event};
+#[doc(inline)]
+pub use libp2p::{multiaddr, Multiaddr, PeerId};
+pub use request_responses::{Config, IfDisconnected, RequestFailure};
+pub use sc_network_common::{
+    role::{ObservedRole, Roles},
+    types::ReputationChange,
+};
+pub use service::{
+    signature::Signature,
+    traits::{
+        KademliaKey, MessageSink, NetworkBlock, NetworkDHTProvider, NetworkEventStream,
+        NetworkNotification, NetworkPeers, NetworkRequest, NetworkSigner, NetworkStateInfo,
+        NetworkStatus, NetworkStatusProvider, NetworkSyncForkRequest,
+        NotificationSender as NotificationSenderT, NotificationSenderError,
+        NotificationSenderReady, NotificationService,
+    },
+    DecodingError, Keypair, NetworkService, NetworkWorker, NotificationSender, OutboundFailure,
+    PublicKey,
+};
+pub use types::ProtocolName;
+
+/// The maximum allowed number of established connections per peer.
+///
+/// Typically, and by design of the network behaviours in this crate,
+/// there is a single established connection per peer. However, to
+/// avoid unnecessary and nondeterministic connection closure in
+/// case of (possibly repeated) simultaneous dialing attempts between
+/// two peers, the per-peer connection limit is not set to 1 but 2.
+const MAX_CONNECTIONS_PER_PEER: usize = 2;
+
+/// The maximum number of concurrent established connections that were incoming.
+const MAX_CONNECTIONS_ESTABLISHED_INCOMING: u32 = 10_000;
diff --git a/libp2p/src/mock.rs b/libp2p/src/mock.rs
new file mode 100644
index 000000000..7e2462ce8
--- /dev/null
+++ b/libp2p/src/mock.rs
@@ -0,0 +1,64 @@
+// 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/>.
+
+//! Mocked components for tests.
+
+use crate::{peer_store::PeerStoreProvider, protocol_controller::ProtocolHandle, ReputationChange};
+use libp2p::PeerId;
+use sc_network_common::role::ObservedRole;
+use std::collections::HashSet;
+
+/// No-op `PeerStore`.
+#[derive(Debug)]
+pub struct MockPeerStore {}
+
+impl PeerStoreProvider for MockPeerStore {
+    fn is_banned(&self, _peer_id: &PeerId) -> bool {
+        // Make sure that the peer is not banned.
+        false
+    }
+
+    fn register_protocol(&self, _protocol_handle: ProtocolHandle) {
+        // Make sure not to fail.
+    }
+
+    fn report_disconnect(&mut self, _peer_id: PeerId) {
+        // Make sure not to fail.
+    }
+
+    fn report_peer(&mut self, _peer_id: PeerId, _change: ReputationChange) {
+        // Make sure not to fail.
+    }
+
+    fn peer_reputation(&self, _peer_id: &PeerId) -> i32 {
+        // Make sure that the peer is not banned.
+        0
+    }
+
+    fn peer_role(&self, _peer_id: &PeerId) -> Option<ObservedRole> {
+        None
+    }
+
+    fn set_peer_role(&mut self, _peer_id: &PeerId, _role: ObservedRole) {
+        unimplemented!();
+    }
+
+    fn outgoing_candidates(&self, _count: usize, _ignored: HashSet<&PeerId>) -> Vec<PeerId> {
+        unimplemented!()
+    }
+}
diff --git a/libp2p/src/network_state.rs b/libp2p/src/network_state.rs
new file mode 100644
index 000000000..0ccc21921
--- /dev/null
+++ b/libp2p/src/network_state.rs
@@ -0,0 +1,131 @@
+// 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/>.
+
+//! Information about the networking, for diagnostic purposes.
+//!
+//! **Warning**: These APIs are not stable.
+
+use libp2p::{
+    core::{ConnectedPoint, Endpoint as CoreEndpoint},
+    Multiaddr,
+};
+use serde::{Deserialize, Serialize};
+use std::{
+    collections::{HashMap, HashSet},
+    time::Duration,
+};
+
+/// Returns general information about the networking.
+///
+/// Meant for general diagnostic purposes.
+///
+/// **Warning**: This API is not stable.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NetworkState {
+    /// PeerId of the local node.
+    pub peer_id: String,
+    /// List of addresses the node is currently listening on.
+    pub listened_addresses: HashSet<Multiaddr>,
+    /// List of addresses the node knows it can be reached as.
+    pub external_addresses: HashSet<Multiaddr>,
+    /// List of node we're connected to.
+    pub connected_peers: HashMap<String, Peer>,
+    /// List of node that we know of but that we're not connected to.
+    pub not_connected_peers: HashMap<String, NotConnectedPeer>,
+    /// State of the peerset manager.
+    pub peerset: serde_json::Value,
+}
+
+/// Part of the `NetworkState` struct. Unstable.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Peer {
+    /// How we are connected to the node.
+    pub endpoint: PeerEndpoint,
+    /// Node information, as provided by the node itself. Can be empty if not known yet.
+    pub version_string: Option<String>,
+    /// Latest ping duration with this node.
+    pub latest_ping_time: Option<Duration>,
+    /// List of addresses known for this node.
+    pub known_addresses: HashSet<Multiaddr>,
+}
+
+/// Part of the `NetworkState` struct. Unstable.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NotConnectedPeer {
+    /// List of addresses known for this node.
+    pub known_addresses: HashSet<Multiaddr>,
+    /// Node information, as provided by the node itself, if we were ever connected to this node.
+    pub version_string: Option<String>,
+    /// Latest ping duration with this node, if we were ever connected to this node.
+    pub latest_ping_time: Option<Duration>,
+}
+
+/// Part of the `NetworkState` struct. Unstable.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum PeerEndpoint {
+    /// We are dialing the given address.
+    Dialing(Multiaddr, Endpoint),
+    /// We are listening.
+    Listening {
+        /// Local address of the connection.
+        local_addr: Multiaddr,
+        /// Address data is sent back to.
+        send_back_addr: Multiaddr,
+    },
+}
+
+/// Part of the `NetworkState` struct. Unstable.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum Endpoint {
+    /// The socket comes from a dialer.
+    Dialer,
+    /// The socket comes from a listener.
+    Listener,
+}
+
+impl From<ConnectedPoint> for PeerEndpoint {
+    fn from(endpoint: ConnectedPoint) -> Self {
+        match endpoint {
+            ConnectedPoint::Dialer {
+                address,
+                role_override,
+            } => Self::Dialing(address, role_override.into()),
+            ConnectedPoint::Listener {
+                local_addr,
+                send_back_addr,
+            } => Self::Listening {
+                local_addr,
+                send_back_addr,
+            },
+        }
+    }
+}
+
+impl From<CoreEndpoint> for Endpoint {
+    fn from(endpoint: CoreEndpoint) -> Self {
+        match endpoint {
+            CoreEndpoint::Dialer => Self::Dialer,
+            CoreEndpoint::Listener => Self::Listener,
+        }
+    }
+}
diff --git a/libp2p/src/peer_info.rs b/libp2p/src/peer_info.rs
new file mode 100644
index 000000000..fb527decd
--- /dev/null
+++ b/libp2p/src/peer_info.rs
@@ -0,0 +1,567 @@
+// 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/>.
+
+//! [`PeerInfoBehaviour`] is implementation of `NetworkBehaviour` that holds information about peers
+//! in cache.
+
+use crate::utils::interval;
+use either::Either;
+
+use fnv::FnvHashMap;
+use futures::prelude::*;
+use libp2p::{
+    core::{ConnectedPoint, Endpoint},
+    identify::{
+        Behaviour as Identify, Config as IdentifyConfig, Event as IdentifyEvent,
+        Info as IdentifyInfo,
+    },
+    identity::PublicKey,
+    ping::{Behaviour as Ping, Config as PingConfig, Event as PingEvent, Success as PingSuccess},
+    swarm::{
+        behaviour::{
+            AddressChange, ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm,
+            ListenFailure,
+        },
+        ConnectionDenied, ConnectionHandler, ConnectionId, IntoConnectionHandlerSelect,
+        NetworkBehaviour, PollParameters, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
+    },
+    Multiaddr, PeerId,
+};
+use log::{debug, error, trace};
+use parking_lot::Mutex;
+use smallvec::SmallVec;
+
+use std::{
+    collections::{hash_map::Entry, HashSet},
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+    time::{Duration, Instant},
+};
+
+/// Time after we disconnect from a node before we purge its information from the cache.
+const CACHE_EXPIRE: Duration = Duration::from_secs(10 * 60);
+/// Interval at which we perform garbage collection on the node info.
+const GARBAGE_COLLECT_INTERVAL: Duration = Duration::from_secs(2 * 60);
+
+/// Implementation of `NetworkBehaviour` that holds information about peers in cache.
+pub struct PeerInfoBehaviour {
+    /// Periodically ping nodes, and close the connection if it's unresponsive.
+    ping: Ping,
+    /// Periodically identifies the remote and responds to incoming requests.
+    identify: Identify,
+    /// Information that we know about all nodes.
+    nodes_info: FnvHashMap<PeerId, NodeInfo>,
+    /// Interval at which we perform garbage collection in `nodes_info`.
+    garbage_collect: Pin<Box<dyn Stream<Item = ()> + Send>>,
+    /// Record keeping of external addresses. Data is queried by the `NetworkService`.
+    external_addresses: ExternalAddresses,
+}
+
+/// Information about a node we're connected to.
+#[derive(Debug)]
+struct NodeInfo {
+    /// When we will remove the entry about this node from the list, or `None` if we're connected
+    /// to the node.
+    info_expire: Option<Instant>,
+    /// Non-empty list of connected endpoints, one per connection.
+    endpoints: SmallVec<[ConnectedPoint; crate::MAX_CONNECTIONS_PER_PEER]>,
+    /// Version reported by the remote, or `None` if unknown.
+    client_version: Option<String>,
+    /// Latest ping time with this node.
+    latest_ping: Option<Duration>,
+}
+
+impl NodeInfo {
+    fn new(endpoint: ConnectedPoint) -> Self {
+        let mut endpoints = SmallVec::new();
+        endpoints.push(endpoint);
+        Self {
+            info_expire: None,
+            endpoints,
+            client_version: None,
+            latest_ping: None,
+        }
+    }
+}
+
+/// Utility struct for tracking external addresses. The data is shared with the `NetworkService`.
+#[derive(Debug, Clone, Default)]
+pub struct ExternalAddresses {
+    addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+}
+
+impl ExternalAddresses {
+    /// Add an external address.
+    pub fn add(&mut self, addr: Multiaddr) {
+        self.addresses.lock().insert(addr);
+    }
+
+    /// Remove an external address.
+    pub fn remove(&mut self, addr: &Multiaddr) {
+        self.addresses.lock().remove(addr);
+    }
+}
+
+impl PeerInfoBehaviour {
+    /// Builds a new `PeerInfoBehaviour`.
+    pub fn new(
+        user_agent: String,
+        local_public_key: PublicKey,
+        external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+    ) -> Self {
+        let identify = {
+            let cfg = IdentifyConfig::new("/substrate/1.0".to_string(), local_public_key)
+                .with_agent_version(user_agent)
+                // We don't need any peer information cached.
+                .with_cache_size(0);
+            Identify::new(cfg)
+        };
+
+        Self {
+            ping: Ping::new(PingConfig::new()),
+            identify,
+            nodes_info: FnvHashMap::default(),
+            garbage_collect: Box::pin(interval(GARBAGE_COLLECT_INTERVAL)),
+            external_addresses: ExternalAddresses {
+                addresses: external_addresses,
+            },
+        }
+    }
+
+    /// Borrows `self` and returns a struct giving access to the information about a node.
+    ///
+    /// Returns `None` if we don't know anything about this node. Always returns `Some` for nodes
+    /// we're connected to, meaning that if `None` is returned then we're not connected to that
+    /// node.
+    pub fn node(&self, peer_id: &PeerId) -> Option<Node> {
+        self.nodes_info.get(peer_id).map(Node)
+    }
+
+    /// Inserts a ping time in the cache. Has no effect if we don't have any entry for that node,
+    /// which shouldn't happen.
+    fn handle_ping_report(&mut self, peer_id: &PeerId, ping_time: Duration) {
+        trace!(target: "sub-libp2p", "Ping time with {:?}: {:?}", peer_id, ping_time);
+        if let Some(entry) = self.nodes_info.get_mut(peer_id) {
+            entry.latest_ping = Some(ping_time);
+        } else {
+            error!(target: "sub-libp2p",
+				"Received ping from node we're not connected to {:?}", peer_id);
+        }
+    }
+
+    /// Inserts an identify record in the cache. Has no effect if we don't have any entry for that
+    /// node, which shouldn't happen.
+    fn handle_identify_report(&mut self, peer_id: &PeerId, info: &IdentifyInfo) {
+        trace!(target: "sub-libp2p", "Identified {:?} => {:?}", peer_id, info);
+        if let Some(entry) = self.nodes_info.get_mut(peer_id) {
+            entry.client_version = Some(info.agent_version.clone());
+        } else {
+            error!(target: "sub-libp2p",
+				"Received pong from node we're not connected to {:?}", peer_id);
+        }
+    }
+}
+
+/// Gives access to the information about a node.
+pub struct Node<'a>(&'a NodeInfo);
+
+impl<'a> Node<'a> {
+    /// Returns the endpoint of an established connection to the peer.
+    ///
+    /// Returns `None` if we are disconnected from the node.
+    pub fn endpoint(&self) -> Option<&'a ConnectedPoint> {
+        self.0.endpoints.first()
+    }
+
+    /// Returns the latest version information we know of.
+    pub fn client_version(&self) -> Option<&'a str> {
+        self.0.client_version.as_deref()
+    }
+
+    /// Returns the latest ping time we know of for this node. `None` if we never successfully
+    /// pinged this node.
+    pub fn latest_ping(&self) -> Option<Duration> {
+        self.0.latest_ping
+    }
+}
+
+/// Event that can be emitted by the behaviour.
+#[derive(Debug)]
+pub enum PeerInfoEvent {
+    /// We have obtained identity information from a peer, including the addresses it is listening
+    /// on.
+    Identified {
+        /// Id of the peer that has been identified.
+        peer_id: PeerId,
+        /// Information about the peer.
+        info: IdentifyInfo,
+    },
+}
+
+impl NetworkBehaviour for PeerInfoBehaviour {
+    type ConnectionHandler = IntoConnectionHandlerSelect<
+        <Ping as NetworkBehaviour>::ConnectionHandler,
+        <Identify as NetworkBehaviour>::ConnectionHandler,
+    >;
+    type OutEvent = PeerInfoEvent;
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        self.ping
+            .handle_pending_inbound_connection(connection_id, local_addr, remote_addr)?;
+        self.identify
+            .handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _maybe_peer: Option<PeerId>,
+        _addresses: &[Multiaddr],
+        _effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        // Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
+        // ensure that we don't return unwanted addresses.
+        Ok(Vec::new())
+    }
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        let ping_handler = self.ping.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )?;
+        let identify_handler = self.identify.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )?;
+        Ok(ping_handler.select(identify_handler))
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        let ping_handler = self.ping.handle_established_outbound_connection(
+            connection_id,
+            peer,
+            addr,
+            role_override,
+        )?;
+        let identify_handler = self.identify.handle_established_outbound_connection(
+            connection_id,
+            peer,
+            addr,
+            role_override,
+        )?;
+        Ok(ping_handler.select(identify_handler))
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        match event {
+            FromSwarm::ConnectionEstablished(
+                e @ ConnectionEstablished {
+                    peer_id, endpoint, ..
+                },
+            ) => {
+                self.ping
+                    .on_swarm_event(FromSwarm::ConnectionEstablished(e));
+                self.identify
+                    .on_swarm_event(FromSwarm::ConnectionEstablished(e));
+
+                match self.nodes_info.entry(peer_id) {
+                    Entry::Vacant(e) => {
+                        e.insert(NodeInfo::new(endpoint.clone()));
+                    }
+                    Entry::Occupied(e) => {
+                        let e = e.into_mut();
+                        if e.info_expire
+                            .as_ref()
+                            .map(|exp| *exp < Instant::now())
+                            .unwrap_or(false)
+                        {
+                            e.client_version = None;
+                            e.latest_ping = None;
+                        }
+                        e.info_expire = None;
+                        e.endpoints.push(endpoint.clone());
+                    }
+                }
+            }
+            FromSwarm::ConnectionClosed(ConnectionClosed {
+                peer_id,
+                connection_id,
+                endpoint,
+                handler,
+                remaining_established,
+            }) => {
+                let (ping_handler, identity_handler) = handler.into_inner();
+                self.ping
+                    .on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
+                        peer_id,
+                        connection_id,
+                        endpoint,
+                        handler: ping_handler,
+                        remaining_established,
+                    }));
+                self.identify
+                    .on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
+                        peer_id,
+                        connection_id,
+                        endpoint,
+                        handler: identity_handler,
+                        remaining_established,
+                    }));
+
+                if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
+                    if remaining_established == 0 {
+                        entry.info_expire = Some(Instant::now() + CACHE_EXPIRE);
+                    }
+                    entry.endpoints.retain(|ep| ep != endpoint)
+                } else {
+                    error!(target: "sub-libp2p",
+						"Unknown connection to {:?} closed: {:?}", peer_id, endpoint);
+                }
+            }
+            FromSwarm::DialFailure(DialFailure {
+                peer_id,
+                error,
+                connection_id,
+            }) => {
+                self.ping
+                    .on_swarm_event(FromSwarm::DialFailure(DialFailure {
+                        peer_id,
+                        error,
+                        connection_id,
+                    }));
+                self.identify
+                    .on_swarm_event(FromSwarm::DialFailure(DialFailure {
+                        peer_id,
+                        error,
+                        connection_id,
+                    }));
+            }
+            FromSwarm::ListenerClosed(e) => {
+                self.ping.on_swarm_event(FromSwarm::ListenerClosed(e));
+                self.identify.on_swarm_event(FromSwarm::ListenerClosed(e));
+            }
+            FromSwarm::ListenFailure(ListenFailure {
+                local_addr,
+                send_back_addr,
+                error,
+                connection_id,
+            }) => {
+                self.ping
+                    .on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
+                        local_addr,
+                        send_back_addr,
+                        error,
+                        connection_id,
+                    }));
+                self.identify
+                    .on_swarm_event(FromSwarm::ListenFailure(ListenFailure {
+                        local_addr,
+                        send_back_addr,
+                        error,
+                        connection_id,
+                    }));
+            }
+            FromSwarm::ListenerError(e) => {
+                self.ping.on_swarm_event(FromSwarm::ListenerError(e));
+                self.identify.on_swarm_event(FromSwarm::ListenerError(e));
+            }
+            FromSwarm::ExpiredExternalAddr(e) => {
+                self.ping.on_swarm_event(FromSwarm::ExpiredExternalAddr(e));
+                self.identify
+                    .on_swarm_event(FromSwarm::ExpiredExternalAddr(e));
+            }
+            FromSwarm::NewListener(e) => {
+                self.ping.on_swarm_event(FromSwarm::NewListener(e));
+                self.identify.on_swarm_event(FromSwarm::NewListener(e));
+            }
+            FromSwarm::ExpiredListenAddr(e) => {
+                self.ping.on_swarm_event(FromSwarm::ExpiredListenAddr(e));
+                self.identify
+                    .on_swarm_event(FromSwarm::ExpiredListenAddr(e));
+                self.external_addresses.remove(e.addr);
+            }
+            FromSwarm::NewExternalAddr(e) => {
+                self.ping.on_swarm_event(FromSwarm::NewExternalAddr(e));
+                self.identify.on_swarm_event(FromSwarm::NewExternalAddr(e));
+                self.external_addresses.add(e.addr.clone());
+            }
+            FromSwarm::AddressChange(
+                e @ AddressChange {
+                    peer_id, old, new, ..
+                },
+            ) => {
+                self.ping.on_swarm_event(FromSwarm::AddressChange(e));
+                self.identify.on_swarm_event(FromSwarm::AddressChange(e));
+
+                if let Some(entry) = self.nodes_info.get_mut(&peer_id) {
+                    if let Some(endpoint) = entry.endpoints.iter_mut().find(|e| e == &old) {
+                        *endpoint = new.clone();
+                    } else {
+                        error!(target: "sub-libp2p",
+							"Unknown address change for peer {:?} from {:?} to {:?}", peer_id, old, new);
+                    }
+                } else {
+                    error!(target: "sub-libp2p",
+						"Unknown peer {:?} to change address from {:?} to {:?}", peer_id, old, new);
+                }
+            }
+            FromSwarm::NewListenAddr(e) => {
+                self.ping.on_swarm_event(FromSwarm::NewListenAddr(e));
+                self.identify.on_swarm_event(FromSwarm::NewListenAddr(e));
+            }
+        }
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        match event {
+            Either::Left(event) => {
+                self.ping
+                    .on_connection_handler_event(peer_id, connection_id, event)
+            }
+            Either::Right(event) => {
+                self.identify
+                    .on_connection_handler_event(peer_id, connection_id, event)
+            }
+        }
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+        params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        loop {
+            match self.ping.poll(cx, params) {
+                Poll::Pending => break,
+                Poll::Ready(ToSwarm::GenerateEvent(ev)) => {
+                    if let PingEvent {
+                        peer,
+                        result: Ok(PingSuccess::Ping { rtt }),
+                    } = ev
+                    {
+                        self.handle_ping_report(&peer, rtt)
+                    }
+                }
+                Poll::Ready(ToSwarm::Dial { opts }) => return Poll::Ready(ToSwarm::Dial { opts }),
+                Poll::Ready(ToSwarm::NotifyHandler {
+                    peer_id,
+                    handler,
+                    event,
+                }) => {
+                    return Poll::Ready(ToSwarm::NotifyHandler {
+                        peer_id,
+                        handler,
+                        event: Either::Left(event),
+                    })
+                }
+                Poll::Ready(ToSwarm::ReportObservedAddr { address, score }) => {
+                    return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+                }
+                Poll::Ready(ToSwarm::CloseConnection {
+                    peer_id,
+                    connection,
+                }) => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+            }
+        }
+
+        loop {
+            match self.identify.poll(cx, params) {
+                Poll::Pending => break,
+                Poll::Ready(ToSwarm::GenerateEvent(event)) => match event {
+                    IdentifyEvent::Received { peer_id, info, .. } => {
+                        self.handle_identify_report(&peer_id, &info);
+                        let event = PeerInfoEvent::Identified { peer_id, info };
+                        return Poll::Ready(ToSwarm::GenerateEvent(event));
+                    }
+                    IdentifyEvent::Error { peer_id, error } => {
+                        debug!(target: "sub-libp2p", "Identification with peer {:?} failed => {}", peer_id, error)
+                    }
+                    IdentifyEvent::Pushed { .. } => {}
+                    IdentifyEvent::Sent { .. } => {}
+                },
+                Poll::Ready(ToSwarm::Dial { opts }) => return Poll::Ready(ToSwarm::Dial { opts }),
+                Poll::Ready(ToSwarm::NotifyHandler {
+                    peer_id,
+                    handler,
+                    event,
+                }) => {
+                    return Poll::Ready(ToSwarm::NotifyHandler {
+                        peer_id,
+                        handler,
+                        event: Either::Right(event),
+                    })
+                }
+                Poll::Ready(ToSwarm::ReportObservedAddr { address, score }) => {
+                    return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+                }
+                Poll::Ready(ToSwarm::CloseConnection {
+                    peer_id,
+                    connection,
+                }) => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+            }
+        }
+
+        while let Poll::Ready(Some(())) = self.garbage_collect.poll_next_unpin(cx) {
+            self.nodes_info.retain(|_, node| {
+                node.info_expire
+                    .as_ref()
+                    .map(|exp| *exp >= Instant::now())
+                    .unwrap_or(true)
+            });
+        }
+
+        Poll::Pending
+    }
+}
diff --git a/libp2p/src/peer_store.rs b/libp2p/src/peer_store.rs
new file mode 100644
index 000000000..e3ec4a60a
--- /dev/null
+++ b/libp2p/src/peer_store.rs
@@ -0,0 +1,475 @@
+// 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/>.
+
+//! [`PeerStore`] manages peer reputations and provides connection candidates to
+//! [`crate::protocol_controller::ProtocolController`].
+
+use libp2p::PeerId;
+use log::trace;
+use parking_lot::Mutex;
+use partial_sort::PartialSort;
+use sc_network_common::{role::ObservedRole, types::ReputationChange};
+use std::{
+    cmp::{Ord, Ordering, PartialOrd},
+    collections::{hash_map::Entry, HashMap, HashSet},
+    fmt::Debug,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use wasm_timer::Delay;
+
+use crate::protocol_controller::ProtocolHandle;
+
+/// Log target for this file.
+pub const LOG_TARGET: &str = "peerset";
+
+/// We don't accept nodes whose reputation is under this value.
+pub const BANNED_THRESHOLD: i32 = 82 * (i32::MIN / 100);
+/// Reputation change for a node when we get disconnected from it.
+const DISCONNECT_REPUTATION_CHANGE: i32 = -256;
+/// Relative decrement of a reputation value that is applied every second. I.e., for inverse
+/// decrement of 50 we decrease absolute value of the reputation by 1/50. This corresponds to a
+/// factor of `k = 0.98`. It takes ~ `ln(0.5) / ln(k)` seconds to reduce the reputation by half,
+/// or 34.3 seconds for the values above. In this setup the maximum allowed absolute value of
+/// `i32::MAX` becomes 0 in ~1100 seconds (actually less due to integer arithmetic).
+const INVERSE_DECREMENT: i32 = 50;
+/// Amount of time between the moment we last updated the [`PeerStore`] entry and the moment we
+/// remove it, once the reputation value reaches 0.
+const FORGET_AFTER: Duration = Duration::from_secs(3600);
+
+/// Trait providing peer reputation management and connection candidates.
+pub trait PeerStoreProvider: Debug + Send {
+    /// Check whether the peer is banned.
+    fn is_banned(&self, peer_id: &PeerId) -> bool;
+
+    /// Register a protocol handle to disconnect peers whose reputation drops below the threshold.
+    fn register_protocol(&self, protocol_handle: ProtocolHandle);
+
+    /// Report peer disconnection for reputation adjustment.
+    fn report_disconnect(&mut self, peer_id: PeerId);
+
+    /// Adjust peer reputation.
+    fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange);
+
+    /// Set peer role.
+    fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole);
+
+    /// Get peer reputation.
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32;
+
+    /// Get peer role, if available.
+    fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole>;
+
+    /// Get candidates with highest reputations for initiating outgoing connections.
+    fn outgoing_candidates(&self, count: usize, ignored: HashSet<&PeerId>) -> Vec<PeerId>;
+}
+
+/// Actual implementation of peer reputations and connection candidates provider.
+#[derive(Debug, Clone)]
+pub struct PeerStoreHandle {
+    inner: Arc<Mutex<PeerStoreInner>>,
+}
+
+impl PeerStoreProvider for PeerStoreHandle {
+    fn is_banned(&self, peer_id: &PeerId) -> bool {
+        self.inner.lock().is_banned(peer_id)
+    }
+
+    fn register_protocol(&self, protocol_handle: ProtocolHandle) {
+        self.inner.lock().register_protocol(protocol_handle);
+    }
+
+    fn report_disconnect(&mut self, peer_id: PeerId) {
+        self.inner.lock().report_disconnect(peer_id)
+    }
+
+    fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange) {
+        self.inner.lock().report_peer(peer_id, change)
+    }
+
+    fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole) {
+        self.inner.lock().set_peer_role(peer_id, role)
+    }
+
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
+        self.inner.lock().peer_reputation(peer_id)
+    }
+
+    fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole> {
+        self.inner.lock().peer_role(peer_id)
+    }
+
+    fn outgoing_candidates(&self, count: usize, ignored: HashSet<&PeerId>) -> Vec<PeerId> {
+        self.inner.lock().outgoing_candidates(count, ignored)
+    }
+}
+
+impl PeerStoreHandle {
+    /// Get the number of known peers.
+    ///
+    /// This number might not include some connected peers in rare cases when their reputation
+    /// was not updated for one hour, because their entries in [`PeerStore`] were dropped.
+    pub fn num_known_peers(&self) -> usize {
+        self.inner.lock().peers.len()
+    }
+
+    /// Add known peer.
+    pub fn add_known_peer(&mut self, peer_id: PeerId) {
+        self.inner.lock().add_known_peer(peer_id);
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+struct PeerInfo {
+    /// Reputation of the peer.
+    reputation: i32,
+
+    /// Instant when the peer was last updated.
+    last_updated: Instant,
+
+    /// Role of the peer, if known.
+    role: Option<ObservedRole>,
+}
+
+impl Default for PeerInfo {
+    fn default() -> Self {
+        Self {
+            reputation: 0,
+            last_updated: Instant::now(),
+            role: None,
+        }
+    }
+}
+
+impl PartialEq for PeerInfo {
+    fn eq(&self, other: &Self) -> bool {
+        self.reputation == other.reputation
+    }
+}
+
+impl Eq for PeerInfo {}
+
+impl Ord for PeerInfo {
+    // We define reverse order by reputation values.
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.reputation.cmp(&other.reputation).reverse()
+    }
+}
+
+impl PartialOrd for PeerInfo {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl PeerInfo {
+    fn is_banned(&self) -> bool {
+        self.reputation < BANNED_THRESHOLD
+    }
+
+    fn add_reputation(&mut self, increment: i32) {
+        self.reputation = self.reputation.saturating_add(increment);
+        self.bump_last_updated();
+    }
+
+    fn decay_reputation(&mut self, seconds_passed: u64) {
+        // Note that decaying the reputation value happens "on its own",
+        // so we don't do `bump_last_updated()`.
+        for _ in 0..seconds_passed {
+            let mut diff = self.reputation / INVERSE_DECREMENT;
+            if diff == 0 && self.reputation < 0 {
+                diff = -1;
+            } else if diff == 0 && self.reputation > 0 {
+                diff = 1;
+            }
+
+            self.reputation = self.reputation.saturating_sub(diff);
+
+            if self.reputation == 0 {
+                break;
+            }
+        }
+    }
+
+    fn bump_last_updated(&mut self) {
+        self.last_updated = Instant::now();
+    }
+}
+
+#[derive(Debug)]
+struct PeerStoreInner {
+    peers: HashMap<PeerId, PeerInfo>,
+    protocols: Vec<ProtocolHandle>,
+}
+
+impl PeerStoreInner {
+    fn is_banned(&self, peer_id: &PeerId) -> bool {
+        self.peers
+            .get(peer_id)
+            .map_or(false, |info| info.is_banned())
+    }
+
+    fn register_protocol(&mut self, protocol_handle: ProtocolHandle) {
+        self.protocols.push(protocol_handle);
+    }
+
+    fn report_disconnect(&mut self, peer_id: PeerId) {
+        let peer_info = self.peers.entry(peer_id).or_default();
+        peer_info.add_reputation(DISCONNECT_REPUTATION_CHANGE);
+
+        log::trace!(
+            target: LOG_TARGET,
+            "Peer {} disconnected, reputation: {:+} to {}",
+            peer_id,
+            DISCONNECT_REPUTATION_CHANGE,
+            peer_info.reputation,
+        );
+    }
+
+    fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange) {
+        let peer_info = self.peers.entry(peer_id).or_default();
+        peer_info.add_reputation(change.value);
+
+        if peer_info.reputation < BANNED_THRESHOLD {
+            self.protocols
+                .iter()
+                .for_each(|handle| handle.disconnect_peer(peer_id));
+
+            log::warn!(
+                target: LOG_TARGET,
+                "Report {}: {:+} to {}. Reason: {}. Banned, disconnecting.",
+                peer_id,
+                change.value,
+                peer_info.reputation,
+                change.reason,
+            );
+        } else {
+            log::trace!(
+                target: LOG_TARGET,
+                "Report {}: {:+} to {}. Reason: {}.",
+                peer_id,
+                change.value,
+                peer_info.reputation,
+                change.reason,
+            );
+        }
+    }
+
+    fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole) {
+        log::trace!(target: LOG_TARGET, "Set {peer_id} role to {role:?}");
+
+        match self.peers.entry(*peer_id) {
+            Entry::Occupied(mut entry) => {
+                entry.get_mut().role = Some(role);
+            }
+            Entry::Vacant(entry) => {
+                entry.insert(PeerInfo {
+                    role: Some(role),
+                    ..Default::default()
+                });
+            }
+        }
+    }
+
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
+        self.peers.get(peer_id).map_or(0, |info| info.reputation)
+    }
+
+    fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole> {
+        self.peers.get(peer_id).and_then(|info| info.role)
+    }
+
+    fn outgoing_candidates(&self, count: usize, ignored: HashSet<&PeerId>) -> Vec<PeerId> {
+        let mut candidates = self
+            .peers
+            .iter()
+            .filter_map(|(peer_id, info)| {
+                (!info.is_banned() && !ignored.contains(peer_id)).then_some((*peer_id, *info))
+            })
+            .collect::<Vec<_>>();
+        let count = std::cmp::min(count, candidates.len());
+        candidates.partial_sort(count, |(_, info1), (_, info2)| info1.cmp(info2));
+        candidates
+            .iter()
+            .take(count)
+            .map(|(peer_id, _)| *peer_id)
+            .collect()
+
+        // TODO: keep the peers sorted (in a "bi-multi-map"?) to not repeat sorting every time.
+    }
+
+    fn progress_time(&mut self, seconds_passed: u64) {
+        if seconds_passed == 0 {
+            return;
+        }
+
+        // Drive reputation values towards 0.
+        self.peers
+            .iter_mut()
+            .for_each(|(_, info)| info.decay_reputation(seconds_passed));
+
+        // Retain only entries with non-zero reputation values or not expired ones.
+        let now = Instant::now();
+        self.peers
+            .retain(|_, info| info.reputation != 0 || info.last_updated + FORGET_AFTER > now);
+    }
+
+    fn add_known_peer(&mut self, peer_id: PeerId) {
+        match self.peers.entry(peer_id) {
+            Entry::Occupied(mut e) => {
+                trace!(
+                    target: LOG_TARGET,
+                    "Trying to add an already known peer {peer_id}, bumping `last_updated`.",
+                );
+                e.get_mut().bump_last_updated();
+            }
+            Entry::Vacant(e) => {
+                trace!(target: LOG_TARGET, "Adding a new known peer {peer_id}.");
+                e.insert(PeerInfo::default());
+            }
+        }
+    }
+}
+
+/// Worker part of [`PeerStoreHandle`]
+#[derive(Debug)]
+pub struct PeerStore {
+    inner: Arc<Mutex<PeerStoreInner>>,
+}
+
+impl PeerStore {
+    /// Create a new peer store from the list of bootnodes.
+    pub fn new(bootnodes: Vec<PeerId>) -> Self {
+        PeerStore {
+            inner: Arc::new(Mutex::new(PeerStoreInner {
+                peers: bootnodes
+                    .into_iter()
+                    .map(|peer_id| (peer_id, PeerInfo::default()))
+                    .collect(),
+                protocols: Vec::new(),
+            })),
+        }
+    }
+
+    /// Get `PeerStoreHandle`.
+    pub fn handle(&self) -> PeerStoreHandle {
+        PeerStoreHandle {
+            inner: self.inner.clone(),
+        }
+    }
+
+    /// Drive the `PeerStore`, decaying reputation values over time and removing expired entries.
+    pub async fn run(self) {
+        let started = Instant::now();
+        let mut latest_time_update = started;
+
+        loop {
+            let now = Instant::now();
+            // We basically do `(now - self.latest_update).as_secs()`, except that by the way we do
+            // it we know that we're not going to miss seconds because of rounding to integers.
+            let seconds_passed = {
+                let elapsed_latest = latest_time_update - started;
+                let elapsed_now = now - started;
+                latest_time_update = now;
+                elapsed_now.as_secs() - elapsed_latest.as_secs()
+            };
+
+            self.inner.lock().progress_time(seconds_passed);
+            let _ = Delay::new(Duration::from_secs(1)).await;
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::PeerInfo;
+
+    #[test]
+    fn decaying_zero_reputation_yields_zero() {
+        let mut peer_info = PeerInfo::default();
+        assert_eq!(peer_info.reputation, 0);
+
+        peer_info.decay_reputation(1);
+        assert_eq!(peer_info.reputation, 0);
+
+        peer_info.decay_reputation(100_000);
+        assert_eq!(peer_info.reputation, 0);
+    }
+
+    #[test]
+    fn decaying_positive_reputation_decreases_it() {
+        const INITIAL_REPUTATION: i32 = 100;
+
+        let mut peer_info = PeerInfo {
+            reputation: INITIAL_REPUTATION,
+            ..Default::default()
+        };
+
+        peer_info.decay_reputation(1);
+        assert!(peer_info.reputation >= 0);
+        assert!(peer_info.reputation < INITIAL_REPUTATION);
+    }
+
+    #[test]
+    fn decaying_negative_reputation_increases_it() {
+        const INITIAL_REPUTATION: i32 = -100;
+
+        let mut peer_info = PeerInfo {
+            reputation: INITIAL_REPUTATION,
+            ..Default::default()
+        };
+
+        peer_info.decay_reputation(1);
+        assert!(peer_info.reputation <= 0);
+        assert!(peer_info.reputation > INITIAL_REPUTATION);
+    }
+
+    #[test]
+    fn decaying_max_reputation_finally_yields_zero() {
+        const INITIAL_REPUTATION: i32 = i32::MAX;
+        const SECONDS: u64 = 1000;
+
+        let mut peer_info = PeerInfo {
+            reputation: INITIAL_REPUTATION,
+            ..Default::default()
+        };
+
+        peer_info.decay_reputation(SECONDS / 2);
+        assert!(peer_info.reputation > 0);
+
+        peer_info.decay_reputation(SECONDS / 2);
+        assert_eq!(peer_info.reputation, 0);
+    }
+
+    #[test]
+    fn decaying_min_reputation_finally_yields_zero() {
+        const INITIAL_REPUTATION: i32 = i32::MIN;
+        const SECONDS: u64 = 1000;
+
+        let mut peer_info = PeerInfo {
+            reputation: INITIAL_REPUTATION,
+            ..Default::default()
+        };
+
+        peer_info.decay_reputation(SECONDS / 2);
+        assert!(peer_info.reputation < 0);
+
+        peer_info.decay_reputation(SECONDS / 2);
+        assert_eq!(peer_info.reputation, 0);
+    }
+}
diff --git a/libp2p/src/protocol.rs b/libp2p/src/protocol.rs
new file mode 100644
index 000000000..8027daee6
--- /dev/null
+++ b/libp2p/src/protocol.rs
@@ -0,0 +1,417 @@
+// 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::{
+    config, error,
+    peer_store::{PeerStoreHandle, PeerStoreProvider},
+    protocol_controller::{self, SetId},
+    service::traits::Direction,
+    types::ProtocolName,
+};
+
+use codec::Encode;
+use libp2p::{
+    core::Endpoint,
+    swarm::{
+        behaviour::FromSwarm, ConnectionDenied, ConnectionId, NetworkBehaviour, PollParameters,
+        THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
+    },
+    Multiaddr, PeerId,
+};
+use log::warn;
+
+use codec::DecodeAll;
+use prometheus_endpoint::Registry;
+use sc_network_common::role::Roles;
+use sc_utils::mpsc::TracingUnboundedReceiver;
+
+use std::{collections::HashSet, task::Poll};
+
+use notifications::{metrics, Notifications, NotificationsOut};
+
+pub(crate) use notifications::ProtocolHandle;
+
+pub use notifications::{
+    notification_service, NotificationsSink, NotifsHandlerError, ProtocolHandlePair, Ready,
+};
+
+pub mod notifications;
+
+pub mod message;
+
+/// Maximum size used for notifications in the block announce and transaction protocols.
+// Must be equal to `max(MAX_BLOCK_ANNOUNCE_SIZE, MAX_TRANSACTIONS_SIZE)`.
+pub(crate) const BLOCK_ANNOUNCES_TRANSACTIONS_SUBSTREAM_SIZE: u64 = 16 * 1024 * 1024;
+
+/// Identifier of the peerset for the block announces protocol.
+const HARDCODED_PEERSETS_SYNC: SetId = SetId::from(0);
+
+// Lock must always be taken in order declared here.
+pub struct Protocol {
+    /// Handles opening the unique substream and sending and receiving raw messages.
+    behaviour: Notifications,
+    /// List of notifications protocols that have been registered.
+    notification_protocols: Vec<ProtocolName>,
+    /// Handle to `PeerStore`.
+    peer_store_handle: PeerStoreHandle,
+    /// Streams for peers whose handshake couldn't be determined.
+    bad_handshake_streams: HashSet<PeerId>,
+    sync_handle: ProtocolHandle,
+}
+
+impl Protocol {
+    /// Create a new instance.
+    #[allow(clippy::result_large_err)]
+    pub(crate) fn new(
+        roles: Roles,
+        registry: &Option<Registry>,
+        notification_protocols: Vec<config::NonDefaultSetConfig>,
+        peer_store_handle: PeerStoreHandle,
+        protocol_controller_handles: Vec<protocol_controller::ProtocolHandle>,
+        from_protocol_controllers: TracingUnboundedReceiver<protocol_controller::Message>,
+    ) -> error::Result<(Self, Vec<ProtocolHandle>)> {
+        let (behaviour, notification_protocols, handles) = {
+            let installed_protocols = notification_protocols
+                .iter()
+                .map(|p| p.protocol_name().clone())
+                .collect::<Vec<_>>();
+
+            let (protocol_configs, mut handles): (Vec<_>, Vec<_>) = notification_protocols
+                .into_iter()
+                .map(|s| {
+                    let config = notifications::ProtocolConfig {
+                        name: s.protocol_name().clone(),
+                        fallback_names: s.fallback_names().cloned().collect(),
+                        handshake: s
+                            .handshake()
+                            .as_ref()
+                            .map_or(roles.encode(), |h| (*h).to_vec()),
+                        max_notification_size: s.max_notification_size(),
+                    };
+
+                    let (handle, command_stream) = s.take_protocol_handle().split();
+
+                    ((config, handle.clone(), command_stream), handle)
+                })
+                .unzip();
+
+            let metrics = registry
+                .as_ref()
+                .and_then(|registry| metrics::register(registry).ok());
+            handles.iter_mut().for_each(|handle| {
+                handle.set_metrics(metrics.clone());
+            });
+
+            (
+                Notifications::new(
+                    protocol_controller_handles,
+                    from_protocol_controllers,
+                    metrics,
+                    protocol_configs.into_iter(),
+                ),
+                installed_protocols,
+                handles,
+            )
+        };
+
+        let protocol = Self {
+            behaviour,
+            sync_handle: handles[0].clone(),
+            peer_store_handle,
+            notification_protocols,
+            bad_handshake_streams: HashSet::new(),
+        };
+
+        Ok((protocol, handles))
+    }
+
+    pub fn num_sync_peers(&self) -> usize {
+        self.sync_handle.num_peers()
+    }
+
+    /// Returns the list of all the peers we have an open channel to.
+    pub fn open_peers(&self) -> impl Iterator<Item = &PeerId> {
+        self.behaviour.open_peers()
+    }
+
+    /// Disconnects the given peer if we are connected to it.
+    pub fn disconnect_peer(&mut self, peer_id: &PeerId, protocol_name: ProtocolName) {
+        if let Some(position) = self
+            .notification_protocols
+            .iter()
+            .position(|p| *p == protocol_name)
+        {
+            // Note: no need to remove a peer from `self.peers` if we are dealing with sync
+            // protocol, because it will be done when handling
+            // `NotificationsOut::CustomProtocolClosed`.
+            self.behaviour
+                .disconnect_peer(peer_id, SetId::from(position));
+        } else {
+            warn!(target: "sub-libp2p", "disconnect_peer() with invalid protocol name")
+        }
+    }
+
+    /// Check if role is available for `peer_id` by attempt to decode the handshake to roles and if
+    /// that fails, check if the role has been registered to `PeerStore`.
+    fn role_available(&self, peer_id: &PeerId, handshake: &[u8]) -> bool {
+        match Roles::decode_all(&mut &handshake[..]) {
+            Ok(_) => true,
+            Err(_) => self.peer_store_handle.peer_role(peer_id).is_some(),
+        }
+    }
+}
+
+/// Outcome of an incoming custom message.
+#[derive(Debug)]
+#[must_use]
+pub enum CustomMessageOutcome {
+    /// Notification protocols have been opened with a remote.
+    NotificationStreamOpened {
+        remote: PeerId,
+        // protocol: ProtocolName,
+        set_id: SetId,
+        /// Direction of the stream.
+        direction: Direction,
+        /// See [`crate::Event::NotificationStreamOpened::negotiated_fallback`].
+        negotiated_fallback: Option<ProtocolName>,
+        /// Received handshake.
+        received_handshake: Vec<u8>,
+        /// Notification sink.
+        notifications_sink: NotificationsSink,
+    },
+    /// The [`NotificationsSink`] of some notification protocols need an update.
+    NotificationStreamReplaced {
+        // Peer ID.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+        /// New notification sink.
+        notifications_sink: NotificationsSink,
+    },
+    /// Notification protocols have been closed with a remote.
+    NotificationStreamClosed {
+        // Peer ID.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+    },
+    /// Messages have been received on one or more notifications protocols.
+    NotificationsReceived {
+        // Peer ID.
+        remote: PeerId,
+        /// Set ID.
+        set_id: SetId,
+        /// Received notification.
+        notification: Vec<u8>,
+    },
+}
+
+impl NetworkBehaviour for Protocol {
+    type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
+    type OutEvent = CustomMessageOutcome;
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.behaviour.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.behaviour.handle_established_outbound_connection(
+            connection_id,
+            peer,
+            addr,
+            role_override,
+        )
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _maybe_peer: Option<PeerId>,
+        _addresses: &[Multiaddr],
+        _effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        // Only `Discovery::handle_pending_outbound_connection` must be returning addresses to
+        // ensure that we don't return unwanted addresses.
+        Ok(Vec::new())
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        self.behaviour.on_swarm_event(event);
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        self.behaviour
+            .on_connection_handler_event(peer_id, connection_id, event);
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut std::task::Context,
+        params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        let event = match self.behaviour.poll(cx, params) {
+            Poll::Pending => return Poll::Pending,
+            Poll::Ready(ToSwarm::GenerateEvent(ev)) => ev,
+            Poll::Ready(ToSwarm::Dial { opts }) => return Poll::Ready(ToSwarm::Dial { opts }),
+            Poll::Ready(ToSwarm::NotifyHandler {
+                peer_id,
+                handler,
+                event,
+            }) => {
+                return Poll::Ready(ToSwarm::NotifyHandler {
+                    peer_id,
+                    handler,
+                    event,
+                })
+            }
+            Poll::Ready(ToSwarm::ReportObservedAddr { address, score }) => {
+                return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+            }
+            Poll::Ready(ToSwarm::CloseConnection {
+                peer_id,
+                connection,
+            }) => {
+                return Poll::Ready(ToSwarm::CloseConnection {
+                    peer_id,
+                    connection,
+                })
+            }
+        };
+
+        let outcome = match event {
+            NotificationsOut::CustomProtocolOpen {
+                peer_id,
+                set_id,
+                direction,
+                received_handshake,
+                notifications_sink,
+                negotiated_fallback,
+                ..
+            } => {
+                if set_id == HARDCODED_PEERSETS_SYNC {
+                    let _ = self.sync_handle.report_substream_opened(
+                        peer_id,
+                        direction,
+                        received_handshake,
+                        negotiated_fallback,
+                        notifications_sink,
+                    );
+                    None
+                } else {
+                    match self.role_available(&peer_id, &received_handshake) {
+                        true => Some(CustomMessageOutcome::NotificationStreamOpened {
+                            remote: peer_id,
+                            set_id,
+                            direction,
+                            negotiated_fallback,
+                            received_handshake,
+                            notifications_sink,
+                        }),
+                        false => {
+                            self.bad_handshake_streams.insert(peer_id);
+                            None
+                        }
+                    }
+                }
+            }
+            NotificationsOut::CustomProtocolReplaced {
+                peer_id,
+                notifications_sink,
+                set_id,
+            } => {
+                if set_id == HARDCODED_PEERSETS_SYNC {
+                    let _ = self
+                        .sync_handle
+                        .report_notification_sink_replaced(peer_id, notifications_sink);
+                    None
+                } else {
+                    (!self.bad_handshake_streams.contains(&peer_id)).then_some(
+                        CustomMessageOutcome::NotificationStreamReplaced {
+                            remote: peer_id,
+                            set_id,
+                            notifications_sink,
+                        },
+                    )
+                }
+            }
+            NotificationsOut::CustomProtocolClosed { peer_id, set_id } => {
+                if set_id == HARDCODED_PEERSETS_SYNC {
+                    let _ = self.sync_handle.report_substream_closed(peer_id);
+                    None
+                } else {
+                    (!self.bad_handshake_streams.remove(&peer_id)).then_some(
+                        CustomMessageOutcome::NotificationStreamClosed {
+                            remote: peer_id,
+                            set_id,
+                        },
+                    )
+                }
+            }
+            NotificationsOut::Notification {
+                peer_id,
+                set_id,
+                message,
+            } => {
+                if set_id == HARDCODED_PEERSETS_SYNC {
+                    let _ = self
+                        .sync_handle
+                        .report_notification_received(peer_id, message.freeze().into());
+                    None
+                } else {
+                    (!self.bad_handshake_streams.contains(&peer_id)).then_some(
+                        CustomMessageOutcome::NotificationsReceived {
+                            remote: peer_id,
+                            set_id,
+                            notification: message.freeze().into(),
+                        },
+                    )
+                }
+            }
+        };
+
+        match outcome {
+            Some(event) => Poll::Ready(ToSwarm::GenerateEvent(event)),
+            None => {
+                cx.waker().wake_by_ref();
+                Poll::Pending
+            }
+        }
+    }
+}
diff --git a/libp2p/src/protocol/message.rs b/libp2p/src/protocol/message.rs
new file mode 100644
index 000000000..4b2cc17d5
--- /dev/null
+++ b/libp2p/src/protocol/message.rs
@@ -0,0 +1,288 @@
+// 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/>.
+
+//! Network packet message types. These get serialized and put into the lower level protocol
+//! payload.
+
+use codec::{Decode, Encode};
+use sc_client_api::StorageProof;
+use sc_network_common::message::RequestId;
+use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
+
+/// Type alias for using the message type using block type parameters.
+#[allow(unused)]
+pub type Message<B> = generic::Message<
+    <B as BlockT>::Header,
+    <B as BlockT>::Hash,
+    <<B as BlockT>::Header as HeaderT>::Number,
+    <B as BlockT>::Extrinsic,
+>;
+
+/// Remote call response.
+#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+pub struct RemoteCallResponse {
+    /// Id of a request this response was made for.
+    pub id: RequestId,
+    /// Execution proof.
+    pub proof: StorageProof,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+/// Remote read response.
+pub struct RemoteReadResponse {
+    /// Id of a request this response was made for.
+    pub id: RequestId,
+    /// Read proof.
+    pub proof: StorageProof,
+}
+
+/// Generic types.
+pub mod generic {
+    use super::{RemoteCallResponse, RemoteReadResponse};
+    use codec::{Decode, Encode, Input};
+    use sc_client_api::StorageProof;
+    use sc_network_common::{
+        message::RequestId,
+        role::Roles,
+        sync::message::{
+            generic::{BlockRequest, BlockResponse},
+            BlockAnnounce,
+        },
+    };
+    use sp_runtime::ConsensusEngineId;
+
+    /// Consensus is mostly opaque to us
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub struct ConsensusMessage {
+        /// Identifies consensus engine.
+        pub protocol: ConsensusEngineId,
+        /// Message payload.
+        pub data: Vec<u8>,
+    }
+
+    /// A network message.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub enum Message<Header, Hash, Number, Extrinsic> {
+        /// Status packet.
+        Status(Status<Hash, Number>),
+        /// Block request.
+        BlockRequest(BlockRequest<Hash, Number>),
+        /// Block response.
+        BlockResponse(BlockResponse<Header, Hash, Extrinsic>),
+        /// Block announce.
+        BlockAnnounce(BlockAnnounce<Header>),
+        /// Consensus protocol message.
+        // NOTE: index is incremented by 1 due to transaction-related
+        // message that was removed
+        #[codec(index = 6)]
+        Consensus(ConsensusMessage),
+        /// Remote method call request.
+        RemoteCallRequest(RemoteCallRequest<Hash>),
+        /// Remote method call response.
+        RemoteCallResponse(RemoteCallResponse),
+        /// Remote storage read request.
+        RemoteReadRequest(RemoteReadRequest<Hash>),
+        /// Remote storage read response.
+        RemoteReadResponse(RemoteReadResponse),
+        /// Remote header request.
+        RemoteHeaderRequest(RemoteHeaderRequest<Number>),
+        /// Remote header response.
+        RemoteHeaderResponse(RemoteHeaderResponse<Header>),
+        /// Remote changes request.
+        RemoteChangesRequest(RemoteChangesRequest<Hash>),
+        /// Remote changes response.
+        RemoteChangesResponse(RemoteChangesResponse<Number, Hash>),
+        /// Remote child storage read request.
+        RemoteReadChildRequest(RemoteReadChildRequest<Hash>),
+        /// Batch of consensus protocol messages.
+        // NOTE: index is incremented by 2 due to finality proof related
+        // messages that were removed.
+        #[codec(index = 17)]
+        ConsensusBatch(Vec<ConsensusMessage>),
+    }
+
+    /// Status sent on connection.
+    // TODO https://github.com/paritytech/substrate/issues/4674: replace the `Status`
+    // struct with this one, after waiting a few releases beyond `NetworkSpecialization`'s
+    // removal (https://github.com/paritytech/substrate/pull/4665)
+    //
+    // and set MIN_VERSION to 6.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    pub struct CompactStatus<Hash, Number> {
+        /// Protocol version.
+        pub version: u32,
+        /// Minimum supported version.
+        pub min_supported_version: u32,
+        /// Supported roles.
+        pub roles: Roles,
+        /// Best block number.
+        pub best_number: Number,
+        /// Best block hash.
+        pub best_hash: Hash,
+        /// Genesis block hash.
+        pub genesis_hash: Hash,
+    }
+
+    /// Status sent on connection.
+    #[derive(Debug, PartialEq, Eq, Clone, Encode)]
+    pub struct Status<Hash, Number> {
+        /// Protocol version.
+        pub version: u32,
+        /// Minimum supported version.
+        pub min_supported_version: u32,
+        /// Supported roles.
+        pub roles: Roles,
+        /// Best block number.
+        pub best_number: Number,
+        /// Best block hash.
+        pub best_hash: Hash,
+        /// Genesis block hash.
+        pub genesis_hash: Hash,
+        /// DEPRECATED. Chain-specific status.
+        pub chain_status: Vec<u8>,
+    }
+
+    impl<Hash: Decode, Number: Decode> Decode for Status<Hash, Number> {
+        fn decode<I: Input>(value: &mut I) -> Result<Self, codec::Error> {
+            const LAST_CHAIN_STATUS_VERSION: u32 = 5;
+            let compact = CompactStatus::decode(value)?;
+            let chain_status = match <Vec<u8>>::decode(value) {
+                Ok(v) => v,
+                Err(e) => {
+                    if compact.version <= LAST_CHAIN_STATUS_VERSION {
+                        return Err(e);
+                    } else {
+                        Vec::new()
+                    }
+                }
+            };
+
+            let CompactStatus {
+                version,
+                min_supported_version,
+                roles,
+                best_number,
+                best_hash,
+                genesis_hash,
+            } = compact;
+
+            Ok(Self {
+                version,
+                min_supported_version,
+                roles,
+                best_number,
+                best_hash,
+                genesis_hash,
+                chain_status,
+            })
+        }
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote call request.
+    pub struct RemoteCallRequest<H> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Block at which to perform call.
+        pub block: H,
+        /// Method name.
+        pub method: String,
+        /// Call data.
+        pub data: Vec<u8>,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote storage read request.
+    pub struct RemoteReadRequest<H> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Block at which to perform call.
+        pub block: H,
+        /// Storage key.
+        pub keys: Vec<Vec<u8>>,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote storage read child request.
+    pub struct RemoteReadChildRequest<H> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Block at which to perform call.
+        pub block: H,
+        /// Child Storage key.
+        pub storage_key: Vec<u8>,
+        /// Storage key.
+        pub keys: Vec<Vec<u8>>,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote header request.
+    pub struct RemoteHeaderRequest<N> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Block number to request header for.
+        pub block: N,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote header response.
+    pub struct RemoteHeaderResponse<Header> {
+        /// Id of a request this response was made for.
+        pub id: RequestId,
+        /// Header. None if proof generation has failed (e.g. header is unknown).
+        pub header: Option<Header>,
+        /// Header proof.
+        pub proof: StorageProof,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote changes request.
+    pub struct RemoteChangesRequest<H> {
+        /// Unique request id.
+        pub id: RequestId,
+        /// Hash of the first block of the range (including first) where changes are requested.
+        pub first: H,
+        /// Hash of the last block of the range (including last) where changes are requested.
+        pub last: H,
+        /// Hash of the first block for which the requester has the changes trie root. All other
+        /// affected roots must be proved.
+        pub min: H,
+        /// Hash of the last block that we can use when querying changes.
+        pub max: H,
+        /// Storage child node key which changes are requested.
+        pub storage_key: Option<Vec<u8>>,
+        /// Storage key which changes are requested.
+        pub key: Vec<u8>,
+    }
+
+    #[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
+    /// Remote changes response.
+    pub struct RemoteChangesResponse<N, H> {
+        /// Id of a request this response was made for.
+        pub id: RequestId,
+        /// Proof has been generated using block with this number as a max block. Should be
+        /// less than or equal to the RemoteChangesRequest::max block number.
+        pub max: N,
+        /// Changes proof.
+        pub proof: Vec<Vec<u8>>,
+        /// Changes tries roots missing on the requester' node.
+        pub roots: Vec<(N, H)>,
+        /// Missing changes tries roots proof.
+        pub roots_proof: StorageProof,
+    }
+}
diff --git a/libp2p/src/protocol/notifications.rs b/libp2p/src/protocol/notifications.rs
new file mode 100644
index 000000000..2fdc50c6d
--- /dev/null
+++ b/libp2p/src/protocol/notifications.rs
@@ -0,0 +1,34 @@
+// 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/>.
+
+//! Implementation of libp2p's `NetworkBehaviour` trait that establishes communications and opens
+//! notifications substreams.
+
+pub use self::{
+    behaviour::{Notifications, NotificationsOut, ProtocolConfig},
+    handler::{NotificationsSink, NotifsHandlerError, Ready},
+    service::{notification_service, NotificationHandle, ProtocolHandlePair},
+};
+
+pub use self::service::{metrics, ProtocolHandle};
+
+mod behaviour;
+mod handler;
+pub mod service;
+mod tests;
+mod upgrade;
diff --git a/libp2p/src/protocol/notifications/behaviour.rs b/libp2p/src/protocol/notifications/behaviour.rs
new file mode 100644
index 000000000..1fa756e77
--- /dev/null
+++ b/libp2p/src/protocol/notifications/behaviour.rs
@@ -0,0 +1,5746 @@
+// 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::{
+    protocol::notifications::{
+        handler::{self, NotificationsSink, NotifsHandler, NotifsHandlerIn, NotifsHandlerOut},
+        service::{metrics, NotificationCommand, ProtocolHandle, ValidationCallResult},
+    },
+    protocol_controller::{self, IncomingIndex, Message, SetId},
+    service::traits::{Direction, ValidationResult},
+    types::ProtocolName,
+};
+
+use bytes::BytesMut;
+use fnv::FnvHashMap;
+use futures::{future::BoxFuture, prelude::*, stream::FuturesUnordered};
+use libp2p::{
+    core::{ConnectedPoint, Endpoint, Multiaddr},
+    swarm::{
+        behaviour::{ConnectionClosed, ConnectionEstablished, DialFailure, FromSwarm},
+        ConnectionDenied, ConnectionId, DialError, NetworkBehaviour, NotifyHandler, PollParameters,
+        THandler, THandlerInEvent, THandlerOutEvent, ToSwarm,
+    },
+    PeerId,
+};
+use log::{debug, error, trace, warn};
+use parking_lot::RwLock;
+use rand::distributions::{Distribution as _, Uniform};
+use sc_utils::mpsc::TracingUnboundedReceiver;
+use smallvec::SmallVec;
+use tokio::sync::oneshot::error::RecvError;
+use tokio_stream::StreamMap;
+
+use std::{
+    cmp,
+    collections::{hash_map::Entry, VecDeque},
+    mem,
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+    time::{Duration, Instant},
+};
+
+/// Type representing a pending substream validation.
+type PendingInboundValidation =
+    BoxFuture<'static, (Result<ValidationResult, RecvError>, IncomingIndex)>;
+
+/// Logging target for the file.
+const LOG_TARGET: &str = "sub-libp2p";
+
+/// Network behaviour that handles opening substreams for custom protocols with other peers.
+///
+/// # How it works
+///
+/// The role of the `Notifications` is to synchronize the following components:
+///
+/// - The libp2p swarm that opens new connections and reports disconnects.
+/// - The connection handler (see `group.rs`) that handles individual connections.
+/// - The peerset manager (PSM) that requests links to peers to be established or broken.
+/// - The external API, that requires knowledge of the links that have been established.
+///
+/// In the state machine below, each `PeerId` is attributed one of these states:
+///
+/// - [`PeerState::Requested`]: No open connection, but requested by the peerset. Currently dialing.
+/// - [`PeerState::Disabled`]: Has open TCP connection(s) unbeknownst to the peerset. No substream
+///   is open.
+/// - [`PeerState::Enabled`]: Has open TCP connection(s), acknowledged by the peerset.
+///   - Notifications substreams are open on at least one connection, and external API has been
+///     notified.
+///   - Notifications substreams aren't open.
+/// - [`PeerState::Incoming`]: Has open TCP connection(s) and remote would like to open substreams.
+///   Peerset has been asked to attribute an inbound slot.
+///
+/// In addition to these states, there also exists a "banning" system. If we fail to dial a peer,
+/// we back-off for a few seconds. If the PSM requests connecting to a peer that is currently
+/// backed-off, the next dialing attempt is delayed until after the ban expires. However, the PSM
+/// will still consider the peer to be connected. This "ban" is thus not a ban in a strict sense:
+/// if a backed-off peer tries to connect, the connection is accepted. A ban only delays dialing
+/// attempts.
+///
+/// There may be multiple connections to a peer. The status of a peer on
+/// the API of this behaviour and towards the peerset manager is aggregated in
+/// the following way:
+///
+///   1. The enabled/disabled status is the same across all connections, as decided by the peerset
+///      manager.
+///   2. `send_packet` and `write_notification` always send all data over the same connection to
+///      preserve the ordering provided by the transport, as long as that connection is open. If it
+///      closes, a second open connection may take over, if one exists, but that case should be no
+///      different than a single connection failing and being re-established in terms of potential
+///      reordering and dropped messages. Messages can be received on any connection.
+///   3. The behaviour reports `NotificationsOut::CustomProtocolOpen` when the first connection
+///      reports `NotifsHandlerOut::OpenResultOk`.
+///   4. The behaviour reports `NotificationsOut::CustomProtocolClosed` when the last connection
+///      reports `NotifsHandlerOut::ClosedResult`.
+///
+/// In this way, the number of actual established connections to the peer is
+/// an implementation detail of this behaviour. Note that, in practice and at
+/// the time of this writing, there may be at most two connections to a peer
+/// and only as a result of simultaneous dialing. However, the implementation
+/// accommodates for any number of connections.
+pub struct Notifications {
+    /// Notification protocols. Entries never change after initialization.
+    notif_protocols: Vec<handler::ProtocolConfig>,
+
+    /// Protocol handles.
+    protocol_handles: Vec<ProtocolHandle>,
+
+    // Command streams.
+    command_streams: StreamMap<usize, Box<dyn Stream<Item = NotificationCommand> + Send + Unpin>>,
+
+    /// Protocol controllers are responsible for peer connections management.
+    protocol_controller_handles: Vec<protocol_controller::ProtocolHandle>,
+
+    /// Receiver for instructions about who to connect to or disconnect from.
+    from_protocol_controllers: TracingUnboundedReceiver<Message>,
+
+    /// List of peers in our state.
+    peers: FnvHashMap<(PeerId, SetId), PeerState>,
+
+    /// The elements in `peers` occasionally contain `Delay` objects that we would normally have
+    /// to be polled one by one. In order to avoid doing so, as an optimization, every `Delay` is
+    /// instead put inside of `delays` and reference by a [`DelayId`]. This stream
+    /// yields `PeerId`s whose `DelayId` is potentially ready.
+    ///
+    /// By design, we never remove elements from this list. Elements are removed only when the
+    /// `Delay` triggers. As such, this stream may produce obsolete elements.
+    delays: stream::FuturesUnordered<DelaysT>,
+
+    /// [`DelayId`] to assign to the next delay.
+    next_delay_id: DelayId,
+
+    /// List of incoming messages we have sent to the peer set manager and that are waiting for an
+    /// answer.
+    incoming: SmallVec<[IncomingPeer; 6]>,
+
+    /// We generate indices to identify incoming connections. This is the next value for the index
+    /// to use when a connection is incoming.
+    next_incoming_index: IncomingIndex,
+
+    /// Events to produce from `poll()`.
+    events: VecDeque<ToSwarm<NotificationsOut, NotifsHandlerIn>>,
+
+    /// Pending inbound substream validations.
+    //
+    // NOTE: it's possible to read a stale response from `pending_inbound_validations`
+    // as the substream may get closed by the remote peer before the protocol has had
+    // a chance to validate it. [`Notifications`] must compare the `crate::peerset::IncomingIndex`
+    // returned by the completed future against the `crate::peerset::IncomingIndex` stored in
+    // `PeerState::Incoming` to check whether the completed future is stale or not.
+    pending_inbound_validations: FuturesUnordered<PendingInboundValidation>,
+
+    /// Metrics for notifications.
+    metrics: Option<metrics::Metrics>,
+}
+
+#[allow(private_interfaces)]
+pub type DelaysT = Pin<Box<dyn Future<Output = (DelayId, PeerId, SetId)> + Send>>;
+
+/// Configuration for a notifications protocol.
+#[derive(Debug, Clone)]
+pub struct ProtocolConfig {
+    /// Name of the protocol.
+    pub name: ProtocolName,
+    /// Names of the protocol to use if the main one isn't available.
+    pub fallback_names: Vec<ProtocolName>,
+    /// Handshake of the protocol.
+    pub handshake: Vec<u8>,
+    /// Maximum allowed size for a notification.
+    pub max_notification_size: u64,
+}
+
+/// Identifier for a delay firing.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct DelayId(u64);
+
+/// State of a peer we're connected to.
+///
+/// The variants correspond to the state of the peer w.r.t. the peerset.
+#[derive(Debug)]
+enum PeerState {
+    /// State is poisoned. This is a temporary state for a peer and we should always switch back
+    /// to it later. If it is found in the wild, that means there was either a panic or a bug in
+    /// the state machine code.
+    Poisoned,
+
+    /// The peer misbehaved. If the PSM wants us to connect to this peer, we will add an artificial
+    /// delay to the connection.
+    Backoff {
+        /// When the ban expires. For clean-up purposes. References an entry in `delays`.
+        timer: DelayId,
+        /// Until when the peer is backed-off.
+        timer_deadline: Instant,
+    },
+
+    /// The peerset requested that we connect to this peer. We are currently not connected.
+    PendingRequest {
+        /// When to actually start dialing. References an entry in `delays`.
+        timer: DelayId,
+        /// When the `timer` will trigger.
+        timer_deadline: Instant,
+    },
+
+    /// The peerset requested that we connect to this peer. We are currently dialing this peer.
+    Requested,
+
+    /// We are connected to this peer but the peerset hasn't requested it or has denied it.
+    ///
+    /// The handler is either in the closed state, or a `Close` message has been sent to it and
+    /// hasn't been answered yet.
+    Disabled {
+        /// If `Some`, any connection request from the peerset to this peer is delayed until the
+        /// given `Instant`.
+        backoff_until: Option<Instant>,
+
+        /// List of connections with this peer, and their state.
+        connections: SmallVec<[(ConnectionId, ConnectionState); crate::MAX_CONNECTIONS_PER_PEER]>,
+    },
+
+    /// We are connected to this peer. The peerset has requested a connection to this peer, but
+    /// it is currently in a "backed-off" phase. The state will switch to `Enabled` once the timer
+    /// expires.
+    ///
+    /// The handler is either in the closed state, or a `Close` message has been sent to it and
+    /// hasn't been answered yet.
+    ///
+    /// The handler will be opened when `timer` fires.
+    DisabledPendingEnable {
+        /// When to enable this remote. References an entry in `delays`.
+        timer: DelayId,
+        /// When the `timer` will trigger.
+        timer_deadline: Instant,
+
+        /// List of connections with this peer, and their state.
+        connections: SmallVec<[(ConnectionId, ConnectionState); crate::MAX_CONNECTIONS_PER_PEER]>,
+    },
+
+    /// We are connected to this peer and the peerset has accepted it.
+    Enabled {
+        /// List of connections with this peer, and their state.
+        connections: SmallVec<[(ConnectionId, ConnectionState); crate::MAX_CONNECTIONS_PER_PEER]>,
+    },
+
+    /// We are connected to this peer. We have received an `OpenDesiredByRemote` from one of the
+    /// handlers and forwarded that request to the peerset. The connection handlers are waiting for
+    /// a response, i.e. to be opened or closed based on whether the peerset accepts or rejects
+    /// the peer.
+    Incoming {
+        /// If `Some`, any dial attempts to this peer are delayed until the given `Instant`.
+        backoff_until: Option<Instant>,
+
+        /// Incoming index tracking this connection.
+        incoming_index: IncomingIndex,
+
+        /// Peerset has signaled it wants the substream closed.
+        peerset_rejected: bool,
+
+        /// List of connections with this peer, and their state.
+        connections: SmallVec<[(ConnectionId, ConnectionState); crate::MAX_CONNECTIONS_PER_PEER]>,
+    },
+}
+
+impl PeerState {
+    /// True if there exists an established connection to the peer
+    /// that is open for custom protocol traffic.
+    fn is_open(&self) -> bool {
+        self.get_open().is_some()
+    }
+
+    /// Returns the [`NotificationsSink`] of the first established connection
+    /// that is open for custom protocol traffic.
+    fn get_open(&self) -> Option<&NotificationsSink> {
+        match self {
+            Self::Enabled { connections, .. } => connections.iter().find_map(|(_, s)| match s {
+                ConnectionState::Open(s) => Some(s),
+                _ => None,
+            }),
+            _ => None,
+        }
+    }
+}
+
+/// State of the handler of a single connection visible from this state machine.
+#[derive(Debug)]
+enum ConnectionState {
+    /// Connection is in the `Closed` state, meaning that the remote hasn't requested anything.
+    Closed,
+
+    /// Connection is either in the `Open` or the `Closed` state, but a
+    /// [`NotifsHandlerIn::Close`] message has been sent. Waiting for this message to be
+    /// acknowledged through a [`NotifsHandlerOut::CloseResult`].
+    Closing,
+
+    /// Connection is in the `Closed` state but a [`NotifsHandlerIn::Open`] message has been sent.
+    /// An `OpenResultOk`/`OpenResultErr` message is expected.
+    Opening,
+
+    /// Connection is in the `Closed` state but a [`NotifsHandlerIn::Open`] message then a
+    /// [`NotifsHandlerIn::Close`] message has been sent. An `OpenResultOk`/`OpenResultErr` message
+    /// followed with a `CloseResult` message are expected.
+    OpeningThenClosing,
+
+    /// Connection is in the `Closed` state, but a [`NotifsHandlerOut::OpenDesiredByRemote`]
+    /// message has been received, meaning that the remote wants to open a substream.
+    OpenDesiredByRemote,
+
+    /// Connection is in the `Open` state.
+    ///
+    /// The external API is notified of a channel with this peer if any of its connection is in
+    /// this state.
+    Open(NotificationsSink),
+}
+
+/// State of an "incoming" message sent to the peer set manager.
+#[derive(Debug)]
+struct IncomingPeer {
+    /// Id of the remote peer of the incoming substream.
+    peer_id: PeerId,
+    /// Id of the set the incoming substream would belong to.
+    set_id: SetId,
+    /// If true, this "incoming" still corresponds to an actual connection. If false, then the
+    /// connection corresponding to it has been closed or replaced already.
+    alive: bool,
+    /// Id that the we sent to the peerset.
+    incoming_id: IncomingIndex,
+    /// Received handshake.
+    handshake: Vec<u8>,
+}
+
+/// Event that can be emitted by the `Notifications`.
+#[derive(Debug)]
+pub enum NotificationsOut {
+    /// Opened a custom protocol with the remote.
+    CustomProtocolOpen {
+        /// Id of the peer we are connected to.
+        peer_id: PeerId,
+        /// Peerset set ID the substream is tied to.
+        set_id: SetId,
+        /// Direction of the stream.
+        direction: Direction,
+        /// If `Some`, a fallback protocol name has been used rather the main protocol name.
+        /// Always matches one of the fallback names passed at initialization.
+        negotiated_fallback: Option<ProtocolName>,
+        /// Handshake that was sent to us.
+        /// This is normally a "Status" message, but this is out of the concern of this code.
+        received_handshake: Vec<u8>,
+        /// Object that permits sending notifications to the peer.
+        notifications_sink: NotificationsSink,
+        /// Is the connection inbound.
+        inbound: bool,
+    },
+
+    /// The [`NotificationsSink`] object used to send notifications with the given peer must be
+    /// replaced with a new one.
+    ///
+    /// This event is typically emitted when a transport-level connection is closed and we fall
+    /// back to a secondary connection.
+    CustomProtocolReplaced {
+        /// Id of the peer we are connected to.
+        peer_id: PeerId,
+        /// Peerset set ID the substream is tied to.
+        set_id: SetId,
+        /// Replacement for the previous [`NotificationsSink`].
+        notifications_sink: NotificationsSink,
+    },
+
+    /// Closed a custom protocol with the remote. The existing [`NotificationsSink`] should
+    /// be dropped.
+    CustomProtocolClosed {
+        /// Id of the peer we were connected to.
+        peer_id: PeerId,
+        /// Peerset set ID the substream was tied to.
+        set_id: SetId,
+    },
+
+    /// Receives a message on a custom protocol substream.
+    ///
+    /// Also concerns received notifications for the notifications API.
+    Notification {
+        /// Id of the peer the message came from.
+        peer_id: PeerId,
+        /// Peerset set ID the substream is tied to.
+        set_id: SetId,
+        /// Message that has been received.
+        message: BytesMut,
+    },
+}
+
+impl Notifications {
+    /// Creates a `CustomProtos`.
+    pub(crate) fn new(
+        protocol_controller_handles: Vec<protocol_controller::ProtocolHandle>,
+        from_protocol_controllers: TracingUnboundedReceiver<Message>,
+        metrics: Option<metrics::Metrics>,
+        notif_protocols: impl Iterator<
+            Item = (
+                ProtocolConfig,
+                ProtocolHandle,
+                Box<dyn Stream<Item = NotificationCommand> + Send + Unpin>,
+            ),
+        >,
+    ) -> Self {
+        let (notif_protocols, protocol_handles): (Vec<_>, Vec<_>) = notif_protocols
+            .map(|(cfg, protocol_handle, command_stream)| {
+                (
+                    handler::ProtocolConfig {
+                        name: cfg.name,
+                        fallback_names: cfg.fallback_names,
+                        handshake: Arc::new(RwLock::new(cfg.handshake)),
+                        max_notification_size: cfg.max_notification_size,
+                    },
+                    (protocol_handle, command_stream),
+                )
+            })
+            .unzip();
+        assert!(!notif_protocols.is_empty());
+
+        let (mut protocol_handles, command_streams): (Vec<_>, Vec<_>) = protocol_handles
+            .into_iter()
+            .enumerate()
+            .map(|(set_id, (mut protocol_handle, command_stream))| {
+                protocol_handle.set_metrics(metrics.clone());
+
+                (protocol_handle, (set_id, command_stream))
+            })
+            .unzip();
+
+        protocol_handles.iter_mut().skip(1).for_each(|handle| {
+            handle.delegate_to_peerset(true);
+        });
+
+        Self {
+            notif_protocols,
+            protocol_handles,
+            command_streams: StreamMap::from_iter(command_streams),
+            protocol_controller_handles,
+            from_protocol_controllers,
+            peers: FnvHashMap::default(),
+            delays: Default::default(),
+            next_delay_id: DelayId(0),
+            incoming: SmallVec::new(),
+            next_incoming_index: IncomingIndex(0),
+            events: VecDeque::new(),
+            pending_inbound_validations: FuturesUnordered::new(),
+            metrics,
+        }
+    }
+
+    /// Modifies the handshake of the given notifications protocol.
+    pub fn set_notif_protocol_handshake(
+        &mut self,
+        set_id: SetId,
+        handshake_message: impl Into<Vec<u8>>,
+    ) {
+        if let Some(p) = self.notif_protocols.get_mut(usize::from(set_id)) {
+            *p.handshake.write() = handshake_message.into();
+        } else {
+            log::error!(target: "sub-libp2p", "Unknown handshake change set: {:?}", set_id);
+            debug_assert!(false);
+        }
+    }
+
+    /// Returns the list of all the peers we have an open channel to.
+    pub fn open_peers(&self) -> impl Iterator<Item = &PeerId> {
+        self.peers
+            .iter()
+            .filter(|(_, state)| state.is_open())
+            .map(|((id, _), _)| id)
+    }
+
+    /// Returns true if we have an open substream to the given peer.
+    pub fn is_open(&self, peer_id: &PeerId, set_id: SetId) -> bool {
+        self.peers
+            .get(&(*peer_id, set_id))
+            .map(|p| p.is_open())
+            .unwrap_or(false)
+    }
+
+    /// Disconnects the given peer if we are connected to it.
+    pub fn disconnect_peer(&mut self, peer_id: &PeerId, set_id: SetId) {
+        trace!(target: "sub-libp2p", "External API => Disconnect({}, {:?})", peer_id, set_id);
+        self.disconnect_peer_inner(peer_id, set_id);
+    }
+
+    /// Inner implementation of `disconnect_peer`.
+    fn disconnect_peer_inner(&mut self, peer_id: &PeerId, set_id: SetId) {
+        let mut entry = if let Entry::Occupied(entry) = self.peers.entry((*peer_id, set_id)) {
+            entry
+        } else {
+            return;
+        };
+
+        match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+            // We're not connected anyway.
+            st @ PeerState::Disabled { .. } => *entry.into_mut() = st,
+            st @ PeerState::Requested => *entry.into_mut() = st,
+            st @ PeerState::PendingRequest { .. } => *entry.into_mut() = st,
+            st @ PeerState::Backoff { .. } => *entry.into_mut() = st,
+
+            // DisabledPendingEnable => Disabled.
+            PeerState::DisabledPendingEnable {
+                connections,
+                timer_deadline,
+                timer: _,
+            } => {
+                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                self.protocol_controller_handles[usize::from(set_id)].dropped(*peer_id);
+                *entry.into_mut() = PeerState::Disabled {
+                    connections,
+                    backoff_until: Some(timer_deadline),
+                }
+            }
+
+            // Enabled => Disabled.
+            // All open or opening connections are sent a `Close` message.
+            // If relevant, the external API is instantly notified.
+            PeerState::Enabled { mut connections } => {
+                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                self.protocol_controller_handles[usize::from(set_id)].dropped(*peer_id);
+
+                if connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::Open(_)))
+                {
+                    trace!(target: "sub-libp2p", "External API <= Closed({}, {:?})", peer_id, set_id);
+                    let event = NotificationsOut::CustomProtocolClosed {
+                        peer_id: *peer_id,
+                        set_id,
+                    };
+                    self.events.push_back(ToSwarm::GenerateEvent(event));
+                }
+
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::Open(_)))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})", peer_id, *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: *peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Closing;
+                }
+
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::Opening))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})", peer_id, *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: *peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::OpeningThenClosing;
+                }
+
+                debug_assert!(!connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::Open(_))));
+                debug_assert!(!connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::Opening)));
+
+                *entry.into_mut() = PeerState::Disabled {
+                    connections,
+                    backoff_until: None,
+                }
+            }
+
+            // Incoming => Disabled.
+            // Ongoing opening requests from the remote are rejected.
+            PeerState::Incoming {
+                mut connections,
+                backoff_until,
+                ..
+            } => {
+                let inc = if let Some(inc) = self
+                    .incoming
+                    .iter_mut()
+                    .find(|i| i.peer_id == entry.key().0 && i.set_id == set_id && i.alive)
+                {
+                    inc
+                } else {
+                    error!(
+                        target: "sub-libp2p",
+                        "State mismatch in libp2p: no entry in incoming for incoming peer"
+                    );
+                    return;
+                };
+
+                inc.alive = false;
+
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})", peer_id, *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: *peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Closing;
+                }
+
+                debug_assert!(!connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote)));
+                *entry.into_mut() = PeerState::Disabled {
+                    connections,
+                    backoff_until,
+                }
+            }
+
+            PeerState::Poisoned => {
+                error!(target: "sub-libp2p", "State of {:?} is poisoned", peer_id)
+            }
+        }
+    }
+
+    /// Function that is called when the peerset wants us to connect to a peer.
+    fn peerset_report_connect(&mut self, peer_id: PeerId, set_id: SetId) {
+        // If `PeerId` is unknown to us, insert an entry, start dialing, and return early.
+        let mut occ_entry = match self.peers.entry((peer_id, set_id)) {
+            Entry::Occupied(entry) => entry,
+            Entry::Vacant(entry) => {
+                // If there's no entry in `self.peers`, start dialing.
+                trace!(
+                    target: "sub-libp2p",
+                    "PSM => Connect({}, {:?}): Starting to connect",
+                    entry.key().0,
+                    set_id,
+                );
+                trace!(target: "sub-libp2p", "Libp2p <= Dial {}", entry.key().0);
+                self.events.push_back(ToSwarm::Dial {
+                    opts: entry.key().0.into(),
+                });
+                entry.insert(PeerState::Requested);
+                return;
+            }
+        };
+
+        let now = Instant::now();
+
+        match mem::replace(occ_entry.get_mut(), PeerState::Poisoned) {
+            // Backoff (not expired) => PendingRequest
+            PeerState::Backoff {
+                ref timer,
+                ref timer_deadline,
+            } if *timer_deadline > now => {
+                let peer_id = occ_entry.key().0;
+                trace!(
+                    target: "sub-libp2p",
+                    "PSM => Connect({}, {:?}): Will start to connect at until {:?}",
+                    peer_id,
+                    set_id,
+                    timer_deadline,
+                );
+                *occ_entry.into_mut() = PeerState::PendingRequest {
+                    timer: *timer,
+                    timer_deadline: *timer_deadline,
+                };
+            }
+
+            // Backoff (expired) => Requested
+            PeerState::Backoff { .. } => {
+                trace!(
+                    target: "sub-libp2p",
+                    "PSM => Connect({}, {:?}): Starting to connect",
+                    occ_entry.key().0,
+                    set_id,
+                );
+                trace!(target: "sub-libp2p", "Libp2p <= Dial {:?}", occ_entry.key());
+                self.events.push_back(ToSwarm::Dial {
+                    opts: occ_entry.key().0.into(),
+                });
+                *occ_entry.into_mut() = PeerState::Requested;
+            }
+
+            // Disabled (with non-expired ban) => DisabledPendingEnable
+            PeerState::Disabled {
+                connections,
+                backoff_until: Some(ref backoff),
+            } if *backoff > now => {
+                let peer_id = occ_entry.key().0;
+                trace!(
+                    target: "sub-libp2p",
+                    "PSM => Connect({}, {:?}): But peer is backed-off until {:?}",
+                    peer_id,
+                    set_id,
+                    backoff,
+                );
+
+                let delay_id = self.next_delay_id;
+                self.next_delay_id.0 += 1;
+                let delay = futures_timer::Delay::new(*backoff - now);
+                self.delays.push(
+                    async move {
+                        delay.await;
+                        (delay_id, peer_id, set_id)
+                    }
+                    .boxed(),
+                );
+
+                *occ_entry.into_mut() = PeerState::DisabledPendingEnable {
+                    connections,
+                    timer: delay_id,
+                    timer_deadline: *backoff,
+                };
+            }
+
+            // Disabled => Enabled
+            PeerState::Disabled {
+                mut connections,
+                backoff_until,
+            } => {
+                debug_assert!(!connections
+                    .iter()
+                    .any(|(_, s)| { matches!(s, ConnectionState::Open(_)) }));
+
+                // The first element of `closed` is chosen to open the notifications substream.
+                if let Some((connec_id, connec_state)) = connections
+                    .iter_mut()
+                    .find(|(_, s)| matches!(s, ConnectionState::Closed))
+                {
+                    trace!(target: "sub-libp2p", "PSM => Connect({}, {:?}): Enabling connections.",
+						occ_entry.key().0, set_id);
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Open({:?})", peer_id, *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Open {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Opening;
+                    *occ_entry.into_mut() = PeerState::Enabled { connections };
+                } else {
+                    // If no connection is available, switch to `DisabledPendingEnable` in order
+                    // to try again later.
+                    debug_assert!(connections.iter().any(|(_, s)| {
+                        matches!(
+                            s,
+                            ConnectionState::OpeningThenClosing | ConnectionState::Closing
+                        )
+                    }));
+                    trace!(
+                        target: "sub-libp2p",
+                        "PSM => Connect({}, {:?}): No connection in proper state. Delaying.",
+                        occ_entry.key().0, set_id
+                    );
+
+                    let timer_deadline = {
+                        let base = now + Duration::from_secs(5);
+                        if let Some(backoff_until) = backoff_until {
+                            cmp::max(base, backoff_until)
+                        } else {
+                            base
+                        }
+                    };
+
+                    let delay_id = self.next_delay_id;
+                    self.next_delay_id.0 += 1;
+                    debug_assert!(timer_deadline > now);
+                    let delay = futures_timer::Delay::new(timer_deadline - now);
+                    self.delays.push(
+                        async move {
+                            delay.await;
+                            (delay_id, peer_id, set_id)
+                        }
+                        .boxed(),
+                    );
+
+                    *occ_entry.into_mut() = PeerState::DisabledPendingEnable {
+                        connections,
+                        timer: delay_id,
+                        timer_deadline,
+                    };
+                }
+            }
+            // Incoming => Incoming
+            st @ PeerState::Incoming { .. } => {
+                debug!(
+                    target: "sub-libp2p",
+                    "PSM => Connect({}, {:?}): Ignoring obsolete connect, we are awaiting accept/reject.",
+                    occ_entry.key().0, set_id
+                );
+                *occ_entry.into_mut() = st;
+            }
+
+            // Other states are kept as-is.
+            st @ PeerState::Enabled { .. } => {
+                debug!(target: "sub-libp2p",
+					"PSM => Connect({}, {:?}): Already connected.",
+					occ_entry.key().0, set_id);
+                *occ_entry.into_mut() = st;
+            }
+            st @ PeerState::DisabledPendingEnable { .. } => {
+                debug!(target: "sub-libp2p",
+					"PSM => Connect({}, {:?}): Already pending enabling.",
+					occ_entry.key().0, set_id);
+                *occ_entry.into_mut() = st;
+            }
+            st @ PeerState::Requested { .. } | st @ PeerState::PendingRequest { .. } => {
+                debug!(target: "sub-libp2p",
+					"PSM => Connect({}, {:?}): Duplicate request.",
+					occ_entry.key().0, set_id);
+                *occ_entry.into_mut() = st;
+            }
+
+            PeerState::Poisoned => {
+                error!(target: "sub-libp2p", "State of {:?} is poisoned", occ_entry.key());
+                debug_assert!(false);
+            }
+        }
+    }
+
+    /// Function that is called when the peerset wants us to disconnect from a peer.
+    fn peerset_report_disconnect(&mut self, peer_id: PeerId, set_id: SetId) {
+        let mut entry = match self.peers.entry((peer_id, set_id)) {
+            Entry::Occupied(entry) => entry,
+            Entry::Vacant(entry) => {
+                trace!(target: "sub-libp2p", "PSM => Drop({}, {:?}): Already disabled.",
+					entry.key().0, set_id);
+                return;
+            }
+        };
+
+        match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+            st @ PeerState::Disabled { .. } | st @ PeerState::Backoff { .. } => {
+                trace!(target: "sub-libp2p", "PSM => Drop({}, {:?}): Already disabled.",
+					entry.key().0, set_id);
+                *entry.into_mut() = st;
+            }
+
+            // DisabledPendingEnable => Disabled
+            PeerState::DisabledPendingEnable {
+                connections,
+                timer_deadline,
+                timer: _,
+            } => {
+                debug_assert!(!connections.is_empty());
+                trace!(target: "sub-libp2p",
+					"PSM => Drop({}, {:?}): Interrupting pending enabling.",
+					entry.key().0, set_id);
+                *entry.into_mut() = PeerState::Disabled {
+                    connections,
+                    backoff_until: Some(timer_deadline),
+                };
+            }
+
+            // Enabled => Disabled
+            PeerState::Enabled { mut connections } => {
+                trace!(target: "sub-libp2p", "PSM => Drop({}, {:?}): Disabling connections.",
+					entry.key().0, set_id);
+
+                debug_assert!(connections.iter().any(|(_, s)| matches!(
+                    s,
+                    ConnectionState::Opening | ConnectionState::Open(_)
+                )));
+
+                if connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::Open(_)))
+                {
+                    trace!(target: "sub-libp2p", "External API <= Closed({}, {:?})", entry.key().0, set_id);
+                    let event = NotificationsOut::CustomProtocolClosed {
+                        peer_id: entry.key().0,
+                        set_id,
+                    };
+                    self.events.push_back(ToSwarm::GenerateEvent(event));
+                }
+
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::Opening))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})",
+						entry.key(), *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: entry.key().0,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::OpeningThenClosing;
+                }
+
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::Open(_)))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})",
+						entry.key(), *connec_id, set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: entry.key().0,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Closing;
+                }
+
+                *entry.into_mut() = PeerState::Disabled {
+                    connections,
+                    backoff_until: None,
+                }
+            }
+
+            // Requested => Ø
+            PeerState::Requested => {
+                // We don't cancel dialing. Libp2p doesn't expose that on purpose, as other
+                // sub-systems (such as the discovery mechanism) may require dialing this peer as
+                // well at the same time.
+                trace!(target: "sub-libp2p", "PSM => Drop({}, {:?}): Not yet connected.",
+					entry.key().0, set_id);
+                entry.remove();
+            }
+
+            // PendingRequest => Backoff
+            PeerState::PendingRequest {
+                timer,
+                timer_deadline,
+            } => {
+                trace!(target: "sub-libp2p", "PSM => Drop({}, {:?}): Not yet connected",
+					entry.key().0, set_id);
+                *entry.into_mut() = PeerState::Backoff {
+                    timer,
+                    timer_deadline,
+                }
+            }
+
+            // `ProtocolController` disconnected peer while it was still being validated by the
+            // protocol, mark the connection as rejected and once the validation is received from
+            // the protocol, reject the substream
+            PeerState::Incoming {
+                backoff_until,
+                connections,
+                incoming_index,
+                ..
+            } => {
+                debug!(
+                    target: "sub-libp2p",
+                    "PSM => Drop({}, {:?}): Ignoring obsolete disconnect, we are awaiting accept/reject.",
+                    entry.key().0, set_id,
+                );
+                *entry.into_mut() = PeerState::Incoming {
+                    backoff_until,
+                    connections,
+                    incoming_index,
+                    peerset_rejected: true,
+                };
+            }
+            PeerState::Poisoned => {
+                error!(target: "sub-libp2p", "State of {:?} is poisoned", entry.key());
+                debug_assert!(false);
+            }
+        }
+    }
+
+    /// Substream has been accepted by the `ProtocolController` and must now be sent
+    /// to the protocol for validation.
+    fn peerset_report_preaccept(&mut self, index: IncomingIndex) {
+        let Some(pos) = self.incoming.iter().position(|i| i.incoming_id == index) else {
+            error!(target: LOG_TARGET, "PSM => Preaccept({:?}): Invalid index", index);
+            return;
+        };
+
+        trace!(
+            target: LOG_TARGET,
+            "PSM => Preaccept({:?}): Sent to protocol for validation",
+            index
+        );
+        let incoming = &self.incoming[pos];
+
+        match self.protocol_handles[usize::from(incoming.set_id)]
+            .report_incoming_substream(incoming.peer_id, incoming.handshake.clone())
+        {
+            Ok(ValidationCallResult::Delegated) => {
+                self.protocol_report_accept(index);
+            }
+            Ok(ValidationCallResult::WaitForValidation(rx)) => {
+                self.pending_inbound_validations
+                    .push(Box::pin(async move { (rx.await, index) }));
+            }
+            Err(err) => {
+                // parachain collators enable the syncing protocol but `NotificationService` for
+                // `SyncingEngine` is not created which causes `report_incoming_substream()` to
+                // fail. This is not a fatal error and should be ignored even though in typical
+                // cases the `NotificationService` not existing is a fatal error and indicates that
+                // the protocol has exited. Until the parachain collator issue is fixed, just report
+                // and error and reject the peer.
+                debug!(target: LOG_TARGET, "protocol has exited: {err:?} {:?}", incoming.set_id);
+
+                self.protocol_report_reject(index);
+            }
+        }
+    }
+
+    /// Function that is called when the peerset wants us to accept a connection
+    /// request from a peer.
+    #[allow(clippy::comparison_chain)]
+    fn protocol_report_accept(&mut self, index: IncomingIndex) {
+        let (pos, incoming) =
+            if let Some(pos) = self.incoming.iter().position(|i| i.incoming_id == index) {
+                (pos, self.incoming.get(pos))
+            } else {
+                error!(target: "sub-libp2p", "PSM => Accept({:?}): Invalid index", index);
+                return;
+            };
+
+        let Some(incoming) = incoming else {
+            error!(target: "sub-libp2p", "Incoming connection ({:?}) doesn't exist", index);
+            debug_assert!(false);
+            return;
+        };
+
+        if !incoming.alive {
+            trace!(
+                target: "sub-libp2p",
+                "PSM => Accept({:?}, {}, {:?}): Obsolete incoming",
+                index,
+                incoming.peer_id,
+                incoming.set_id,
+            );
+
+            match self.peers.get_mut(&(incoming.peer_id, incoming.set_id)) {
+                Some(PeerState::DisabledPendingEnable { .. }) | Some(PeerState::Enabled { .. }) => {
+                }
+                _ => {
+                    trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})",
+						incoming.peer_id, incoming.set_id);
+                    self.protocol_controller_handles[usize::from(incoming.set_id)]
+                        .dropped(incoming.peer_id);
+                }
+            }
+
+            self.incoming.remove(pos);
+            return;
+        }
+
+        let state = match self.peers.get_mut(&(incoming.peer_id, incoming.set_id)) {
+            Some(s) => s,
+            None => {
+                log::debug!(
+                    target: "sub-libp2p",
+                    "Connection to {:?} closed, ({:?} {:?}), ignoring accept",
+                    incoming.peer_id,
+                    incoming.set_id,
+                    index,
+                );
+                self.incoming.remove(pos);
+                return;
+            }
+        };
+
+        match mem::replace(state, PeerState::Poisoned) {
+            // Incoming => Enabled
+            PeerState::Incoming {
+                mut connections,
+                incoming_index,
+                peerset_rejected,
+                backoff_until,
+            } => {
+                if index < incoming_index {
+                    warn!(
+                        target: "sub-libp2p",
+                        "PSM => Accept({:?}, {}, {:?}): Ignoring obsolete incoming index, we are already awaiting {:?}.",
+                        index, incoming.peer_id, incoming.set_id, incoming_index
+                    );
+
+                    self.incoming.remove(pos);
+                    return;
+                } else if index > incoming_index {
+                    error!(
+                        target: "sub-libp2p",
+                        "PSM => Accept({:?}, {}, {:?}): Ignoring incoming index from the future, we are awaiting {:?}.",
+                        index, incoming.peer_id, incoming.set_id, incoming_index
+                    );
+
+                    self.incoming.remove(pos);
+                    debug_assert!(false);
+                    return;
+                }
+
+                // while the substream was being validated by the protocol, `Peerset` had request
+                // for the it to be closed so reject the substream now
+                if peerset_rejected {
+                    trace!(
+                        target: "sub-libp2p",
+                        "Protocol accepted ({:?} {:?} {:?}) but Peerset had request disconnection, rejecting",
+                        index,
+                        incoming.peer_id,
+                        incoming.set_id
+                    );
+
+                    *state = PeerState::Incoming {
+                        connections,
+                        backoff_until,
+                        peerset_rejected,
+                        incoming_index,
+                    };
+                    return self.report_reject(index).map_or((), |_| ());
+                }
+
+                trace!(
+                    target: "sub-libp2p",
+                    "PSM => Accept({:?}, {}, {:?}): Enabling connections.",
+                    index,
+                    incoming.peer_id,
+                    incoming.set_id
+                );
+
+                debug_assert!(connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote)));
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Open({:?})",
+						incoming.peer_id, *connec_id, incoming.set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: incoming.peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Open {
+                            protocol_index: incoming.set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Opening;
+                }
+
+                self.incoming.remove(pos);
+                *state = PeerState::Enabled { connections };
+            }
+            st @ PeerState::Disabled { .. } | st @ PeerState::Backoff { .. } => {
+                self.incoming.remove(pos);
+                *state = st;
+            }
+            // Any state other than `Incoming` is invalid.
+            peer => {
+                error!(
+                    target: "sub-libp2p",
+                    "State mismatch in libp2p: Expected alive incoming. Got {:?}.",
+                    peer
+                );
+
+                self.incoming.remove(pos);
+                debug_assert!(false);
+            }
+        }
+    }
+
+    /// Function that is called when `ProtocolController` wants us to reject an incoming peer.
+    fn peerset_report_reject(&mut self, index: IncomingIndex) {
+        let _ = self.report_reject(index);
+    }
+
+    /// Function that is called when the protocol wants us to reject an incoming peer.
+    fn protocol_report_reject(&mut self, index: IncomingIndex) {
+        if let Some((set_id, peer_id)) = self.report_reject(index) {
+            self.protocol_controller_handles[usize::from(set_id)].dropped(peer_id)
+        }
+    }
+
+    /// Function that is called when the peerset wants us to reject an incoming peer.
+    #[allow(clippy::comparison_chain)]
+    fn report_reject(&mut self, index: IncomingIndex) -> Option<(SetId, PeerId)> {
+        let incoming = if let Some(pos) = self.incoming.iter().position(|i| i.incoming_id == index)
+        {
+            self.incoming.remove(pos)
+        } else {
+            error!(target: "sub-libp2p", "PSM => Reject({:?}): Invalid index", index);
+            return None;
+        };
+
+        if !incoming.alive {
+            trace!(
+                target: "sub-libp2p",
+                "PSM => Reject({:?}, {}, {:?}): Obsolete incoming, ignoring",
+                index,
+                incoming.peer_id,
+                incoming.set_id,
+            );
+
+            return None;
+        }
+
+        let state = match self.peers.get_mut(&(incoming.peer_id, incoming.set_id)) {
+            Some(s) => s,
+            None => {
+                log::debug!(
+                    target: "sub-libp2p",
+                    "Connection to {:?} closed, ({:?} {:?}), ignoring accept",
+                    incoming.peer_id,
+                    incoming.set_id,
+                    index,
+                );
+                return None;
+            }
+        };
+
+        match mem::replace(state, PeerState::Poisoned) {
+            // Incoming => Disabled
+            PeerState::Incoming {
+                mut connections,
+                backoff_until,
+                incoming_index,
+                ..
+            } => {
+                if index < incoming_index {
+                    warn!(
+                        target: "sub-libp2p",
+                        "PSM => Reject({:?}, {}, {:?}): Ignoring obsolete incoming index, we are already awaiting {:?}.",
+                        index, incoming.peer_id, incoming.set_id, incoming_index
+                    );
+                    return None;
+                } else if index > incoming_index {
+                    error!(
+                        target: "sub-libp2p",
+                        "PSM => Reject({:?}, {}, {:?}): Ignoring incoming index from the future, we are awaiting {:?}.",
+                        index, incoming.peer_id, incoming.set_id, incoming_index
+                    );
+                    debug_assert!(false);
+                    return None;
+                }
+
+                trace!(target: "sub-libp2p", "PSM => Reject({:?}, {}, {:?}): Rejecting connections.",
+					index, incoming.peer_id, incoming.set_id);
+
+                debug_assert!(connections
+                    .iter()
+                    .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote)));
+                for (connec_id, connec_state) in connections
+                    .iter_mut()
+                    .filter(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote))
+                {
+                    trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Close({:?})",
+						incoming.peer_id, connec_id, incoming.set_id);
+                    self.events.push_back(ToSwarm::NotifyHandler {
+                        peer_id: incoming.peer_id,
+                        handler: NotifyHandler::One(*connec_id),
+                        event: NotifsHandlerIn::Close {
+                            protocol_index: incoming.set_id.into(),
+                        },
+                    });
+                    *connec_state = ConnectionState::Closing;
+                }
+
+                *state = PeerState::Disabled {
+                    connections,
+                    backoff_until,
+                };
+                Some((incoming.set_id, incoming.peer_id))
+            }
+            // connection to peer may have been closed already
+            st @ PeerState::Disabled { .. } | st @ PeerState::Backoff { .. } => {
+                *state = st;
+                None
+            }
+            peer => {
+                error!(
+                    target: LOG_TARGET,
+                    "State mismatch in libp2p: Expected alive incoming. Got {peer:?}.",
+                );
+                None
+            }
+        }
+    }
+}
+
+impl NetworkBehaviour for Notifications {
+    type ConnectionHandler = NotifsHandler;
+    type OutEvent = NotificationsOut;
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _local_addr: &Multiaddr,
+        _remote_addr: &Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        Ok(())
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _maybe_peer: Option<PeerId>,
+        _addresses: &[Multiaddr],
+        _effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        Ok(Vec::new())
+    }
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        Ok(NotifsHandler::new(
+            peer,
+            ConnectedPoint::Listener {
+                local_addr: local_addr.clone(),
+                send_back_addr: remote_addr.clone(),
+            },
+            self.notif_protocols.clone(),
+            self.metrics.clone(),
+        ))
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        Ok(NotifsHandler::new(
+            peer,
+            ConnectedPoint::Dialer {
+                address: addr.clone(),
+                role_override,
+            },
+            self.notif_protocols.clone(),
+            self.metrics.clone(),
+        ))
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        match event {
+            FromSwarm::ConnectionEstablished(ConnectionEstablished {
+                peer_id,
+                endpoint,
+                connection_id,
+                ..
+            }) => {
+                for set_id in (0..self.notif_protocols.len()).map(SetId::from) {
+                    match self
+                        .peers
+                        .entry((peer_id, set_id))
+                        .or_insert(PeerState::Poisoned)
+                    {
+                        // Requested | PendingRequest => Enabled
+                        st @ &mut PeerState::Requested
+                        | st @ &mut PeerState::PendingRequest { .. } => {
+                            trace!(target: "sub-libp2p",
+                                "Libp2p => Connected({}, {:?}, {:?}): Connection was requested by PSM.",
+                                peer_id, set_id, endpoint
+                            );
+                            trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Open({:?})", peer_id, connection_id, set_id);
+                            self.events.push_back(ToSwarm::NotifyHandler {
+                                peer_id,
+                                handler: NotifyHandler::One(connection_id),
+                                event: NotifsHandlerIn::Open {
+                                    protocol_index: set_id.into(),
+                                },
+                            });
+
+                            let mut connections = SmallVec::new();
+                            connections.push((connection_id, ConnectionState::Opening));
+                            *st = PeerState::Enabled { connections };
+                        }
+
+                        // Poisoned gets inserted above if the entry was missing.
+                        // Ø | Backoff => Disabled
+                        st @ &mut PeerState::Poisoned | st @ &mut PeerState::Backoff { .. } => {
+                            let backoff_until =
+                                if let PeerState::Backoff { timer_deadline, .. } = st {
+                                    Some(*timer_deadline)
+                                } else {
+                                    None
+                                };
+                            trace!(target: "sub-libp2p",
+								"Libp2p => Connected({}, {:?}, {:?}, {:?}): Not requested by PSM, disabling.",
+								peer_id, set_id, endpoint, connection_id);
+
+                            let mut connections = SmallVec::new();
+                            connections.push((connection_id, ConnectionState::Closed));
+                            *st = PeerState::Disabled {
+                                connections,
+                                backoff_until,
+                            };
+                        }
+
+                        // In all other states, add this new connection to the list of closed
+                        // inactive connections.
+                        PeerState::Incoming { connections, .. }
+                        | PeerState::Disabled { connections, .. }
+                        | PeerState::DisabledPendingEnable { connections, .. }
+                        | PeerState::Enabled { connections, .. } => {
+                            trace!(target: "sub-libp2p",
+								"Libp2p => Connected({}, {:?}, {:?}, {:?}): Secondary connection. Leaving closed.",
+								peer_id, set_id, endpoint, connection_id);
+                            connections.push((connection_id, ConnectionState::Closed));
+                        }
+                    }
+                }
+            }
+            FromSwarm::ConnectionClosed(ConnectionClosed {
+                peer_id,
+                connection_id,
+                ..
+            }) => {
+                for set_id in (0..self.notif_protocols.len()).map(SetId::from) {
+                    let mut entry = if let Entry::Occupied(entry) =
+                        self.peers.entry((peer_id, set_id))
+                    {
+                        entry
+                    } else {
+                        error!(target: "sub-libp2p", "inject_connection_closed: State mismatch in the custom protos handler");
+                        debug_assert!(false);
+                        return;
+                    };
+
+                    match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+                        // Disabled => Disabled | Backoff | Ø
+                        PeerState::Disabled {
+                            mut connections,
+                            backoff_until,
+                        } => {
+                            trace!(target: "sub-libp2p", "Libp2p => Disconnected({}, {:?}, {:?}): Disabled.",
+								peer_id, set_id, connection_id);
+
+                            if let Some(pos) =
+                                connections.iter().position(|(c, _)| *c == connection_id)
+                            {
+                                connections.remove(pos);
+                            } else {
+                                debug_assert!(false);
+                                error!(target: "sub-libp2p",
+									"inject_connection_closed: State mismatch in the custom protos handler");
+                            }
+
+                            if connections.is_empty() {
+                                if let Some(until) = backoff_until {
+                                    let now = Instant::now();
+                                    if until > now {
+                                        let delay_id = self.next_delay_id;
+                                        self.next_delay_id.0 += 1;
+                                        let delay = futures_timer::Delay::new(until - now);
+                                        self.delays.push(
+                                            async move {
+                                                delay.await;
+                                                (delay_id, peer_id, set_id)
+                                            }
+                                            .boxed(),
+                                        );
+
+                                        *entry.get_mut() = PeerState::Backoff {
+                                            timer: delay_id,
+                                            timer_deadline: until,
+                                        };
+                                    } else {
+                                        entry.remove();
+                                    }
+                                } else {
+                                    entry.remove();
+                                }
+                            } else {
+                                *entry.get_mut() = PeerState::Disabled {
+                                    connections,
+                                    backoff_until,
+                                };
+                            }
+                        }
+
+                        // DisabledPendingEnable => DisabledPendingEnable | Backoff
+                        PeerState::DisabledPendingEnable {
+                            mut connections,
+                            timer_deadline,
+                            timer,
+                        } => {
+                            trace!(
+                                target: "sub-libp2p",
+                                "Libp2p => Disconnected({}, {:?}, {:?}): Disabled but pending enable.",
+                                peer_id, set_id, connection_id
+                            );
+
+                            if let Some(pos) =
+                                connections.iter().position(|(c, _)| *c == connection_id)
+                            {
+                                connections.remove(pos);
+                            } else {
+                                error!(target: "sub-libp2p",
+									"inject_connection_closed: State mismatch in the custom protos handler");
+                                debug_assert!(false);
+                            }
+
+                            if connections.is_empty() {
+                                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                                self.protocol_controller_handles[usize::from(set_id)]
+                                    .dropped(peer_id);
+                                *entry.get_mut() = PeerState::Backoff {
+                                    timer,
+                                    timer_deadline,
+                                };
+                            } else {
+                                *entry.get_mut() = PeerState::DisabledPendingEnable {
+                                    connections,
+                                    timer_deadline,
+                                    timer,
+                                };
+                            }
+                        }
+
+                        // Incoming => Incoming | Disabled | Backoff | Ø
+                        PeerState::Incoming {
+                            mut connections,
+                            backoff_until,
+                            incoming_index,
+                            peerset_rejected,
+                        } => {
+                            trace!(
+                                target: "sub-libp2p",
+                                "Libp2p => Disconnected({}, {:?}, {:?}): OpenDesiredByRemote.",
+                                peer_id, set_id, connection_id
+                            );
+
+                            debug_assert!(connections
+                                .iter()
+                                .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote)));
+
+                            if let Some(pos) =
+                                connections.iter().position(|(c, _)| *c == connection_id)
+                            {
+                                connections.remove(pos);
+                            } else {
+                                error!(target: "sub-libp2p",
+									"inject_connection_closed: State mismatch in the custom protos handler");
+                                debug_assert!(false);
+                            }
+
+                            let no_desired_left = !connections
+                                .iter()
+                                .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote));
+
+                            // If no connection is `OpenDesiredByRemote` anymore, clean up the
+                            // peerset incoming request.
+                            if no_desired_left {
+                                // In the incoming state, we don't report "Dropped" straight away.
+                                // Instead we will report "Dropped" if receive the corresponding
+                                // "Accept".
+                                if let Some(state) = self
+                                    .incoming
+                                    .iter_mut()
+                                    .find(|i| i.alive && i.set_id == set_id && i.peer_id == peer_id)
+                                {
+                                    state.alive = false;
+                                } else {
+                                    error!(target: "sub-libp2p", "State mismatch in libp2p: no entry in \
+										incoming corresponding to an incoming state in peers");
+                                    debug_assert!(false);
+                                }
+                            }
+
+                            if connections.is_empty() {
+                                if let Some(until) = backoff_until {
+                                    let now = Instant::now();
+                                    if until > now {
+                                        let delay_id = self.next_delay_id;
+                                        self.next_delay_id.0 += 1;
+                                        let delay = futures_timer::Delay::new(until - now);
+                                        self.delays.push(
+                                            async move {
+                                                delay.await;
+                                                (delay_id, peer_id, set_id)
+                                            }
+                                            .boxed(),
+                                        );
+
+                                        *entry.get_mut() = PeerState::Backoff {
+                                            timer: delay_id,
+                                            timer_deadline: until,
+                                        };
+                                    } else {
+                                        entry.remove();
+                                    }
+                                } else {
+                                    entry.remove();
+                                }
+                            } else if no_desired_left {
+                                // If no connection is `OpenDesiredByRemote` anymore, switch to
+                                // `Disabled`.
+                                *entry.get_mut() = PeerState::Disabled {
+                                    connections,
+                                    backoff_until,
+                                };
+                            } else {
+                                *entry.get_mut() = PeerState::Incoming {
+                                    connections,
+                                    backoff_until,
+                                    incoming_index,
+                                    peerset_rejected,
+                                };
+                            }
+                        }
+
+                        // Enabled => Enabled | Backoff
+                        // Peers are always backed-off when disconnecting while Enabled.
+                        PeerState::Enabled { mut connections } => {
+                            trace!(
+                                target: "sub-libp2p",
+                                "Libp2p => Disconnected({}, {:?}, {:?}): Enabled.",
+                                peer_id, set_id, connection_id
+                            );
+
+                            debug_assert!(connections.iter().any(|(_, s)| matches!(
+                                s,
+                                ConnectionState::Opening | ConnectionState::Open(_)
+                            )));
+
+                            if let Some(pos) =
+                                connections.iter().position(|(c, _)| *c == connection_id)
+                            {
+                                let (_, state) = connections.remove(pos);
+                                if let ConnectionState::Open(_) = state {
+                                    if let Some((replacement_pos, replacement_sink)) = connections
+                                        .iter()
+                                        .enumerate()
+                                        .find_map(|(num, (_, s))| match s {
+                                            ConnectionState::Open(s) => Some((num, s.clone())),
+                                            _ => None,
+                                        })
+                                    {
+                                        if pos <= replacement_pos {
+                                            trace!(
+                                                target: "sub-libp2p",
+                                                "External API <= Sink replaced({}, {:?})",
+                                                peer_id, set_id
+                                            );
+                                            let event = NotificationsOut::CustomProtocolReplaced {
+                                                peer_id,
+                                                set_id,
+                                                notifications_sink: replacement_sink.clone(),
+                                            };
+                                            self.events.push_back(ToSwarm::GenerateEvent(event));
+                                        }
+                                    } else {
+                                        trace!(
+                                            target: "sub-libp2p", "External API <= Closed({}, {:?})",
+                                            peer_id, set_id
+                                        );
+                                        let event = NotificationsOut::CustomProtocolClosed {
+                                            peer_id,
+                                            set_id,
+                                        };
+                                        self.events.push_back(ToSwarm::GenerateEvent(event));
+                                    }
+                                }
+                            } else {
+                                error!(target: "sub-libp2p",
+									"inject_connection_closed: State mismatch in the custom protos handler");
+                                debug_assert!(false);
+                            }
+
+                            if connections.is_empty() {
+                                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                                self.protocol_controller_handles[usize::from(set_id)]
+                                    .dropped(peer_id);
+                                let ban_dur = Uniform::new(5, 10).sample(&mut rand::thread_rng());
+
+                                let delay_id = self.next_delay_id;
+                                self.next_delay_id.0 += 1;
+                                let delay = futures_timer::Delay::new(Duration::from_secs(ban_dur));
+                                self.delays.push(
+                                    async move {
+                                        delay.await;
+                                        (delay_id, peer_id, set_id)
+                                    }
+                                    .boxed(),
+                                );
+
+                                *entry.get_mut() = PeerState::Backoff {
+                                    timer: delay_id,
+                                    timer_deadline: Instant::now() + Duration::from_secs(ban_dur),
+                                };
+                            } else if !connections.iter().any(|(_, s)| {
+                                matches!(s, ConnectionState::Opening | ConnectionState::Open(_))
+                            }) {
+                                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                                self.protocol_controller_handles[usize::from(set_id)]
+                                    .dropped(peer_id);
+
+                                *entry.get_mut() = PeerState::Disabled {
+                                    connections,
+                                    backoff_until: None,
+                                };
+                            } else {
+                                *entry.get_mut() = PeerState::Enabled { connections };
+                            }
+                        }
+
+                        PeerState::Requested
+                        | PeerState::PendingRequest { .. }
+                        | PeerState::Backoff { .. } => {
+                            // This is a serious bug either in this state machine or in libp2p.
+                            error!(target: "sub-libp2p",
+								"`inject_connection_closed` called for unknown peer {}",
+								peer_id);
+                            debug_assert!(false);
+                        }
+                        PeerState::Poisoned => {
+                            error!(target: "sub-libp2p", "State of peer {} is poisoned", peer_id);
+                            debug_assert!(false);
+                        }
+                    }
+                }
+            }
+            FromSwarm::DialFailure(DialFailure { peer_id, error, .. }) => {
+                if let DialError::Transport(errors) = error {
+                    for (addr, error) in errors.iter() {
+                        trace!(target: "sub-libp2p", "Libp2p => Reach failure for {:?} through {:?}: {:?}", peer_id, addr, error);
+                    }
+                }
+
+                if let Some(peer_id) = peer_id {
+                    trace!(target: "sub-libp2p", "Libp2p => Dial failure for {:?}", peer_id);
+
+                    for set_id in (0..self.notif_protocols.len()).map(SetId::from) {
+                        if let Entry::Occupied(mut entry) = self.peers.entry((peer_id, set_id)) {
+                            match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+                                // The peer is not in our list.
+                                st @ PeerState::Backoff { .. } => {
+                                    *entry.into_mut() = st;
+                                }
+
+                                // "Basic" situation: we failed to reach a peer that the peerset
+                                // requested.
+                                st @ PeerState::Requested
+                                | st @ PeerState::PendingRequest { .. } => {
+                                    trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                                    self.protocol_controller_handles[usize::from(set_id)]
+                                        .dropped(peer_id);
+
+                                    let now = Instant::now();
+                                    let ban_duration = match st {
+                                        PeerState::PendingRequest { timer_deadline, .. }
+                                            if timer_deadline > now =>
+                                        {
+                                            cmp::max(timer_deadline - now, Duration::from_secs(5))
+                                        }
+                                        _ => Duration::from_secs(5),
+                                    };
+
+                                    let delay_id = self.next_delay_id;
+                                    self.next_delay_id.0 += 1;
+                                    let delay = futures_timer::Delay::new(ban_duration);
+                                    self.delays.push(
+                                        async move {
+                                            delay.await;
+                                            (delay_id, peer_id, set_id)
+                                        }
+                                        .boxed(),
+                                    );
+
+                                    *entry.into_mut() = PeerState::Backoff {
+                                        timer: delay_id,
+                                        timer_deadline: now + ban_duration,
+                                    };
+                                }
+
+                                // We can still get dial failures even if we are already connected
+                                // to the peer, as an extra diagnostic for an earlier attempt.
+                                st @ PeerState::Disabled { .. }
+                                | st @ PeerState::Enabled { .. }
+                                | st @ PeerState::DisabledPendingEnable { .. }
+                                | st @ PeerState::Incoming { .. } => {
+                                    *entry.into_mut() = st;
+                                }
+
+                                PeerState::Poisoned => {
+                                    error!(target: "sub-libp2p", "State of {:?} is poisoned", peer_id);
+                                    debug_assert!(false);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            FromSwarm::ListenerClosed(_) => {}
+            FromSwarm::ListenFailure(_) => {}
+            FromSwarm::ListenerError(_) => {}
+            FromSwarm::ExpiredExternalAddr(_) => {}
+            FromSwarm::NewListener(_) => {}
+            FromSwarm::ExpiredListenAddr(_) => {}
+            FromSwarm::NewExternalAddr(_) => {}
+            FromSwarm::AddressChange(_) => {}
+            FromSwarm::NewListenAddr(_) => {}
+        }
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        match event {
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index,
+                handshake,
+            } => {
+                let set_id = SetId::from(protocol_index);
+
+                trace!(target: "sub-libp2p",
+					"Handler({:?}, {:?}]) => OpenDesiredByRemote({:?})",
+					peer_id, connection_id, set_id);
+
+                let mut entry = if let Entry::Occupied(entry) = self.peers.entry((peer_id, set_id))
+                {
+                    entry
+                } else {
+                    error!(
+                        target: "sub-libp2p",
+                        "OpenDesiredByRemote: State mismatch in the custom protos handler"
+                    );
+                    debug_assert!(false);
+                    return;
+                };
+
+                match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+                    // Incoming => Incoming
+                    PeerState::Incoming {
+                        mut connections,
+                        backoff_until,
+                        incoming_index,
+                        peerset_rejected,
+                    } => {
+                        debug_assert!(connections
+                            .iter()
+                            .any(|(_, s)| matches!(s, ConnectionState::OpenDesiredByRemote)));
+                        if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, _)| *c == connection_id)
+                        {
+                            if let ConnectionState::Closed = *connec_state {
+                                *connec_state = ConnectionState::OpenDesiredByRemote;
+                            } else {
+                                // Connections in `OpeningThenClosing` and `Closing` state can be
+                                // in a Closed phase, and as such can emit `OpenDesiredByRemote`
+                                // messages.
+                                // Since an `Open` and/or a `Close` message have already been sent,
+                                // there is nothing much that can be done about this anyway.
+                                debug_assert!(matches!(
+                                    connec_state,
+                                    ConnectionState::OpeningThenClosing | ConnectionState::Closing
+                                ));
+                            }
+                        } else {
+                            error!(
+                                target: "sub-libp2p",
+                                "OpenDesiredByRemote: State mismatch in the custom protos handler"
+                            );
+                            debug_assert!(false);
+                        }
+
+                        *entry.into_mut() = PeerState::Incoming {
+                            connections,
+                            backoff_until,
+                            incoming_index,
+                            peerset_rejected,
+                        };
+                    }
+
+                    PeerState::Enabled { mut connections } => {
+                        debug_assert!(connections.iter().any(|(_, s)| matches!(
+                            s,
+                            ConnectionState::Opening | ConnectionState::Open(_)
+                        )));
+
+                        if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, _)| *c == connection_id)
+                        {
+                            if let ConnectionState::Closed = *connec_state {
+                                trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Open({:?})",
+									peer_id, connection_id, set_id);
+                                self.events.push_back(ToSwarm::NotifyHandler {
+                                    peer_id,
+                                    handler: NotifyHandler::One(connection_id),
+                                    event: NotifsHandlerIn::Open {
+                                        protocol_index: set_id.into(),
+                                    },
+                                });
+                                *connec_state = ConnectionState::Opening;
+                            } else {
+                                // Connections in `OpeningThenClosing`, `Opening`, and `Closing`
+                                // state can be in a Closed phase, and as such can emit
+                                // `OpenDesiredByRemote` messages.
+                                // Since an `Open` message haS already been sent, there is nothing
+                                // more to do.
+                                debug_assert!(matches!(
+                                    connec_state,
+                                    ConnectionState::OpenDesiredByRemote
+                                        | ConnectionState::Closing
+                                        | ConnectionState::Opening
+                                ));
+                            }
+                        } else {
+                            error!(
+                                target: "sub-libp2p",
+                                "OpenDesiredByRemote: State mismatch in the custom protos handler"
+                            );
+                            debug_assert!(false);
+                        }
+
+                        *entry.into_mut() = PeerState::Enabled { connections };
+                    }
+
+                    // Disabled => Disabled | Incoming
+                    PeerState::Disabled {
+                        mut connections,
+                        backoff_until,
+                    } => {
+                        if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, _)| *c == connection_id)
+                        {
+                            if let ConnectionState::Closed = *connec_state {
+                                *connec_state = ConnectionState::OpenDesiredByRemote;
+
+                                let incoming_id = self.next_incoming_index;
+                                self.next_incoming_index.0 += 1;
+
+                                trace!(target: "sub-libp2p", "PSM <= Incoming({}, {:?}, {:?}).",
+									peer_id, set_id, incoming_id);
+                                self.protocol_controller_handles[usize::from(set_id)]
+                                    .incoming_connection(peer_id, incoming_id);
+                                self.incoming.push(IncomingPeer {
+                                    peer_id,
+                                    set_id,
+                                    alive: true,
+                                    incoming_id,
+                                    handshake,
+                                });
+
+                                *entry.into_mut() = PeerState::Incoming {
+                                    connections,
+                                    backoff_until,
+                                    peerset_rejected: false,
+                                    incoming_index: incoming_id,
+                                };
+                            } else {
+                                // Connections in `OpeningThenClosing` and `Closing` state can be
+                                // in a Closed phase, and as such can emit `OpenDesiredByRemote`
+                                // messages.
+                                // We ignore them.
+                                debug_assert!(matches!(
+                                    connec_state,
+                                    ConnectionState::OpeningThenClosing | ConnectionState::Closing
+                                ));
+                                *entry.into_mut() = PeerState::Disabled {
+                                    connections,
+                                    backoff_until,
+                                };
+                            }
+                        } else {
+                            error!(
+                                target: "sub-libp2p",
+                                "OpenDesiredByRemote: State mismatch in the custom protos handler"
+                            );
+                            debug_assert!(false);
+                        }
+                    }
+
+                    // DisabledPendingEnable => Enabled | DisabledPendingEnable
+                    PeerState::DisabledPendingEnable {
+                        mut connections,
+                        timer,
+                        timer_deadline,
+                    } => {
+                        if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, _)| *c == connection_id)
+                        {
+                            if let ConnectionState::Closed = *connec_state {
+                                trace!(target: "sub-libp2p", "Handler({:?}, {:?}) <= Open({:?})",
+									peer_id, connection_id, set_id);
+                                self.events.push_back(ToSwarm::NotifyHandler {
+                                    peer_id,
+                                    handler: NotifyHandler::One(connection_id),
+                                    event: NotifsHandlerIn::Open {
+                                        protocol_index: set_id.into(),
+                                    },
+                                });
+                                *connec_state = ConnectionState::Opening;
+
+                                *entry.into_mut() = PeerState::Enabled { connections };
+                            } else {
+                                // Connections in `OpeningThenClosing` and `Closing` state can be
+                                // in a Closed phase, and as such can emit `OpenDesiredByRemote`
+                                // messages.
+                                // We ignore them.
+                                debug_assert!(matches!(
+                                    connec_state,
+                                    ConnectionState::OpeningThenClosing | ConnectionState::Closing
+                                ));
+                                *entry.into_mut() = PeerState::DisabledPendingEnable {
+                                    connections,
+                                    timer,
+                                    timer_deadline,
+                                };
+                            }
+                        } else {
+                            error!(
+                                target: "sub-libp2p",
+                                "OpenDesiredByRemote: State mismatch in the custom protos handler"
+                            );
+                            debug_assert!(false);
+                        }
+                    }
+
+                    state => {
+                        error!(target: "sub-libp2p",
+							   "OpenDesiredByRemote: Unexpected state in the custom protos handler: {:?}",
+							   state);
+                        debug_assert!(false);
+                    }
+                };
+            }
+
+            NotifsHandlerOut::CloseDesired { protocol_index } => {
+                let set_id = SetId::from(protocol_index);
+
+                trace!(target: "sub-libp2p",
+					"Handler({}, {:?}) => CloseDesired({:?})",
+					peer_id, connection_id, set_id);
+
+                let mut entry = if let Entry::Occupied(entry) = self.peers.entry((peer_id, set_id))
+                {
+                    entry
+                } else {
+                    error!(target: "sub-libp2p", "CloseDesired: State mismatch in the custom protos handler");
+                    debug_assert!(false);
+                    return;
+                };
+
+                match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+                    // Enabled => Enabled | Disabled
+                    PeerState::Enabled { mut connections } => {
+                        debug_assert!(connections.iter().any(|(_, s)| matches!(
+                            s,
+                            ConnectionState::Opening | ConnectionState::Open(_)
+                        )));
+
+                        let pos = if let Some(pos) =
+                            connections.iter().position(|(c, _)| *c == connection_id)
+                        {
+                            pos
+                        } else {
+                            error!(target: "sub-libp2p",
+								"CloseDesired: State mismatch in the custom protos handler");
+                            debug_assert!(false);
+                            return;
+                        };
+
+                        if matches!(connections[pos].1, ConnectionState::Closing) {
+                            *entry.into_mut() = PeerState::Enabled { connections };
+                            return;
+                        }
+
+                        debug_assert!(matches!(connections[pos].1, ConnectionState::Open(_)));
+                        connections[pos].1 = ConnectionState::Closing;
+
+                        trace!(target: "sub-libp2p", "Handler({}, {:?}) <= Close({:?})", peer_id, connection_id, set_id);
+                        self.events.push_back(ToSwarm::NotifyHandler {
+                            peer_id,
+                            handler: NotifyHandler::One(connection_id),
+                            event: NotifsHandlerIn::Close {
+                                protocol_index: set_id.into(),
+                            },
+                        });
+
+                        if let Some((replacement_pos, replacement_sink)) = connections
+                            .iter()
+                            .enumerate()
+                            .find_map(|(num, (_, s))| match s {
+                                ConnectionState::Open(s) => Some((num, s.clone())),
+                                _ => None,
+                            })
+                        {
+                            if pos <= replacement_pos {
+                                trace!(target: "sub-libp2p", "External API <= Sink replaced({:?}, {:?})", peer_id, set_id);
+                                let event = NotificationsOut::CustomProtocolReplaced {
+                                    peer_id,
+                                    set_id,
+                                    notifications_sink: replacement_sink.clone(),
+                                };
+                                self.events.push_back(ToSwarm::GenerateEvent(event));
+                            }
+
+                            *entry.into_mut() = PeerState::Enabled { connections };
+                        } else {
+                            // List of open connections wasn't empty before but now it is.
+                            if !connections
+                                .iter()
+                                .any(|(_, s)| matches!(s, ConnectionState::Opening))
+                            {
+                                trace!(target: "sub-libp2p", "PSM <= Dropped({}, {:?})", peer_id, set_id);
+                                self.protocol_controller_handles[usize::from(set_id)]
+                                    .dropped(peer_id);
+                                *entry.into_mut() = PeerState::Disabled {
+                                    connections,
+                                    backoff_until: None,
+                                };
+                            } else {
+                                *entry.into_mut() = PeerState::Enabled { connections };
+                            }
+
+                            trace!(target: "sub-libp2p", "External API <= Closed({}, {:?})", peer_id, set_id);
+                            let event = NotificationsOut::CustomProtocolClosed { peer_id, set_id };
+                            self.events.push_back(ToSwarm::GenerateEvent(event));
+                        }
+                    }
+
+                    // All connections in `Disabled` and `DisabledPendingEnable` have been sent a
+                    // `Close` message already, and as such ignore any `CloseDesired` message.
+                    state @ PeerState::Disabled { .. }
+                    | state @ PeerState::DisabledPendingEnable { .. } => {
+                        *entry.into_mut() = state;
+                    }
+                    state => {
+                        error!(target: "sub-libp2p",
+							"Unexpected state in the custom protos handler: {:?}",
+							state);
+                    }
+                }
+            }
+
+            NotifsHandlerOut::CloseResult { protocol_index } => {
+                let set_id = SetId::from(protocol_index);
+
+                trace!(target: "sub-libp2p",
+					"Handler({}, {:?}) => CloseResult({:?})",
+					peer_id, connection_id, set_id);
+
+                match self.peers.get_mut(&(peer_id, set_id)) {
+                    // Move the connection from `Closing` to `Closed`.
+                    Some(PeerState::Incoming { connections, .. })
+                    | Some(PeerState::DisabledPendingEnable { connections, .. })
+                    | Some(PeerState::Disabled { connections, .. })
+                    | Some(PeerState::Enabled { connections, .. }) => {
+                        if let Some((_, connec_state)) = connections.iter_mut().find(|(c, s)| {
+                            *c == connection_id && matches!(s, ConnectionState::Closing)
+                        }) {
+                            *connec_state = ConnectionState::Closed;
+                        } else {
+                            error!(target: "sub-libp2p",
+								"CloseResult: State mismatch in the custom protos handler");
+                            debug_assert!(false);
+                        }
+                    }
+
+                    state => {
+                        error!(target: "sub-libp2p",
+							   "CloseResult: Unexpected state in the custom protos handler: {:?}",
+							   state);
+                        debug_assert!(false);
+                    }
+                }
+            }
+
+            NotifsHandlerOut::OpenResultOk {
+                protocol_index,
+                negotiated_fallback,
+                received_handshake,
+                notifications_sink,
+                inbound,
+                ..
+            } => {
+                let set_id = SetId::from(protocol_index);
+                trace!(target: "sub-libp2p",
+					"Handler({}, {:?}) => OpenResultOk({:?})",
+					peer_id, connection_id, set_id);
+
+                match self.peers.get_mut(&(peer_id, set_id)) {
+                    Some(PeerState::Enabled { connections, .. }) => {
+                        debug_assert!(connections.iter().any(|(_, s)| matches!(
+                            s,
+                            ConnectionState::Opening | ConnectionState::Open(_)
+                        )));
+                        let any_open = connections
+                            .iter()
+                            .any(|(_, s)| matches!(s, ConnectionState::Open(_)));
+
+                        if let Some((_, connec_state)) = connections.iter_mut().find(|(c, s)| {
+                            *c == connection_id && matches!(s, ConnectionState::Opening)
+                        }) {
+                            if !any_open {
+                                trace!(target: "sub-libp2p", "External API <= Open({}, {:?})", peer_id, set_id);
+                                let event = NotificationsOut::CustomProtocolOpen {
+                                    peer_id,
+                                    set_id,
+                                    inbound,
+                                    direction: if inbound {
+                                        Direction::Inbound
+                                    } else {
+                                        Direction::Outbound
+                                    },
+                                    received_handshake: received_handshake.clone(),
+                                    negotiated_fallback: negotiated_fallback.clone(),
+                                    notifications_sink: notifications_sink.clone(),
+                                };
+                                self.events.push_back(ToSwarm::GenerateEvent(event));
+                            }
+                            *connec_state = ConnectionState::Open(notifications_sink);
+                        } else if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, s)| {
+                                *c == connection_id
+                                    && matches!(s, ConnectionState::OpeningThenClosing)
+                            })
+                        {
+                            *connec_state = ConnectionState::Closing;
+                        } else {
+                            error!(target: "sub-libp2p",
+								"OpenResultOk State mismatch in the custom protos handler");
+                            debug_assert!(false);
+                        }
+                    }
+
+                    Some(PeerState::Incoming { connections, .. })
+                    | Some(PeerState::DisabledPendingEnable { connections, .. })
+                    | Some(PeerState::Disabled { connections, .. }) => {
+                        if let Some((_, connec_state)) = connections.iter_mut().find(|(c, s)| {
+                            *c == connection_id && matches!(s, ConnectionState::OpeningThenClosing)
+                        }) {
+                            *connec_state = ConnectionState::Closing;
+                        } else {
+                            error!(target: "sub-libp2p",
+								"OpenResultOk State mismatch in the custom protos handler");
+                            debug_assert!(false);
+                        }
+                    }
+
+                    state => {
+                        error!(target: "sub-libp2p",
+							   "OpenResultOk: Unexpected state in the custom protos handler: {:?}",
+							   state);
+                        debug_assert!(false);
+                    }
+                }
+            }
+
+            NotifsHandlerOut::OpenResultErr { protocol_index } => {
+                let set_id = SetId::from(protocol_index);
+                trace!(target: "sub-libp2p",
+					"Handler({:?}, {:?}) => OpenResultErr({:?})",
+					peer_id, connection_id, set_id);
+
+                let mut entry = if let Entry::Occupied(entry) = self.peers.entry((peer_id, set_id))
+                {
+                    entry
+                } else {
+                    error!(target: "sub-libp2p", "OpenResultErr: State mismatch in the custom protos handler");
+                    debug_assert!(false);
+                    return;
+                };
+
+                match mem::replace(entry.get_mut(), PeerState::Poisoned) {
+                    PeerState::Enabled { mut connections } => {
+                        debug_assert!(connections.iter().any(|(_, s)| matches!(
+                            s,
+                            ConnectionState::Opening | ConnectionState::Open(_)
+                        )));
+
+                        if let Some((_, connec_state)) = connections.iter_mut().find(|(c, s)| {
+                            *c == connection_id && matches!(s, ConnectionState::Opening)
+                        }) {
+                            *connec_state = ConnectionState::Closed;
+                        } else if let Some((_, connec_state)) =
+                            connections.iter_mut().find(|(c, s)| {
+                                *c == connection_id
+                                    && matches!(s, ConnectionState::OpeningThenClosing)
+                            })
+                        {
+                            *connec_state = ConnectionState::Closing;
+                        } else {
+                            error!(target: "sub-libp2p",
+								"OpenResultErr: State mismatch in the custom protos handler");
+                            debug_assert!(false);
+                        }
+
+                        if !connections.iter().any(|(_, s)| {
+                            matches!(s, ConnectionState::Opening | ConnectionState::Open(_))
+                        }) {
+                            trace!(target: "sub-libp2p", "PSM <= Dropped({:?}, {:?})", peer_id, set_id);
+                            self.protocol_controller_handles[usize::from(set_id)].dropped(peer_id);
+
+                            let ban_dur = Uniform::new(5, 10).sample(&mut rand::thread_rng());
+                            *entry.into_mut() = PeerState::Disabled {
+                                connections,
+                                backoff_until: Some(Instant::now() + Duration::from_secs(ban_dur)),
+                            };
+                        } else {
+                            *entry.into_mut() = PeerState::Enabled { connections };
+                        }
+                    }
+                    mut state @ PeerState::Incoming { .. }
+                    | mut state @ PeerState::DisabledPendingEnable { .. }
+                    | mut state @ PeerState::Disabled { .. } => {
+                        match &mut state {
+                            PeerState::Incoming { connections, .. }
+                            | PeerState::Disabled { connections, .. }
+                            | PeerState::DisabledPendingEnable { connections, .. } => {
+                                if let Some((_, connec_state)) =
+                                    connections.iter_mut().find(|(c, s)| {
+                                        *c == connection_id
+                                            && matches!(s, ConnectionState::OpeningThenClosing)
+                                    })
+                                {
+                                    *connec_state = ConnectionState::Closing;
+                                } else {
+                                    error!(target: "sub-libp2p",
+										"OpenResultErr: State mismatch in the custom protos handler");
+                                    debug_assert!(false);
+                                }
+                            }
+                            _ => unreachable!(
+                                "Match branches are the same as the one on which we
+							enter this block; qed"
+                            ),
+                        };
+
+                        *entry.into_mut() = state;
+                    }
+                    state => {
+                        error!(target: "sub-libp2p",
+							"Unexpected state in the custom protos handler: {:?}",
+							state);
+                        debug_assert!(false);
+                    }
+                };
+            }
+
+            NotifsHandlerOut::Notification {
+                protocol_index,
+                message,
+            } => {
+                let set_id = SetId::from(protocol_index);
+                if self.is_open(&peer_id, set_id) {
+                    trace!(
+                        target: "sub-libp2p",
+                        "Handler({:?}) => Notification({}, {:?}, {} bytes)",
+                        connection_id,
+                        peer_id,
+                        set_id,
+                        message.len()
+                    );
+                    trace!(
+                        target: "sub-libp2p",
+                        "External API <= Message({}, {:?})",
+                        peer_id,
+                        set_id,
+                    );
+                    let event = NotificationsOut::Notification {
+                        peer_id,
+                        set_id,
+                        message: message.clone(),
+                    };
+                    self.events.push_back(ToSwarm::GenerateEvent(event));
+                } else {
+                    trace!(
+                        target: "sub-libp2p",
+                        "Handler({:?}) => Post-close notification({}, {:?}, {} bytes)",
+                        connection_id,
+                        peer_id,
+                        set_id,
+                        message.len()
+                    );
+                }
+            }
+        }
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+        _params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        if let Some(event) = self.events.pop_front() {
+            return Poll::Ready(event);
+        }
+
+        // Poll for instructions from the protocol controllers.
+        loop {
+            match futures::Stream::poll_next(Pin::new(&mut self.from_protocol_controllers), cx) {
+                Poll::Ready(Some(Message::Accept(index))) => {
+                    self.peerset_report_preaccept(index);
+                }
+                Poll::Ready(Some(Message::Reject(index))) => {
+                    self.peerset_report_reject(index);
+                }
+                Poll::Ready(Some(Message::Connect {
+                    peer_id, set_id, ..
+                })) => {
+                    self.peerset_report_connect(peer_id, set_id);
+                }
+                Poll::Ready(Some(Message::Drop {
+                    peer_id, set_id, ..
+                })) => {
+                    self.peerset_report_disconnect(peer_id, set_id);
+                }
+                Poll::Ready(None) => {
+                    error!(
+                        target: "sub-libp2p",
+                        "Protocol controllers receiver stream has returned `None`. Ignore this error if the node is shutting down.",
+                    );
+                    break;
+                }
+                Poll::Pending => break,
+            }
+        }
+
+        // poll commands from protocols
+        loop {
+            match futures::Stream::poll_next(Pin::new(&mut self.command_streams), cx) {
+                Poll::Ready(Some((set_id, command))) => match command {
+                    NotificationCommand::SetHandshake(handshake) => {
+                        self.set_notif_protocol_handshake(set_id.into(), handshake);
+                    }
+                    NotificationCommand::OpenSubstream(_peer)
+                    | NotificationCommand::CloseSubstream(_peer) => {
+                        todo!("substream control not implemented");
+                    }
+                },
+                Poll::Ready(None) => {
+                    trace!(target: LOG_TARGET, "Protocol command streams have been shut down");
+                    break;
+                }
+                Poll::Pending => break,
+            }
+        }
+
+        while let Poll::Ready(Some((result, index))) =
+            self.pending_inbound_validations.poll_next_unpin(cx)
+        {
+            match result {
+                Ok(ValidationResult::Accept) => {
+                    self.protocol_report_accept(index);
+                }
+                Ok(ValidationResult::Reject) => {
+                    self.protocol_report_reject(index);
+                }
+                Err(_) => {
+                    error!(target: LOG_TARGET, "Protocol has shut down");
+                    break;
+                }
+            }
+        }
+
+        while let Poll::Ready(Some((delay_id, peer_id, set_id))) =
+            Pin::new(&mut self.delays).poll_next(cx)
+        {
+            let peer_state = match self.peers.get_mut(&(peer_id, set_id)) {
+                Some(s) => s,
+                // We intentionally never remove elements from `delays`, and it may
+                // thus contain peers which are now gone. This is a normal situation.
+                None => continue,
+            };
+
+            match peer_state {
+                PeerState::Backoff { timer, .. } if *timer == delay_id => {
+                    trace!(target: "sub-libp2p", "Libp2p <= Clean up ban of {:?} from the state ({:?})", peer_id, set_id);
+                    self.peers.remove(&(peer_id, set_id));
+                }
+
+                PeerState::PendingRequest { timer, .. } if *timer == delay_id => {
+                    trace!(target: "sub-libp2p", "Libp2p <= Dial {:?} now that ban has expired ({:?})", peer_id, set_id);
+                    self.events.push_back(ToSwarm::Dial {
+                        opts: peer_id.into(),
+                    });
+                    *peer_state = PeerState::Requested;
+                }
+
+                PeerState::DisabledPendingEnable {
+                    connections,
+                    timer,
+                    timer_deadline,
+                } if *timer == delay_id => {
+                    // The first element of `closed` is chosen to open the notifications substream.
+                    if let Some((connec_id, connec_state)) = connections
+                        .iter_mut()
+                        .find(|(_, s)| matches!(s, ConnectionState::Closed))
+                    {
+                        trace!(target: "sub-libp2p", "Handler({}, {:?}) <= Open({:?}) (ban expired)",
+							peer_id, *connec_id, set_id);
+                        self.events.push_back(ToSwarm::NotifyHandler {
+                            peer_id,
+                            handler: NotifyHandler::One(*connec_id),
+                            event: NotifsHandlerIn::Open {
+                                protocol_index: set_id.into(),
+                            },
+                        });
+                        *connec_state = ConnectionState::Opening;
+                        *peer_state = PeerState::Enabled {
+                            connections: mem::take(connections),
+                        };
+                    } else {
+                        *timer_deadline = Instant::now() + Duration::from_secs(5);
+                        let delay = futures_timer::Delay::new(Duration::from_secs(5));
+                        let timer = *timer;
+                        self.delays.push(
+                            async move {
+                                delay.await;
+                                (timer, peer_id, set_id)
+                            }
+                            .boxed(),
+                        );
+                    }
+                }
+
+                // We intentionally never remove elements from `delays`, and it may
+                // thus contain obsolete entries. This is a normal situation.
+                _ => {}
+            }
+        }
+
+        if let Some(event) = self.events.pop_front() {
+            return Poll::Ready(event);
+        }
+
+        Poll::Pending
+    }
+}
+
+#[cfg(test)]
+#[allow(deprecated)]
+mod tests {
+    use super::*;
+    use crate::{
+        mock::MockPeerStore,
+        protocol::notifications::handler::tests::*,
+        protocol_controller::{IncomingIndex, ProtoSetConfig, ProtocolController},
+    };
+    use libp2p::swarm::AddressRecord;
+    use sc_utils::mpsc::tracing_unbounded;
+    use std::{collections::HashSet, iter};
+
+    impl PartialEq for ConnectionState {
+        #[allow(clippy::match_like_matches_macro)]
+        fn eq(&self, other: &ConnectionState) -> bool {
+            match (self, other) {
+                (ConnectionState::Closed, ConnectionState::Closed) => true,
+                (ConnectionState::Closing, ConnectionState::Closing) => true,
+                (ConnectionState::Opening, ConnectionState::Opening) => true,
+                (ConnectionState::OpeningThenClosing, ConnectionState::OpeningThenClosing) => true,
+                (ConnectionState::OpenDesiredByRemote, ConnectionState::OpenDesiredByRemote) => {
+                    true
+                }
+                (ConnectionState::Open(_), ConnectionState::Open(_)) => true,
+                _ => false,
+            }
+        }
+    }
+
+    #[derive(Clone)]
+    struct MockPollParams {
+        peer_id: PeerId,
+        addr: Multiaddr,
+    }
+
+    impl PollParameters for MockPollParams {
+        type SupportedProtocolsIter = std::vec::IntoIter<Vec<u8>>;
+        type ListenedAddressesIter = std::vec::IntoIter<Multiaddr>;
+        type ExternalAddressesIter = std::vec::IntoIter<AddressRecord>;
+
+        fn supported_protocols(&self) -> Self::SupportedProtocolsIter {
+            vec![].into_iter()
+        }
+
+        fn listened_addresses(&self) -> Self::ListenedAddressesIter {
+            vec![self.addr.clone()].into_iter()
+        }
+
+        fn external_addresses(&self) -> Self::ExternalAddressesIter {
+            vec![].into_iter()
+        }
+
+        fn local_peer_id(&self) -> &PeerId {
+            &self.peer_id
+        }
+    }
+
+    fn development_notifs() -> (
+        Notifications,
+        ProtocolController,
+        Box<dyn crate::service::traits::NotificationService>,
+    ) {
+        let (protocol_handle_pair, notif_service) =
+            crate::protocol::notifications::service::notification_service("/proto/1".into());
+        let (to_notifications, from_controller) =
+            tracing_unbounded("test_controller_to_notifications", 10_000);
+
+        let (handle, controller) = ProtocolController::new(
+            SetId::from(0),
+            ProtoSetConfig {
+                in_peers: 25,
+                out_peers: 25,
+                reserved_nodes: HashSet::new(),
+                reserved_only: false,
+            },
+            to_notifications,
+            Box::new(MockPeerStore {}),
+        );
+
+        let (notif_handle, command_stream) = protocol_handle_pair.split();
+        (
+            Notifications::new(
+                vec![handle],
+                from_controller,
+                None,
+                iter::once((
+                    ProtocolConfig {
+                        name: "/foo".into(),
+                        fallback_names: Vec::new(),
+                        handshake: vec![1, 2, 3, 4],
+                        max_notification_size: u64::MAX,
+                    },
+                    notif_handle,
+                    command_stream,
+                )),
+            ),
+            controller,
+            Box::new(notif_service),
+        )
+    }
+
+    #[test]
+    fn update_handshake() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        let inner = notif
+            .notif_protocols
+            .get_mut(0)
+            .unwrap()
+            .handshake
+            .read()
+            .clone();
+        assert_eq!(inner, vec![1, 2, 3, 4]);
+
+        notif.set_notif_protocol_handshake(0.into(), vec![5, 6, 7, 8]);
+
+        let inner = notif
+            .notif_protocols
+            .get_mut(0)
+            .unwrap()
+            .handshake
+            .read()
+            .clone();
+        assert_eq!(inner, vec![5, 6, 7, 8]);
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn update_unknown_handshake() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        notif.set_notif_protocol_handshake(1337.into(), vec![5, 6, 7, 8]);
+    }
+
+    #[test]
+    fn disconnect_backoff_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        let peer = PeerId::random();
+        notif.peers.insert(
+            (peer, 0.into()),
+            PeerState::Backoff {
+                timer: DelayId(0),
+                timer_deadline: Instant::now(),
+            },
+        );
+        notif.disconnect_peer(&peer, 0.into());
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, 0.into())),
+            Some(PeerState::Backoff {
+                timer: DelayId(0),
+                ..
+            })
+        ));
+    }
+
+    #[test]
+    fn disconnect_pending_request() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+
+        notif.peers.insert(
+            (peer, 0.into()),
+            PeerState::PendingRequest {
+                timer: DelayId(0),
+                timer_deadline: Instant::now(),
+            },
+        );
+        notif.disconnect_peer(&peer, 0.into());
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, 0.into())),
+            Some(PeerState::PendingRequest {
+                timer: DelayId(0),
+                ..
+            })
+        ));
+    }
+
+    #[test]
+    fn disconnect_requested_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        let peer = PeerId::random();
+        notif.peers.insert((peer, 0.into()), PeerState::Requested);
+        notif.disconnect_peer(&peer, 0.into());
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, 0.into())),
+            Some(PeerState::Requested)
+        ));
+    }
+
+    #[test]
+    fn disconnect_disabled_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        notif.peers.insert(
+            (peer, 0.into()),
+            PeerState::Disabled {
+                backoff_until: None,
+                connections: SmallVec::new(),
+            },
+        );
+        notif.disconnect_peer(&peer, 0.into());
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, 0.into())),
+            Some(PeerState::Disabled {
+                backoff_until: None,
+                ..
+            })
+        ));
+    }
+
+    #[test]
+    fn remote_opens_connection_and_substream() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+
+        if let Some(&PeerState::Disabled {
+            ref connections,
+            backoff_until: None,
+        }) = notif.peers.get(&(peer, 0.into()))
+        {
+            assert_eq!(connections[0], (conn, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+
+        if let Some(&PeerState::Incoming {
+            ref connections,
+            backoff_until: None,
+            ..
+        }) = notif.peers.get(&(peer, 0.into()))
+        {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0], (conn, ConnectionState::OpenDesiredByRemote));
+        } else {
+            panic!("invalid state");
+        }
+
+        assert!(std::matches!(
+            notif.incoming.pop(),
+            Some(IncomingPeer {
+                alive: true,
+                incoming_id: IncomingIndex(0),
+                ..
+            }),
+        ));
+    }
+
+    #[tokio::test]
+    async fn disconnect_remote_substream_before_handled_by_controller() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        notif.disconnect_peer(&peer, 0.into());
+
+        if let Some(&PeerState::Disabled {
+            ref connections,
+            backoff_until: None,
+        }) = notif.peers.get(&(peer, 0.into()))
+        {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0], (conn, ConnectionState::Closing));
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[test]
+    fn peerset_report_connect_backoff() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        //
+        // there is not straight-forward way of adding backoff to `PeerState::Disabled`
+        // so manually adjust the value in order to progress on to the next stage.
+        // This modification together with `ConnectionClosed` will conver the peer
+        // state into `PeerState::Backoff`.
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        let timer = if let Some(&PeerState::Backoff { timer_deadline, .. }) =
+            notif.peers.get(&(peer, set_id))
+        {
+            timer_deadline
+        } else {
+            panic!("invalid state");
+        };
+
+        // attempt to connect the backed-off peer and verify that the request is pending
+        notif.peerset_report_connect(peer, set_id);
+
+        if let Some(&PeerState::PendingRequest { timer_deadline, .. }) =
+            notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(timer, timer_deadline);
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[test]
+    fn peerset_connect_incoming() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+
+        // attempt to connect to the peer and verify that the peer state is `Enabled`;
+        // we rely on implementation detail that incoming indices are counted from 0
+        // to not mock the `Peerset`
+        notif.protocol_report_accept(IncomingIndex(0));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+    }
+
+    #[test]
+    fn peerset_disconnect_disable_pending_enable() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        // switch state to `DisabledPendingEnable`
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+
+        notif.peerset_report_disconnect(peer, set_id);
+
+        if let Some(PeerState::Disabled { backoff_until, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert!(backoff_until.is_some());
+            assert!(backoff_until.unwrap() > Instant::now());
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[test]
+    fn peerset_disconnect_enabled() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        // Set peer into `Enabled` state.
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        // we rely on the implementation detail that incoming indices are counted from 0
+        // to not mock the `Peerset`
+        notif.protocol_report_accept(IncomingIndex(0));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        // disconnect peer and verify that the state is `Disabled`
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+    }
+
+    #[test]
+    fn peerset_disconnect_requested() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+
+        // Set peer into `Requested` state.
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Requested)
+        ));
+
+        // disconnect peer and verify that the state is `Disabled`
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(notif.peers.get(&(peer, set_id)).is_none());
+    }
+
+    #[test]
+    fn peerset_disconnect_pending_request() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+
+        // attempt to connect the backed-off peer and verify that the request is pending
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::PendingRequest { .. })
+        ));
+
+        // attempt to disconnect the backed-off peer and verify that the request is pending
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+    }
+
+    #[test]
+    fn peerset_accept_peer_not_alive() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        assert!(std::matches!(
+            notif.incoming[0],
+            IncomingPeer {
+                alive: true,
+                incoming_id: IncomingIndex(0),
+                ..
+            },
+        ));
+
+        notif.disconnect_peer(&peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+        assert!(std::matches!(
+            notif.incoming[0],
+            IncomingPeer {
+                alive: false,
+                incoming_id: IncomingIndex(0),
+                ..
+            },
+        ));
+
+        notif.protocol_report_accept(IncomingIndex(0));
+        assert_eq!(notif.incoming.len(), 0);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(PeerState::Disabled { .. })
+        ));
+    }
+
+    #[test]
+    fn secondary_connection_peer_state_incoming() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        if let Some(PeerState::Incoming { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0], (conn, ConnectionState::OpenDesiredByRemote));
+        } else {
+            panic!("invalid state");
+        }
+
+        // add another connection
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn2,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+
+        if let Some(PeerState::Incoming { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections.len(), 2);
+            assert_eq!(connections[0], (conn, ConnectionState::OpenDesiredByRemote));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[test]
+    fn close_connection_for_disabled_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(notif.peers.get(&(peer, set_id)).is_none());
+    }
+
+    #[test]
+    fn close_connection_for_incoming_peer_one_connection() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(notif.peers.get(&(peer, set_id)).is_none());
+        assert!(std::matches!(
+            notif.incoming[0],
+            IncomingPeer {
+                alive: false,
+                incoming_id: IncomingIndex(0),
+                ..
+            },
+        ));
+    }
+
+    #[test]
+    fn close_connection_for_incoming_peer_two_connections() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let conn1 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conns = SmallVec::<
+            [(ConnectionId, ConnectionState); crate::MAX_CONNECTIONS_PER_PEER],
+        >::from(vec![(conn, ConnectionState::Closed)]);
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        conns.push((conn1, ConnectionState::Closed));
+
+        if let Some(PeerState::Incoming {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(connections.len(), 2);
+            assert_eq!(connections[0], (conn, ConnectionState::OpenDesiredByRemote));
+            assert_eq!(connections[1], (conn1, ConnectionState::Closed));
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        if let Some(PeerState::Disabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0], (conn1, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[test]
+    fn connection_and_substream_open() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        // move the peer to `Enabled` state
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        // We rely on the implementation detail that incoming indices are counted
+        // from 0 to not mock the `Peerset`.
+        notif.protocol_report_accept(IncomingIndex(0));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        // open new substream
+        let event = conn_yielder.open_substream(peer, 0, connected, vec![1, 2, 3, 4]);
+
+        notif.on_connection_handler_event(peer, conn, event);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0].0, conn);
+            assert!(std::matches!(connections[0].1, ConnectionState::Open(_)));
+        }
+
+        assert!(std::matches!(
+            notif.events[notif.events.len() - 1],
+            ToSwarm::GenerateEvent(NotificationsOut::CustomProtocolOpen { .. })
+        ));
+    }
+
+    #[test]
+    fn connection_closed_sink_replaced() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        // open two connections
+        for conn_id in [conn1, conn2] {
+            notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+                libp2p::swarm::behaviour::ConnectionEstablished {
+                    peer_id: peer,
+                    connection_id: conn_id,
+                    endpoint: &connected,
+                    failed_addresses: &[],
+                    other_established: 0usize,
+                },
+            ));
+        }
+
+        if let Some(PeerState::Disabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Closed));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        // open substreams on both active connections
+        notif.peerset_report_connect(peer, set_id);
+        notif.on_connection_handler_event(
+            peer,
+            conn2,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+
+        if let Some(PeerState::Enabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Opening));
+            assert_eq!(connections[1], (conn2, ConnectionState::Opening));
+        } else {
+            panic!("invalid state");
+        }
+
+        // add two new substreams, one for each connection and verify that both are in open state
+        for conn in [conn1, conn2].iter() {
+            notif.on_connection_handler_event(
+                peer,
+                *conn,
+                conn_yielder.open_substream(peer, 0, connected.clone(), vec![1, 2, 3, 4]),
+            );
+        }
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(connections[0].0, conn1);
+            assert!(std::matches!(connections[0].1, ConnectionState::Open(_)));
+            assert_eq!(connections[1].0, conn2);
+            assert!(std::matches!(connections[1].1, ConnectionState::Open(_)));
+        } else {
+            panic!("invalid state");
+        }
+
+        // check peer information
+        assert_eq!(notif.open_peers().collect::<Vec<_>>(), vec![&peer],);
+
+        // close the other connection and verify that notification replacement event is emitted
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(connections.len(), 1);
+            assert_eq!(connections[0].0, conn2);
+            assert!(std::matches!(connections[0].1, ConnectionState::Open(_)));
+        } else {
+            panic!("invalid state");
+        }
+
+        assert!(std::matches!(
+            notif.events[notif.events.len() - 1],
+            ToSwarm::GenerateEvent(NotificationsOut::CustomProtocolReplaced { .. })
+        ));
+    }
+
+    #[test]
+    fn dial_failure_for_requested_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+
+        // Set peer into `Requested` state.
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Requested)
+        ));
+
+        notif.on_swarm_event(FromSwarm::DialFailure(
+            libp2p::swarm::behaviour::DialFailure {
+                peer_id: Some(peer),
+                error: &libp2p::swarm::DialError::Banned,
+                connection_id: ConnectionId::new_unchecked(1337),
+            },
+        ));
+
+        if let Some(PeerState::Backoff { timer_deadline, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert!(timer_deadline > &Instant::now());
+        } else {
+            panic!("invalid state");
+        }
+    }
+
+    #[tokio::test]
+    async fn write_notification() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            conn_yielder.open_substream(peer, 0, connected, vec![1, 2, 3, 4]),
+        );
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(connections[0].0, conn);
+            assert!(std::matches!(connections[0].1, ConnectionState::Open(_)));
+        } else {
+            panic!("invalid state");
+        }
+
+        notif
+            .peers
+            .get(&(peer, set_id))
+            .unwrap()
+            .get_open()
+            .unwrap()
+            .send_sync_notification(vec![1, 3, 3, 7]);
+        assert_eq!(
+            conn_yielder.get_next_event(peer, set_id.into()).await,
+            Some(vec![1, 3, 3, 7])
+        );
+    }
+
+    #[test]
+    fn peerset_report_connect_backoff_expired() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let backoff_duration = Duration::from_millis(100);
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(Instant::now().checked_add(backoff_duration).unwrap());
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        // wait until the backoff time has passed
+        std::thread::sleep(backoff_duration * 2);
+
+        // attempt to connect the backed-off peer and verify that the request is pending
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Requested { .. })
+        ))
+    }
+
+    #[test]
+    fn peerset_report_disconnect_disabled() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+    }
+
+    #[test]
+    fn peerset_report_disconnect_backoff() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let backoff_duration = Duration::from_secs(2);
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(Instant::now().checked_add(backoff_duration).unwrap());
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+    }
+
+    #[test]
+    fn peer_is_backed_off_if_both_connections_get_closed_while_peer_is_disabled_with_back_off() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn2,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        // switch state to `DisabledPendingEnable`
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected.clone(), vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn2,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+    }
+
+    #[test]
+    fn inject_connection_closed_incoming_with_backoff() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+
+        // manually add backoff for the entry
+        if let Some(&mut PeerState::Incoming {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, 0.into()))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+    }
+
+    #[test]
+    fn two_connections_inactive_connection_gets_closed_peer_state_is_still_incoming() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        // open two connections
+        for conn_id in [conn1, conn2] {
+            notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+                libp2p::swarm::behaviour::ConnectionEstablished {
+                    peer_id: peer,
+                    connection_id: conn_id,
+                    endpoint: &connected,
+                    failed_addresses: &[],
+                    other_established: 0usize,
+                },
+            ));
+        }
+
+        if let Some(PeerState::Disabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Closed));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn1,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get_mut(&(peer, 0.into())),
+            Some(&mut PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn2,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+    }
+
+    #[test]
+    fn two_connections_active_connection_gets_closed_peer_state_is_disabled() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        // open two connections
+        for conn_id in [conn1, conn2] {
+            notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+                libp2p::swarm::behaviour::ConnectionEstablished {
+                    peer_id: peer,
+                    connection_id: conn_id,
+                    endpoint: &ConnectedPoint::Listener {
+                        local_addr: Multiaddr::empty(),
+                        send_back_addr: Multiaddr::empty(),
+                    },
+                    failed_addresses: &[],
+                    other_established: 0usize,
+                },
+            ));
+        }
+
+        if let Some(PeerState::Disabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Closed));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn1,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get_mut(&(peer, 0.into())),
+            Some(PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+    }
+
+    #[test]
+    fn inject_connection_closed_for_active_connection() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        // open two connections
+        for conn_id in [conn1, conn2] {
+            notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+                libp2p::swarm::behaviour::ConnectionEstablished {
+                    peer_id: peer,
+                    connection_id: conn_id,
+                    endpoint: &connected,
+                    failed_addresses: &[],
+                    other_established: 0usize,
+                },
+            ));
+        }
+
+        if let Some(PeerState::Disabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Closed));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        // open substreams on both active connections
+        notif.peerset_report_connect(peer, set_id);
+
+        if let Some(PeerState::Enabled { connections, .. }) = notif.peers.get(&(peer, set_id)) {
+            assert_eq!(connections[0], (conn1, ConnectionState::Opening));
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.on_connection_handler_event(
+            peer,
+            conn1,
+            conn_yielder.open_substream(peer, 0, connected.clone(), vec![1, 2, 3, 4]),
+        );
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert!(std::matches!(connections[0].1, ConnectionState::Open(_)));
+            assert_eq!(connections[0].0, conn1);
+            assert_eq!(connections[1], (conn2, ConnectionState::Closed));
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    fn inject_dial_failure_for_pending_request() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+
+        // attempt to connect the backed-off peer and verify that the request is pending
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::PendingRequest { .. })
+        ));
+
+        let now = Instant::now();
+        notif.on_swarm_event(FromSwarm::DialFailure(
+            libp2p::swarm::behaviour::DialFailure {
+                peer_id: Some(peer),
+                error: &libp2p::swarm::DialError::Banned,
+                connection_id: ConnectionId::new_unchecked(0),
+            },
+        ));
+
+        if let Some(PeerState::PendingRequest {
+            ref timer_deadline, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert!(timer_deadline > &(now + std::time::Duration::from_secs(5)));
+        }
+    }
+
+    #[test]
+    fn peerstate_incoming_open_desired_by_remote() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn1 = ConnectionId::new_unchecked(0);
+        let conn2 = ConnectionId::new_unchecked(1);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn1,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn2,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn1,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        // add another open event from remote
+        notif.on_connection_handler_event(
+            peer,
+            conn2,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+
+        if let Some(PeerState::Incoming {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert_eq!(
+                connections[0],
+                (conn1, ConnectionState::OpenDesiredByRemote)
+            );
+            assert_eq!(
+                connections[1],
+                (conn2, ConnectionState::OpenDesiredByRemote)
+            );
+        }
+    }
+
+    #[tokio::test]
+    async fn remove_backoff_peer_after_timeout() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+
+        if let Some(&mut PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, 0.into()))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_millis(100))
+                    .unwrap(),
+            );
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        let until = if let Some(&PeerState::Backoff { timer_deadline, .. }) =
+            notif.peers.get(&(peer, set_id))
+        {
+            timer_deadline
+        } else {
+            panic!("invalid state");
+        };
+
+        if until > Instant::now() {
+            std::thread::sleep(until - Instant::now());
+        }
+
+        assert!(notif.peers.get(&(peer, set_id)).is_some());
+
+        let subroutine = async {
+            let mut params = MockPollParams {
+                peer_id: PeerId::random(),
+                addr: Multiaddr::empty(),
+            };
+
+            loop {
+                futures::future::poll_fn(|cx| {
+                    let _ = notif.poll(cx, &mut params);
+                    Poll::Ready(())
+                })
+                .await;
+
+                if notif.peers.get(&(peer, set_id)).is_none() {
+                    break;
+                }
+            }
+        };
+
+        if tokio::time::timeout(Duration::from_secs(5), subroutine)
+            .await
+            .is_err()
+        {
+            panic!("backoff peer was not removed in time");
+        }
+
+        assert!(notif.peers.get(&(peer, set_id)).is_none());
+    }
+
+    #[tokio::test]
+    async fn reschedule_disabled_pending_enable_when_connection_not_closed() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        // move the peer to `Enabled` state
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &ConnectedPoint::Listener {
+                    local_addr: Multiaddr::empty(),
+                    send_back_addr: Multiaddr::empty(),
+                },
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // open substream
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        // we rely on the implementation detail that incoming indices are counted from 0
+        // to not mock the `Peerset`
+        notif.protocol_report_accept(IncomingIndex(0));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        let event = conn_yielder.open_substream(peer, 0, connected, vec![1, 2, 3, 4]);
+
+        notif.on_connection_handler_event(peer, conn, event);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert!(std::matches!(connections[0], (_, ConnectionState::Open(_))));
+            assert_eq!(connections[0].0, conn);
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.peerset_report_disconnect(peer, set_id);
+
+        if let Some(PeerState::Disabled {
+            ref connections,
+            ref mut backoff_until,
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            assert!(std::matches!(connections[0], (_, ConnectionState::Closing)));
+            assert_eq!(connections[0].0, conn);
+
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(2))
+                    .unwrap(),
+            );
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.peerset_report_connect(peer, set_id);
+
+        let prev_instant = if let Some(PeerState::DisabledPendingEnable {
+            ref connections,
+            timer_deadline,
+            ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert!(std::matches!(connections[0], (_, ConnectionState::Closing)));
+            assert_eq!(connections[0].0, conn);
+
+            *timer_deadline
+        } else {
+            panic!("invalid state");
+        };
+
+        let subroutine = async {
+            let mut params = MockPollParams {
+                peer_id: PeerId::random(),
+                addr: Multiaddr::empty(),
+            };
+
+            loop {
+                futures::future::poll_fn(|cx| {
+                    let _ = notif.poll(cx, &mut params);
+                    Poll::Ready(())
+                })
+                .await;
+
+                if let Some(PeerState::DisabledPendingEnable {
+                    timer_deadline,
+                    connections,
+                    ..
+                }) = notif.peers.get(&(peer, set_id))
+                {
+                    assert!(std::matches!(connections[0], (_, ConnectionState::Closing)));
+
+                    if timer_deadline != &prev_instant {
+                        break;
+                    }
+                } else {
+                    panic!("invalid state");
+                }
+            }
+        };
+
+        // one of the peers has an active backoff timer so poll the notifications code until
+        // the timer has expired. Because the connection is still in the state of `Closing`,
+        // verify that the code continues to keep the peer disabled by resetting the timer
+        // after the first one expired.
+        if tokio::time::timeout(Duration::from_secs(5), subroutine)
+            .await
+            .is_err()
+        {
+            panic!("backoff peer was not removed in time");
+        }
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn peerset_report_connect_with_enabled_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        // move the peer to `Enabled` state
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        let event = conn_yielder.open_substream(peer, 0, connected, vec![1, 2, 3, 4]);
+
+        notif.on_connection_handler_event(peer, conn, event);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        if let Some(PeerState::Enabled {
+            ref connections, ..
+        }) = notif.peers.get(&(peer, set_id))
+        {
+            assert!(std::matches!(connections[0], (_, ConnectionState::Open(_))));
+            assert_eq!(connections[0].0, conn);
+        } else {
+            panic!("invalid state");
+        }
+
+        notif.peerset_report_connect(peer, set_id);
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_connect_with_disabled_pending_enable_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        // switch state to `DisabledPendingEnable`
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+
+        // duplicate "connect" must not change the state
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_connect_with_requested_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+
+        // Set peer into `Requested` state.
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Requested)
+        ));
+
+        // Duplicate "connect" must not change the state.
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Requested)
+        ));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_connect_with_pending_requested() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+
+        // attempt to connect the backed-off peer and verify that the request is pending
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::PendingRequest { .. })
+        ));
+
+        // duplicate "connect" must not change the state
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::PendingRequest { .. })
+        ));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_connect_with_incoming_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_disconnect_with_incoming_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.peerset_report_disconnect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peerset_report_disconnect_with_incoming_peer_protocol_accepts() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        // `Peerset` wants to disconnect the peer but since it's still under validation,
+        // it won't be disabled automatically
+        notif.peerset_report_disconnect(peer, set_id);
+
+        let incoming_index = match notif.peers.get(&(peer, set_id)) {
+            Some(&PeerState::Incoming {
+                peerset_rejected,
+                incoming_index,
+                ..
+            }) => {
+                assert!(peerset_rejected);
+                incoming_index
+            }
+            state => panic!("invalid state: {state:?}"),
+        };
+
+        // protocol accepted peer but since `Peerset` wanted to disconnect it, the peer will be
+        // disabled
+        notif.protocol_report_accept(incoming_index);
+
+        match notif.peers.get(&(peer, set_id)) {
+            Some(&PeerState::Disabled { .. }) => {}
+            state => panic!("invalid state: {state:?}"),
+        };
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peer_disconnected_protocol_accepts() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        assert!(notif
+            .incoming
+            .iter()
+            .any(|entry| entry.incoming_id == IncomingIndex(0)));
+        notif.disconnect_peer(&peer, set_id);
+
+        // since the connection was closed, nothing happens for the peer state because
+        // there is nothing actionable
+        notif.protocol_report_accept(IncomingIndex(0));
+
+        match notif.peers.get(&(peer, set_id)) {
+            Some(&PeerState::Disabled { .. }) => {}
+            state => panic!("invalid state: {state:?}"),
+        };
+
+        assert!(!notif
+            .incoming
+            .iter()
+            .any(|entry| entry.incoming_id == IncomingIndex(0)));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn connection_closed_protocol_accepts() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(0),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        // connection closed, nothing to do
+        notif.protocol_report_accept(IncomingIndex(0));
+
+        match notif.peers.get(&(peer, set_id)) {
+            None => {}
+            state => panic!("invalid state: {state:?}"),
+        };
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn peer_disconnected_protocol_reject() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        assert!(notif
+            .incoming
+            .iter()
+            .any(|entry| entry.incoming_id == IncomingIndex(0)));
+        notif.disconnect_peer(&peer, set_id);
+
+        // since the connection was closed, nothing happens for the peer state because
+        // there is nothing actionable
+        notif.protocol_report_reject(IncomingIndex(0));
+
+        match notif.peers.get(&(peer, set_id)) {
+            Some(&PeerState::Disabled { .. }) => {}
+            state => panic!("invalid state: {state:?}"),
+        };
+
+        assert!(!notif
+            .incoming
+            .iter()
+            .any(|entry| entry.incoming_id == IncomingIndex(0)));
+    }
+
+    #[test]
+    #[cfg(debug_assertions)]
+    fn connection_closed_protocol_rejects() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(0),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+
+        // connection closed, nothing to do
+        notif.protocol_report_reject(IncomingIndex(0));
+
+        match notif.peers.get(&(peer, set_id)) {
+            None => {}
+            state => panic!("invalid state: {state:?}"),
+        };
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn protocol_report_accept_not_incoming_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        assert!(std::matches!(
+            notif.incoming[0],
+            IncomingPeer {
+                alive: true,
+                incoming_id: IncomingIndex(0),
+                ..
+            },
+        ));
+
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        let event = conn_yielder.open_substream(peer, 0, connected, vec![1, 2, 3, 4]);
+        notif.on_connection_handler_event(peer, conn, event);
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+        notif.incoming[0].alive = true;
+        notif.protocol_report_accept(IncomingIndex(0));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_connection_closed_non_existent_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let endpoint = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(0),
+                endpoint: &endpoint.clone(),
+                handler: NotifsHandler::new(peer, endpoint, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    fn disconnect_non_existent_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let set_id = SetId::from(0);
+
+        notif.peerset_report_disconnect(peer, set_id);
+
+        assert!(notif.peers.is_empty());
+        assert!(notif.incoming.is_empty());
+    }
+
+    #[test]
+    fn accept_non_existent_connection() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        notif.protocol_report_accept(0.into());
+
+        assert!(notif.peers.is_empty());
+        assert!(notif.incoming.is_empty());
+    }
+
+    #[test]
+    fn reject_non_existent_connection() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+
+        notif.protocol_report_reject(0.into());
+
+        assert!(notif.peers.is_empty());
+        assert!(notif.incoming.is_empty());
+    }
+
+    #[test]
+    fn reject_non_active_connection() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.incoming[0].alive = false;
+        notif.protocol_report_reject(0.into());
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_non_existent_connection_closed_for_incoming_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(1337),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_non_existent_connection_closed_for_disabled_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(1337),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_non_existent_connection_closed_for_disabled_pending_enable() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        // switch state to `DisabledPendingEnable`
+        notif.peerset_report_connect(peer, set_id);
+
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::DisabledPendingEnable { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(1337),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_connection_closed_for_incoming_peer_state_mismatch() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+        notif.incoming[0].alive = false;
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_connection_closed_for_enabled_state_mismatch() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let set_id = SetId::from(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // remote opens a substream, verify that peer state is updated to `Incoming`
+        notif.on_connection_handler_event(
+            peer,
+            conn,
+            NotifsHandlerOut::OpenDesiredByRemote {
+                protocol_index: 0,
+                handshake: vec![1, 3, 3, 7],
+            },
+        );
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Incoming { .. })
+        ));
+
+        // attempt to connect to the peer and verify that the peer state is `Enabled`
+        notif.peerset_report_connect(peer, set_id);
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Enabled { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: ConnectionId::new_unchecked(1337),
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn inject_connection_closed_for_backoff_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let set_id = SetId::from(0);
+        let peer = PeerId::random();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+
+        notif.on_swarm_event(FromSwarm::ConnectionEstablished(
+            libp2p::swarm::behaviour::ConnectionEstablished {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected,
+                failed_addresses: &[],
+                other_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Disabled { .. })
+        ));
+
+        // manually add backoff for the entry
+        if let Some(PeerState::Disabled {
+            ref mut backoff_until,
+            ..
+        }) = notif.peers.get_mut(&(peer, set_id))
+        {
+            *backoff_until = Some(
+                Instant::now()
+                    .checked_add(std::time::Duration::from_secs(5))
+                    .unwrap(),
+            );
+        }
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected.clone(), vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+        assert!(std::matches!(
+            notif.peers.get(&(peer, set_id)),
+            Some(&PeerState::Backoff { .. })
+        ));
+
+        notif.on_swarm_event(FromSwarm::ConnectionClosed(
+            libp2p::swarm::behaviour::ConnectionClosed {
+                peer_id: peer,
+                connection_id: conn,
+                endpoint: &connected.clone(),
+                handler: NotifsHandler::new(peer, connected, vec![], None),
+                remaining_established: 0usize,
+            },
+        ));
+    }
+
+    #[test]
+    #[should_panic]
+    #[cfg(debug_assertions)]
+    fn open_result_ok_non_existent_peer() {
+        let (mut notif, _controller, _notif_service) = development_notifs();
+        let conn = ConnectionId::new_unchecked(0);
+        let connected = ConnectedPoint::Listener {
+            local_addr: Multiaddr::empty(),
+            send_back_addr: Multiaddr::empty(),
+        };
+        let mut conn_yielder = ConnectionYielder::new();
+
+        notif.on_connection_handler_event(
+            PeerId::random(),
+            conn,
+            conn_yielder.open_substream(PeerId::random(), 0, connected, vec![1, 2, 3, 4]),
+        );
+    }
+}
diff --git a/libp2p/src/protocol/notifications/handler.rs b/libp2p/src/protocol/notifications/handler.rs
new file mode 100644
index 000000000..2804ee1d6
--- /dev/null
+++ b/libp2p/src/protocol/notifications/handler.rs
@@ -0,0 +1,1915 @@
+// 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/>.
+
+//! Implementations of the `IntoConnectionHandler` and `ConnectionHandler` traits for both incoming
+//! and outgoing substreams for all gossiping protocols.
+//!
+//! This is the main implementation of `ConnectionHandler` in this crate, that handles all the
+//! gossiping protocols that are Substrate-related and outside of the scope of libp2p.
+//!
+//! # Usage
+//!
+//! From an API perspective, for each of its protocols, the [`NotifsHandler`] is always in one of
+//! the following state (see [`State`]):
+//!
+//! - Closed substream. This is the initial state.
+//! - Closed substream, but remote desires them to be open.
+//! - Open substream.
+//! - Open substream, but remote desires them to be closed.
+//!
+//! Each protocol in the [`NotifsHandler`] can spontaneously switch between these states:
+//!
+//! - "Closed substream" to "Closed substream but open desired". When that happens, a
+//! [`NotifsHandlerOut::OpenDesiredByRemote`] is emitted.
+//! - "Closed substream but open desired" to "Closed substream" (i.e. the remote has cancelled
+//! their request). When that happens, a [`NotifsHandlerOut::CloseDesired`] is emitted.
+//! - "Open substream" to "Open substream but close desired". When that happens, a
+//! [`NotifsHandlerOut::CloseDesired`] is emitted.
+//!
+//! The user can instruct a protocol in the `NotifsHandler` to switch from "closed" to "open" or
+//! vice-versa by sending either a [`NotifsHandlerIn::Open`] or a [`NotifsHandlerIn::Close`]. The
+//! `NotifsHandler` must answer with [`NotifsHandlerOut::OpenResultOk`] or
+//! [`NotifsHandlerOut::OpenResultErr`], or with [`NotifsHandlerOut::CloseResult`].
+//!
+//! When a [`NotifsHandlerOut::OpenResultOk`] is emitted, the substream is now in the open state.
+//! When a [`NotifsHandlerOut::OpenResultErr`] or [`NotifsHandlerOut::CloseResult`] is emitted,
+//! the `NotifsHandler` is now (or remains) in the closed state.
+//!
+//! When a [`NotifsHandlerOut::OpenDesiredByRemote`] is emitted, the user should always send back
+//! either a [`NotifsHandlerIn::Open`] or a [`NotifsHandlerIn::Close`].If this isn't done, the
+//! remote will be left in a pending state.
+//!
+//! It is illegal to send a [`NotifsHandlerIn::Open`] before a previously-emitted
+//! [`NotifsHandlerIn::Open`] has gotten an answer.
+
+use crate::{
+    protocol::notifications::{
+        service::metrics,
+        upgrade::{
+            NotificationsIn, NotificationsInSubstream, NotificationsOut, NotificationsOutSubstream,
+            UpgradeCollec,
+        },
+    },
+    types::ProtocolName,
+};
+
+use bytes::BytesMut;
+use futures::{
+    channel::mpsc,
+    lock::{Mutex as FuturesMutex, MutexGuard as FuturesMutexGuard},
+    prelude::*,
+};
+use libp2p::{
+    core::ConnectedPoint,
+    swarm::{
+        handler::ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, KeepAlive,
+        NegotiatedSubstream, SubstreamProtocol,
+    },
+    PeerId,
+};
+use log::error;
+use parking_lot::{Mutex, RwLock};
+use std::{
+    collections::VecDeque,
+    mem,
+    pin::Pin,
+    sync::Arc,
+    task::{Context, Poll},
+    time::{Duration, Instant},
+};
+
+/// Number of pending notifications in asynchronous contexts.
+/// See [`NotificationsSink::reserve_notification`] for context.
+pub(crate) const ASYNC_NOTIFICATIONS_BUFFER_SIZE: usize = 8;
+
+/// Number of pending notifications in synchronous contexts.
+const SYNC_NOTIFICATIONS_BUFFER_SIZE: usize = 2048;
+
+/// Maximum duration to open a substream and receive the handshake message. After that, we
+/// consider that we failed to open the substream.
+const OPEN_TIMEOUT: Duration = Duration::from_secs(10);
+
+/// After successfully establishing a connection with the remote, we keep the connection open for
+/// at least this amount of time in order to give the rest of the code the chance to notify us to
+/// open substreams.
+const INITIAL_KEEPALIVE_TIME: Duration = Duration::from_secs(5);
+
+/// The actual handler once the connection has been established.
+///
+/// See the documentation at the module level for more information.
+pub struct NotifsHandler {
+    /// List of notification protocols, specified by the user at initialization.
+    protocols: Vec<Protocol>,
+
+    /// When the connection with the remote has been successfully established.
+    when_connection_open: Instant,
+
+    /// Whether we are the connection dialer or listener.
+    endpoint: ConnectedPoint,
+
+    /// Remote we are connected to.
+    peer_id: PeerId,
+
+    /// Events to return in priority from `poll`.
+    events_queue: VecDeque<
+        ConnectionHandlerEvent<NotificationsOut, usize, NotifsHandlerOut, NotifsHandlerError>,
+    >,
+
+    /// Metrics.
+    metrics: Option<Arc<metrics::Metrics>>,
+}
+
+impl NotifsHandler {
+    /// Creates new [`NotifsHandler`].
+    pub fn new(
+        peer_id: PeerId,
+        endpoint: ConnectedPoint,
+        protocols: Vec<ProtocolConfig>,
+        metrics: Option<metrics::Metrics>,
+    ) -> Self {
+        Self {
+            protocols: protocols
+                .into_iter()
+                .map(|config| {
+                    let in_upgrade = NotificationsIn::new(
+                        config.name.clone(),
+                        config.fallback_names.clone(),
+                        config.max_notification_size,
+                    );
+
+                    Protocol {
+                        config,
+                        in_upgrade,
+                        state: State::Closed {
+                            pending_opening: false,
+                        },
+                    }
+                })
+                .collect(),
+            peer_id,
+            endpoint,
+            when_connection_open: Instant::now(),
+            events_queue: VecDeque::with_capacity(16),
+            metrics: metrics.map(Arc::new),
+        }
+    }
+}
+
+/// Configuration for a notifications protocol.
+#[derive(Debug, Clone)]
+pub struct ProtocolConfig {
+    /// Name of the protocol.
+    pub name: ProtocolName,
+    /// Names of the protocol to use if the main one isn't available.
+    pub fallback_names: Vec<ProtocolName>,
+    /// Handshake of the protocol. The `RwLock` is locked every time a new substream is opened.
+    pub handshake: Arc<RwLock<Vec<u8>>>,
+    /// Maximum allowed size for a notification.
+    pub max_notification_size: u64,
+}
+
+/// Fields specific for each individual protocol.
+struct Protocol {
+    /// Other fields.
+    config: ProtocolConfig,
+
+    /// Prototype for the inbound upgrade.
+    in_upgrade: NotificationsIn,
+
+    /// Current state of the substreams for this protocol.
+    state: State,
+}
+
+/// See the module-level documentation to learn about the meaning of these variants.
+enum State {
+    /// Protocol is in the "Closed" state.
+    Closed {
+        /// True if an outgoing substream is still in the process of being opened.
+        pending_opening: bool,
+    },
+
+    /// Protocol is in the "Closed" state. A [`NotifsHandlerOut::OpenDesiredByRemote`] has been
+    /// emitted.
+    OpenDesiredByRemote {
+        /// Substream opened by the remote and that hasn't been accepted/rejected yet.
+        in_substream: NotificationsInSubstream<NegotiatedSubstream>,
+
+        /// See [`State::Closed::pending_opening`].
+        pending_opening: bool,
+    },
+
+    /// Protocol is in the "Closed" state, but has received a [`NotifsHandlerIn::Open`] and is
+    /// consequently trying to open the various notifications substreams.
+    ///
+    /// A [`NotifsHandlerOut::OpenResultOk`] or a [`NotifsHandlerOut::OpenResultErr`] event must
+    /// be emitted when transitionning to respectively [`State::Open`] or [`State::Closed`].
+    Opening {
+        /// Substream opened by the remote. If `Some`, has been accepted.
+        in_substream: Option<NotificationsInSubstream<NegotiatedSubstream>>,
+        /// Is the connection inbound.
+        inbound: bool,
+    },
+
+    /// Protocol is in the "Open" state.
+    Open {
+        /// Contains the two `Receiver`s connected to the [`NotificationsSink`] that has been
+        /// sent out. The notifications to send out can be pulled from this receivers.
+        /// We use two different channels in order to have two different channel sizes, but from
+        /// the receiving point of view, the two channels are the same.
+        /// The receivers are fused in case the user drops the [`NotificationsSink`] entirely.
+        notifications_sink_rx: stream::Peekable<NotificationsSinkRx>,
+
+        /// Outbound substream that has been accepted by the remote.
+        ///
+        /// Always `Some` on transition to [`State::Open`]. Switched to `None` only if the remote
+        /// closed the substream. If `None`, a [`NotifsHandlerOut::CloseDesired`] event has been
+        /// emitted.
+        out_substream: Option<NotificationsOutSubstream<NegotiatedSubstream>>,
+
+        /// Substream opened by the remote.
+        ///
+        /// Contrary to the `out_substream` field, operations continue as normal even if the
+        /// substream has been closed by the remote. A `None` is treated the same way as if there
+        /// was an idle substream.
+        in_substream: Option<NotificationsInSubstream<NegotiatedSubstream>>,
+    },
+}
+
+pub type NotificationsSinkRx = stream::Select<
+    stream::Fuse<mpsc::Receiver<NotificationsSinkMessage>>,
+    stream::Fuse<mpsc::Receiver<NotificationsSinkMessage>>,
+>;
+
+/// Event that can be received by a `NotifsHandler`.
+#[derive(Debug, Clone)]
+pub enum NotifsHandlerIn {
+    /// Instruct the handler to open the notification substreams.
+    ///
+    /// Must always be answered by a [`NotifsHandlerOut::OpenResultOk`] or a
+    /// [`NotifsHandlerOut::OpenResultErr`] event.
+    ///
+    /// Importantly, it is forbidden to send a [`NotifsHandlerIn::Open`] while a previous one is
+    /// already in the fly. It is however possible if a `Close` is still in the fly.
+    Open {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+    },
+
+    /// Instruct the handler to close the notification substreams, or reject any pending incoming
+    /// substream request.
+    ///
+    /// Must always be answered by a [`NotifsHandlerOut::CloseResult`] event.
+    Close {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+    },
+}
+
+/// Event that can be emitted by a `NotifsHandler`.
+#[derive(Debug)]
+pub enum NotifsHandlerOut {
+    /// Acknowledges a [`NotifsHandlerIn::Open`].
+    OpenResultOk {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+        /// Name of the protocol that was actually negotiated, if the default one wasn't available.
+        negotiated_fallback: Option<ProtocolName>,
+        /// The endpoint of the connection that is open for custom protocols.
+        endpoint: ConnectedPoint,
+        /// Handshake that was sent to us.
+        /// This is normally a "Status" message, but this out of the concern of this code.
+        received_handshake: Vec<u8>,
+        /// How notifications can be sent to this node.
+        notifications_sink: NotificationsSink,
+        /// Is the connection inbound.
+        inbound: bool,
+    },
+
+    /// Acknowledges a [`NotifsHandlerIn::Open`]. The remote has refused the attempt to open
+    /// notification substreams.
+    OpenResultErr {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+    },
+
+    /// Acknowledges a [`NotifsHandlerIn::Close`].
+    CloseResult {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+    },
+
+    /// The remote would like the substreams to be open. Send a [`NotifsHandlerIn::Open`] or a
+    /// [`NotifsHandlerIn::Close`] in order to either accept or deny this request. If a
+    /// [`NotifsHandlerIn::Open`] or [`NotifsHandlerIn::Close`] has been sent before and has not
+    /// yet been acknowledged by a matching [`NotifsHandlerOut`], then you don't need to a send
+    /// another [`NotifsHandlerIn`].
+    OpenDesiredByRemote {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+        /// Received handshake.
+        handshake: Vec<u8>,
+    },
+
+    /// The remote would like the substreams to be closed. Send a [`NotifsHandlerIn::Close`] in
+    /// order to close them. If a [`NotifsHandlerIn::Close`] has been sent before and has not yet
+    /// been acknowledged by a [`NotifsHandlerOut::CloseResult`], then you don't need to a send
+    /// another one.
+    CloseDesired {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+    },
+
+    /// Received a message on a custom protocol substream.
+    ///
+    /// Can only happen when the handler is in the open state.
+    Notification {
+        /// Index of the protocol in the list of protocols passed at initialization.
+        protocol_index: usize,
+        /// Message that has been received.
+        message: BytesMut,
+    },
+}
+
+/// Sink connected directly to the node background task. Allows sending notifications to the peer.
+///
+/// Can be cloned in order to obtain multiple references to the substream of the same peer.
+#[derive(Debug, Clone)]
+pub struct NotificationsSink {
+    inner: Arc<NotificationsSinkInner>,
+    metrics: Option<Arc<metrics::Metrics>>,
+}
+
+impl NotificationsSink {
+    /// Create new [`NotificationsSink`].
+    /// NOTE: only used for testing but must be `pub` as other crates in `client/network` use this.
+    pub fn new(
+        peer_id: PeerId,
+    ) -> (
+        Self,
+        mpsc::Receiver<NotificationsSinkMessage>,
+        mpsc::Receiver<NotificationsSinkMessage>,
+    ) {
+        let (async_tx, async_rx) = mpsc::channel(ASYNC_NOTIFICATIONS_BUFFER_SIZE);
+        let (sync_tx, sync_rx) = mpsc::channel(SYNC_NOTIFICATIONS_BUFFER_SIZE);
+        (
+            NotificationsSink {
+                inner: Arc::new(NotificationsSinkInner {
+                    peer_id,
+                    async_channel: FuturesMutex::new(async_tx),
+                    sync_channel: Mutex::new(Some(sync_tx)),
+                }),
+                metrics: None,
+            },
+            async_rx,
+            sync_rx,
+        )
+    }
+
+    /// Get reference to metrics.
+    pub fn metrics(&self) -> &Option<Arc<metrics::Metrics>> {
+        &self.metrics
+    }
+}
+
+#[derive(Debug)]
+struct NotificationsSinkInner {
+    /// Target of the sink.
+    peer_id: PeerId,
+    /// Sender to use in asynchronous contexts. Uses an asynchronous mutex.
+    async_channel: FuturesMutex<mpsc::Sender<NotificationsSinkMessage>>,
+    /// Sender to use in synchronous contexts. Uses a synchronous mutex.
+    /// Contains `None` if the channel was full at some point, in which case the channel will
+    /// be closed in the near future anyway.
+    /// This channel has a large capacity and is meant to be used in contexts where
+    /// back-pressure cannot be properly exerted.
+    /// It will be removed in a future version.
+    sync_channel: Mutex<Option<mpsc::Sender<NotificationsSinkMessage>>>,
+}
+
+/// Message emitted through the [`NotificationsSink`] and processed by the background task
+/// dedicated to the peer.
+#[derive(Debug, PartialEq, Eq)]
+pub enum NotificationsSinkMessage {
+    /// Message emitted by [`NotificationsSink::reserve_notification`] and
+    /// [`NotificationsSink::write_notification_now`].
+    Notification { message: Vec<u8> },
+
+    /// Must close the connection.
+    ForceClose,
+}
+
+impl NotificationsSink {
+    /// Returns the [`PeerId`] the sink is connected to.
+    pub fn peer_id(&self) -> &PeerId {
+        &self.inner.peer_id
+    }
+
+    /// Sends a notification to the peer.
+    ///
+    /// If too many messages are already buffered, the notification is silently discarded and the
+    /// connection to the peer will be closed shortly after.
+    ///
+    /// The protocol name is expected to be checked ahead of calling this method. It is a logic
+    /// error to send a notification using an unknown protocol.
+    ///
+    /// This method will be removed in a future version.
+    pub fn send_sync_notification(&self, message: impl Into<Vec<u8>>) {
+        let mut lock = self.inner.sync_channel.lock();
+
+        if let Some(tx) = lock.as_mut() {
+            let message = message.into();
+            let result = tx.try_send(NotificationsSinkMessage::Notification { message });
+
+            if result.is_err() {
+                // Cloning the `mpsc::Sender` guarantees the allocation of an extra spot in the
+                // buffer, and therefore `try_send` will succeed.
+                let _result2 = tx.clone().try_send(NotificationsSinkMessage::ForceClose);
+                debug_assert!(_result2
+                    .map(|()| true)
+                    .unwrap_or_else(|err| err.is_disconnected()));
+
+                // Destroy the sender in order to not send more `ForceClose` messages.
+                *lock = None;
+            }
+        }
+    }
+
+    /// Wait until the remote is ready to accept a notification.
+    ///
+    /// Returns an error in the case where the connection is closed.
+    ///
+    /// The protocol name is expected to be checked ahead of calling this method. It is a logic
+    /// error to send a notification using an unknown protocol.
+    pub async fn reserve_notification(&self) -> Result<Ready<'_>, ()> {
+        let mut lock = self.inner.async_channel.lock().await;
+
+        let poll_ready = future::poll_fn(|cx| lock.poll_ready(cx)).await;
+        if poll_ready.is_ok() {
+            Ok(Ready { lock })
+        } else {
+            Err(())
+        }
+    }
+}
+
+/// Notification slot is reserved and the notification can actually be sent.
+#[must_use]
+#[derive(Debug)]
+pub struct Ready<'a> {
+    /// Guarded channel. The channel inside is guaranteed to not be full.
+    lock: FuturesMutexGuard<'a, mpsc::Sender<NotificationsSinkMessage>>,
+}
+
+impl<'a> Ready<'a> {
+    /// Consumes this slots reservation and actually queues the notification.
+    ///
+    /// Returns an error if the substream has been closed.
+    #[allow(clippy::result_unit_err)]
+    pub fn send(mut self, notification: impl Into<Vec<u8>>) -> Result<(), ()> {
+        self.lock
+            .start_send(NotificationsSinkMessage::Notification {
+                message: notification.into(),
+            })
+            .map_err(|_| ())
+    }
+}
+
+/// Error specific to the collection of protocols.
+#[derive(Debug, thiserror::Error)]
+pub enum NotifsHandlerError {
+    #[error("Channel of synchronous notifications is full.")]
+    SyncNotificationsClogged,
+}
+
+impl ConnectionHandler for NotifsHandler {
+    type InEvent = NotifsHandlerIn;
+    type OutEvent = NotifsHandlerOut;
+    type Error = NotifsHandlerError;
+    type InboundProtocol = UpgradeCollec<NotificationsIn>;
+    type OutboundProtocol = NotificationsOut;
+    // Index within the `out_protocols`.
+    type OutboundOpenInfo = usize;
+    type InboundOpenInfo = ();
+
+    fn listen_protocol(&self) -> SubstreamProtocol<Self::InboundProtocol, ()> {
+        let protocols = self
+            .protocols
+            .iter()
+            .map(|p| p.in_upgrade.clone())
+            .collect::<UpgradeCollec<_>>();
+
+        SubstreamProtocol::new(protocols, ())
+    }
+
+    fn on_connection_event(
+        &mut self,
+        event: ConnectionEvent<
+            '_,
+            Self::InboundProtocol,
+            Self::OutboundProtocol,
+            Self::InboundOpenInfo,
+            Self::OutboundOpenInfo,
+        >,
+    ) {
+        match event {
+            ConnectionEvent::FullyNegotiatedInbound(inbound) => {
+                let (mut in_substream_open, protocol_index) = inbound.protocol;
+                let protocol_info = &mut self.protocols[protocol_index];
+
+                match protocol_info.state {
+                    State::Closed { pending_opening } => {
+                        self.events_queue.push_back(ConnectionHandlerEvent::Custom(
+                            NotifsHandlerOut::OpenDesiredByRemote {
+                                protocol_index,
+                                handshake: in_substream_open.handshake,
+                            },
+                        ));
+
+                        protocol_info.state = State::OpenDesiredByRemote {
+                            in_substream: in_substream_open.substream,
+                            pending_opening,
+                        };
+                    }
+                    State::OpenDesiredByRemote { .. } => {
+                        // If a substream already exists, silently drop the new one.
+                        // Note that we drop the substream, which will send an equivalent to a
+                        // TCP "RST" to the remote and force-close the substream. It might
+                        // seem like an unclean way to get rid of a substream. However, keep
+                        // in mind that it is invalid for the remote to open multiple such
+                        // substreams, and therefore sending a "RST" is the most correct thing
+                        // to do.
+                    }
+                    State::Opening {
+                        ref mut in_substream,
+                        ..
+                    }
+                    | State::Open {
+                        ref mut in_substream,
+                        ..
+                    } => {
+                        if in_substream.is_some() {
+                            // Same remark as above.
+                            return;
+                        }
+
+                        // Create `handshake_message` on a separate line to be sure that the
+                        // lock is released as soon as possible.
+                        let handshake_message = protocol_info.config.handshake.read().clone();
+                        in_substream_open
+                            .substream
+                            .send_handshake(handshake_message);
+                        *in_substream = Some(in_substream_open.substream);
+                    }
+                }
+            }
+            ConnectionEvent::FullyNegotiatedOutbound(outbound) => {
+                let (new_open, protocol_index) = (outbound.protocol, outbound.info);
+
+                match self.protocols[protocol_index].state {
+                    State::Closed {
+                        ref mut pending_opening,
+                    }
+                    | State::OpenDesiredByRemote {
+                        ref mut pending_opening,
+                        ..
+                    } => {
+                        debug_assert!(*pending_opening);
+                        *pending_opening = false;
+                    }
+                    State::Open { .. } => {
+                        error!(target: "sub-libp2p", "☎️ State mismatch in notifications handler");
+                        debug_assert!(false);
+                    }
+                    State::Opening {
+                        ref mut in_substream,
+                        inbound,
+                    } => {
+                        let (async_tx, async_rx) = mpsc::channel(ASYNC_NOTIFICATIONS_BUFFER_SIZE);
+                        let (sync_tx, sync_rx) = mpsc::channel(SYNC_NOTIFICATIONS_BUFFER_SIZE);
+                        let notifications_sink = NotificationsSink {
+                            inner: Arc::new(NotificationsSinkInner {
+                                peer_id: self.peer_id,
+                                async_channel: FuturesMutex::new(async_tx),
+                                sync_channel: Mutex::new(Some(sync_tx)),
+                            }),
+                            metrics: self.metrics.clone(),
+                        };
+
+                        self.protocols[protocol_index].state = State::Open {
+                            notifications_sink_rx: stream::select(async_rx.fuse(), sync_rx.fuse())
+                                .peekable(),
+                            out_substream: Some(new_open.substream),
+                            in_substream: in_substream.take(),
+                        };
+
+                        self.events_queue.push_back(ConnectionHandlerEvent::Custom(
+                            NotifsHandlerOut::OpenResultOk {
+                                protocol_index,
+                                negotiated_fallback: new_open.negotiated_fallback,
+                                endpoint: self.endpoint.clone(),
+                                received_handshake: new_open.handshake,
+                                notifications_sink,
+                                inbound,
+                            },
+                        ));
+                    }
+                }
+            }
+            ConnectionEvent::AddressChange(_address_change) => {}
+            ConnectionEvent::DialUpgradeError(dial_upgrade_error) => {
+                match self.protocols[dial_upgrade_error.info].state {
+                    State::Closed {
+                        ref mut pending_opening,
+                    }
+                    | State::OpenDesiredByRemote {
+                        ref mut pending_opening,
+                        ..
+                    } => {
+                        debug_assert!(*pending_opening);
+                        *pending_opening = false;
+                    }
+
+                    State::Opening { .. } => {
+                        self.protocols[dial_upgrade_error.info].state = State::Closed {
+                            pending_opening: false,
+                        };
+
+                        self.events_queue.push_back(ConnectionHandlerEvent::Custom(
+                            NotifsHandlerOut::OpenResultErr {
+                                protocol_index: dial_upgrade_error.info,
+                            },
+                        ));
+                    }
+
+                    // No substream is being open when already `Open`.
+                    State::Open { .. } => debug_assert!(false),
+                }
+            }
+            ConnectionEvent::ListenUpgradeError(_listen_upgrade_error) => {}
+        }
+    }
+
+    fn on_behaviour_event(&mut self, message: NotifsHandlerIn) {
+        match message {
+            NotifsHandlerIn::Open { protocol_index } => {
+                let protocol_info = &mut self.protocols[protocol_index];
+                match &mut protocol_info.state {
+                    State::Closed { pending_opening } => {
+                        if !*pending_opening {
+                            let proto = NotificationsOut::new(
+                                protocol_info.config.name.clone(),
+                                protocol_info.config.fallback_names.clone(),
+                                protocol_info.config.handshake.read().clone(),
+                                protocol_info.config.max_notification_size,
+                            );
+
+                            self.events_queue.push_back(
+                                ConnectionHandlerEvent::OutboundSubstreamRequest {
+                                    protocol: SubstreamProtocol::new(proto, protocol_index)
+                                        .with_timeout(OPEN_TIMEOUT),
+                                },
+                            );
+                        }
+
+                        protocol_info.state = State::Opening {
+                            in_substream: None,
+                            inbound: false,
+                        };
+                    }
+                    State::OpenDesiredByRemote {
+                        pending_opening,
+                        in_substream,
+                    } => {
+                        let handshake_message = protocol_info.config.handshake.read().clone();
+
+                        if !*pending_opening {
+                            let proto = NotificationsOut::new(
+                                protocol_info.config.name.clone(),
+                                protocol_info.config.fallback_names.clone(),
+                                handshake_message.clone(),
+                                protocol_info.config.max_notification_size,
+                            );
+
+                            self.events_queue.push_back(
+                                ConnectionHandlerEvent::OutboundSubstreamRequest {
+                                    protocol: SubstreamProtocol::new(proto, protocol_index)
+                                        .with_timeout(OPEN_TIMEOUT),
+                                },
+                            );
+                        }
+
+                        in_substream.send_handshake(handshake_message);
+
+                        // The state change is done in two steps because of borrowing issues.
+                        let in_substream = match mem::replace(
+                            &mut protocol_info.state,
+                            State::Opening {
+                                in_substream: None,
+                                inbound: false,
+                            },
+                        ) {
+                            State::OpenDesiredByRemote { in_substream, .. } => in_substream,
+                            _ => unreachable!(),
+                        };
+                        protocol_info.state = State::Opening {
+                            in_substream: Some(in_substream),
+                            inbound: true,
+                        };
+                    }
+                    State::Opening { .. } | State::Open { .. } => {
+                        // As documented, it is forbidden to send an `Open` while there is already
+                        // one in the fly.
+                        error!(target: "sub-libp2p", "opening already-opened handler");
+                        debug_assert!(false);
+                    }
+                }
+            }
+
+            NotifsHandlerIn::Close { protocol_index } => {
+                match self.protocols[protocol_index].state {
+                    State::Open { .. } => {
+                        self.protocols[protocol_index].state = State::Closed {
+                            pending_opening: false,
+                        };
+                    }
+                    State::Opening { .. } => {
+                        self.protocols[protocol_index].state = State::Closed {
+                            pending_opening: true,
+                        };
+
+                        self.events_queue.push_back(ConnectionHandlerEvent::Custom(
+                            NotifsHandlerOut::OpenResultErr { protocol_index },
+                        ));
+                    }
+                    State::OpenDesiredByRemote {
+                        pending_opening, ..
+                    } => {
+                        self.protocols[protocol_index].state = State::Closed { pending_opening };
+                    }
+                    State::Closed { .. } => {}
+                }
+
+                self.events_queue.push_back(ConnectionHandlerEvent::Custom(
+                    NotifsHandlerOut::CloseResult { protocol_index },
+                ));
+            }
+        }
+    }
+
+    fn connection_keep_alive(&self) -> KeepAlive {
+        // `Yes` if any protocol has some activity.
+        if self
+            .protocols
+            .iter()
+            .any(|p| !matches!(p.state, State::Closed { .. }))
+        {
+            return KeepAlive::Yes;
+        }
+
+        // A grace period of `INITIAL_KEEPALIVE_TIME` must be given to leave time for the remote
+        // to express desire to open substreams.
+        KeepAlive::Until(self.when_connection_open + INITIAL_KEEPALIVE_TIME)
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+    ) -> Poll<
+        ConnectionHandlerEvent<
+            Self::OutboundProtocol,
+            Self::OutboundOpenInfo,
+            Self::OutEvent,
+            Self::Error,
+        >,
+    > {
+        if let Some(ev) = self.events_queue.pop_front() {
+            return Poll::Ready(ev);
+        }
+
+        // For each open substream, try send messages from `notifications_sink_rx` to the
+        // substream.
+        for protocol_index in 0..self.protocols.len() {
+            if let State::Open {
+                notifications_sink_rx,
+                out_substream: Some(out_substream),
+                ..
+            } = &mut self.protocols[protocol_index].state
+            {
+                loop {
+                    // Only proceed with `out_substream.poll_ready_unpin` if there is an element
+                    // available in `notifications_sink_rx`. This avoids waking up the task when
+                    // a substream is ready to send if there isn't actually something to send.
+                    match Pin::new(&mut *notifications_sink_rx).as_mut().poll_peek(cx) {
+                        Poll::Ready(Some(&NotificationsSinkMessage::ForceClose)) => {
+                            return Poll::Ready(ConnectionHandlerEvent::Close(
+                                NotifsHandlerError::SyncNotificationsClogged,
+                            ))
+                        }
+                        Poll::Ready(Some(&NotificationsSinkMessage::Notification { .. })) => {}
+                        Poll::Ready(None) | Poll::Pending => break,
+                    }
+
+                    // Before we extract the element from `notifications_sink_rx`, check that the
+                    // substream is ready to accept a message.
+                    match out_substream.poll_ready_unpin(cx) {
+                        Poll::Ready(_) => {}
+                        Poll::Pending => break,
+                    }
+
+                    // Now that the substream is ready for a message, grab what to send.
+                    let message = match notifications_sink_rx.poll_next_unpin(cx) {
+                        Poll::Ready(Some(NotificationsSinkMessage::Notification { message })) => {
+                            message
+                        }
+                        Poll::Ready(Some(NotificationsSinkMessage::ForceClose))
+                        | Poll::Ready(None)
+                        | Poll::Pending => {
+                            // Should never be reached, as per `poll_peek` above.
+                            debug_assert!(false);
+                            break;
+                        }
+                    };
+
+                    let _ = out_substream.start_send_unpin(message);
+                    // Note that flushing is performed later down this function.
+                }
+            }
+        }
+
+        // Flush all outbound substreams.
+        // When `poll` returns `Poll::Ready`, the libp2p `Swarm` may decide to no longer call
+        // `poll` again before it is ready to accept more events.
+        // In order to make sure that substreams are flushed as soon as possible, the flush is
+        // performed before the code paths that can produce `Ready` (with some rare exceptions).
+        // Importantly, however, the flush is performed *after* notifications are queued with
+        // `Sink::start_send`.
+        for protocol_index in 0..self.protocols.len() {
+            match &mut self.protocols[protocol_index].state {
+                State::Open {
+                    out_substream: out_substream @ Some(_),
+                    ..
+                } => {
+                    match Sink::poll_flush(Pin::new(out_substream.as_mut().unwrap()), cx) {
+                        Poll::Pending | Poll::Ready(Ok(())) => {}
+                        Poll::Ready(Err(_)) => {
+                            *out_substream = None;
+                            let event = NotifsHandlerOut::CloseDesired { protocol_index };
+                            return Poll::Ready(ConnectionHandlerEvent::Custom(event));
+                        }
+                    };
+                }
+
+                State::Closed { .. }
+                | State::Opening { .. }
+                | State::Open {
+                    out_substream: None,
+                    ..
+                }
+                | State::OpenDesiredByRemote { .. } => {}
+            }
+        }
+
+        // Poll inbound substreams.
+        for protocol_index in 0..self.protocols.len() {
+            // Inbound substreams being closed is always tolerated, except for the
+            // `OpenDesiredByRemote` state which might need to be switched back to `Closed`.
+            match &mut self.protocols[protocol_index].state {
+                State::Closed { .. }
+                | State::Open {
+                    in_substream: None, ..
+                }
+                | State::Opening {
+                    in_substream: None, ..
+                } => {}
+
+                State::Open {
+                    in_substream: in_substream @ Some(_),
+                    ..
+                } => match Stream::poll_next(Pin::new(in_substream.as_mut().unwrap()), cx) {
+                    Poll::Pending => {}
+                    Poll::Ready(Some(Ok(message))) => {
+                        let event = NotifsHandlerOut::Notification {
+                            protocol_index,
+                            message,
+                        };
+                        return Poll::Ready(ConnectionHandlerEvent::Custom(event));
+                    }
+                    Poll::Ready(None) | Poll::Ready(Some(Err(_))) => *in_substream = None,
+                },
+
+                State::OpenDesiredByRemote {
+                    in_substream,
+                    pending_opening,
+                } => match NotificationsInSubstream::poll_process(Pin::new(in_substream), cx) {
+                    Poll::Pending => {}
+                    Poll::Ready(Ok(void)) => match void {},
+                    Poll::Ready(Err(_)) => {
+                        self.protocols[protocol_index].state = State::Closed {
+                            pending_opening: *pending_opening,
+                        };
+                        return Poll::Ready(ConnectionHandlerEvent::Custom(
+                            NotifsHandlerOut::CloseDesired { protocol_index },
+                        ));
+                    }
+                },
+
+                State::Opening {
+                    in_substream: in_substream @ Some(_),
+                    ..
+                } => match NotificationsInSubstream::poll_process(
+                    Pin::new(in_substream.as_mut().unwrap()),
+                    cx,
+                ) {
+                    Poll::Pending => {}
+                    Poll::Ready(Ok(void)) => match void {},
+                    Poll::Ready(Err(_)) => *in_substream = None,
+                },
+            }
+        }
+
+        // This is the only place in this method that can return `Pending`.
+        // By putting it at the very bottom, we are guaranteed that everything has been properly
+        // polled.
+        Poll::Pending
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+    use crate::protocol::notifications::upgrade::{
+        NotificationsInOpen, NotificationsInSubstreamHandshake, NotificationsOutOpen,
+    };
+    use asynchronous_codec::Framed;
+    use libp2p::{
+        core::muxing::SubstreamBox,
+        swarm::{handler, ConnectionHandlerUpgrErr},
+        Multiaddr,
+    };
+    use multistream_select::{dialer_select_proto, listener_select_proto, Negotiated, Version};
+    use std::{
+        collections::HashMap,
+        io::{Error, IoSlice, IoSliceMut},
+    };
+    use tokio::sync::mpsc;
+    use unsigned_varint::codec::UviBytes;
+
+    struct OpenSubstream {
+        notifications: stream::Peekable<NotificationsSubstream>,
+        _in_substream: MockSubstream,
+        _out_substream: MockSubstream,
+    }
+
+    type NotificationsSubstream = stream::Select<
+        stream::Fuse<futures::channel::mpsc::Receiver<NotificationsSinkMessage>>,
+        stream::Fuse<futures::channel::mpsc::Receiver<NotificationsSinkMessage>>,
+    >;
+
+    pub struct ConnectionYielder {
+        connections: HashMap<(PeerId, usize), OpenSubstream>,
+    }
+
+    impl ConnectionYielder {
+        /// Create new [`ConnectionYielder`].
+        pub fn new() -> Self {
+            Self {
+                connections: HashMap::new(),
+            }
+        }
+
+        /// Open a new substream for peer.
+        pub fn open_substream(
+            &mut self,
+            peer: PeerId,
+            protocol_index: usize,
+            endpoint: ConnectedPoint,
+            received_handshake: Vec<u8>,
+        ) -> NotifsHandlerOut {
+            let (async_tx, async_rx) =
+                futures::channel::mpsc::channel(ASYNC_NOTIFICATIONS_BUFFER_SIZE);
+            let (sync_tx, sync_rx) =
+                futures::channel::mpsc::channel(SYNC_NOTIFICATIONS_BUFFER_SIZE);
+            let notifications_sink = NotificationsSink {
+                inner: Arc::new(NotificationsSinkInner {
+                    peer_id: peer,
+                    async_channel: FuturesMutex::new(async_tx),
+                    sync_channel: Mutex::new(Some(sync_tx)),
+                }),
+                metrics: None,
+            };
+            let (in_substream, out_substream) = MockSubstream::new();
+
+            self.connections.insert(
+                (peer, protocol_index),
+                OpenSubstream {
+                    notifications: stream::select(async_rx.fuse(), sync_rx.fuse()).peekable(),
+                    _in_substream: in_substream,
+                    _out_substream: out_substream,
+                },
+            );
+
+            NotifsHandlerOut::OpenResultOk {
+                protocol_index,
+                negotiated_fallback: None,
+                endpoint,
+                received_handshake,
+                notifications_sink,
+                inbound: false,
+            }
+        }
+
+        /// Attempt to get next pending event from one of the notification sinks.
+        pub async fn get_next_event(&mut self, peer: PeerId, set: usize) -> Option<Vec<u8>> {
+            let substream = self.connections.get_mut(&(peer, set))?;
+
+            futures::future::poll_fn(|cx| match substream.notifications.poll_next_unpin(cx) {
+                Poll::Ready(Some(NotificationsSinkMessage::Notification { message })) => {
+                    Poll::Ready(Some(message))
+                }
+                Poll::Pending => Poll::Ready(None),
+                Poll::Ready(Some(NotificationsSinkMessage::ForceClose)) | Poll::Ready(None) => {
+                    panic!("sink closed")
+                }
+            })
+            .await
+        }
+    }
+    struct MockSubstream {
+        pub rx: mpsc::Receiver<Vec<u8>>,
+        pub tx: mpsc::Sender<Vec<u8>>,
+        rx_buffer: BytesMut,
+    }
+
+    impl MockSubstream {
+        /// Create new substream pair.
+        pub fn new() -> (Self, Self) {
+            let (tx1, rx1) = mpsc::channel(32);
+            let (tx2, rx2) = mpsc::channel(32);
+
+            (
+                Self {
+                    rx: rx1,
+                    tx: tx2,
+                    rx_buffer: BytesMut::with_capacity(512),
+                },
+                Self {
+                    rx: rx2,
+                    tx: tx1,
+                    rx_buffer: BytesMut::with_capacity(512),
+                },
+            )
+        }
+
+        /// Create new negotiated substream pair.
+        pub async fn negotiated() -> (Negotiated<SubstreamBox>, Negotiated<SubstreamBox>) {
+            let (socket1, socket2) = Self::new();
+            let socket1 = SubstreamBox::new(socket1);
+            let socket2 = SubstreamBox::new(socket2);
+
+            let protos = vec![b"/echo/1.0.0", b"/echo/2.5.0"];
+            let (res1, res2) = tokio::join!(
+                dialer_select_proto(socket1, protos.clone(), Version::V1),
+                listener_select_proto(socket2, protos),
+            );
+
+            (res1.unwrap().1, res2.unwrap().1)
+        }
+    }
+
+    impl AsyncWrite for MockSubstream {
+        fn poll_write<'a>(
+            self: Pin<&mut Self>,
+            _cx: &mut Context<'a>,
+            buf: &[u8],
+        ) -> Poll<Result<usize, Error>> {
+            match self.tx.try_send(buf.to_vec()) {
+                Ok(_) => Poll::Ready(Ok(buf.len())),
+                Err(_) => Poll::Ready(Err(std::io::ErrorKind::UnexpectedEof.into())),
+            }
+        }
+
+        fn poll_flush<'a>(self: Pin<&mut Self>, _cx: &mut Context<'a>) -> Poll<Result<(), Error>> {
+            Poll::Ready(Ok(()))
+        }
+
+        fn poll_close<'a>(self: Pin<&mut Self>, _cx: &mut Context<'a>) -> Poll<Result<(), Error>> {
+            Poll::Ready(Ok(()))
+        }
+
+        fn poll_write_vectored<'a, 'b>(
+            self: Pin<&mut Self>,
+            _cx: &mut Context<'a>,
+            _bufs: &[IoSlice<'b>],
+        ) -> Poll<Result<usize, Error>> {
+            unimplemented!();
+        }
+    }
+
+    impl AsyncRead for MockSubstream {
+        fn poll_read<'a>(
+            mut self: Pin<&mut Self>,
+            cx: &mut Context<'a>,
+            buf: &mut [u8],
+        ) -> Poll<Result<usize, Error>> {
+            match self.rx.poll_recv(cx) {
+                Poll::Ready(Some(data)) => self.rx_buffer.extend_from_slice(&data),
+                Poll::Ready(None) => {
+                    return Poll::Ready(Err(std::io::ErrorKind::UnexpectedEof.into()))
+                }
+                _ => {}
+            }
+
+            let nsize = std::cmp::min(self.rx_buffer.len(), buf.len());
+            let data = self.rx_buffer.split_to(nsize);
+            buf[..nsize].copy_from_slice(&data[..]);
+
+            if nsize > 0 {
+                return Poll::Ready(Ok(nsize));
+            }
+
+            Poll::Pending
+        }
+
+        fn poll_read_vectored<'a, 'b>(
+            self: Pin<&mut Self>,
+            _cx: &mut Context<'a>,
+            _bufs: &mut [IoSliceMut<'b>],
+        ) -> Poll<Result<usize, Error>> {
+            unimplemented!();
+        }
+    }
+
+    /// Create new [`NotifsHandler`].
+    fn notifs_handler() -> NotifsHandler {
+        let proto = Protocol {
+            config: ProtocolConfig {
+                name: "/foo".into(),
+                fallback_names: vec![],
+                handshake: Arc::new(RwLock::new(b"hello, world".to_vec())),
+                max_notification_size: u64::MAX,
+            },
+            in_upgrade: NotificationsIn::new("/foo", Vec::new(), u64::MAX),
+            state: State::Closed {
+                pending_opening: false,
+            },
+        };
+
+        NotifsHandler {
+            protocols: vec![proto],
+            when_connection_open: Instant::now(),
+            endpoint: ConnectedPoint::Listener {
+                local_addr: Multiaddr::empty(),
+                send_back_addr: Multiaddr::empty(),
+            },
+            peer_id: PeerId::random(),
+            events_queue: VecDeque::new(),
+            metrics: None,
+        }
+    }
+
+    // verify that if another substream is attempted to be opened by remote while an inbound
+    // substream already exists, the new inbound stream is rejected and closed by the local node.
+    #[tokio::test]
+    async fn second_open_desired_by_remote_rejected() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // attempt to open another inbound substream and verify that it is rejected
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the new substream is rejected and closed
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+
+            if let Poll::Ready(Err(err)) = Pin::new(&mut io2).poll_read(cx, &mut buf) {
+                assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof,);
+            }
+
+            Poll::Ready(())
+        })
+        .await;
+    }
+
+    #[tokio::test]
+    async fn open_rejected_if_substream_is_opening() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // move the handler state to 'Opening'
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        // remote now tries to open another substream, verify that it is rejected and closed
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the new substream is rejected and closed but that the first substream is
+        // still in correct state
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+
+            if let Poll::Ready(Err(err)) = Pin::new(&mut io2).poll_read(cx, &mut buf) {
+                assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof,);
+            } else {
+                panic!("unexpected result");
+            }
+
+            Poll::Ready(())
+        })
+        .await;
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn open_rejected_if_substream_already_open() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // move the handler state to 'Opening'
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        // accept the substream and move its state to `Open`
+        let (io, _io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_out = NotificationsOutOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsOutSubstream::new(Framed::new(io, codec)),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedOutbound(
+            handler::FullyNegotiatedOutbound {
+                protocol: notif_out,
+                info: 0,
+            },
+        ));
+
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Open {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        // remote now tries to open another substream, verify that it is rejected and closed
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the new substream is rejected and closed but that the first substream is
+        // still in correct state
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+
+            if let Poll::Ready(Err(err)) = Pin::new(&mut io2).poll_read(cx, &mut buf) {
+                assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
+            } else {
+                panic!("unexpected result");
+            }
+
+            Poll::Ready(())
+        })
+        .await;
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Open {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn fully_negotiated_resets_state_for_closed_substream() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // first instruct the handler to open a connection and then close it right after
+        // so the handler is in state `Closed { pending_opening: true }`
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        handler.on_behaviour_event(NotifsHandlerIn::Close { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: true
+            }
+        ));
+
+        // verify that if the the outbound substream is successfully negotiated, the state is not
+        // changed as the substream was commanded to be closed by the handler.
+        let (io, _io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_out = NotificationsOutOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsOutSubstream::new(Framed::new(io, codec)),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedOutbound(
+            handler::FullyNegotiatedOutbound {
+                protocol: notif_out,
+                info: 0,
+            },
+        ));
+
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: false
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn fully_negotiated_resets_state_for_open_desired_substream() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // first instruct the handler to open a connection and then close it right after
+        // so the handler is in state `Closed { pending_opening: true }`
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        handler.on_behaviour_event(NotifsHandlerIn::Close { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: true
+            }
+        ));
+
+        // attempt to open another inbound substream and verify that it is rejected
+        let (io, _io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote {
+                pending_opening: true,
+                ..
+            }
+        ));
+
+        // verify that if the the outbound substream is successfully negotiated, the state is not
+        // changed as the substream was commanded to be closed by the handler.
+        let (io, _io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_out = NotificationsOutOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsOutSubstream::new(Framed::new(io, codec)),
+        };
+
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedOutbound(
+            handler::FullyNegotiatedOutbound {
+                protocol: notif_out,
+                info: 0,
+            },
+        ));
+
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote {
+                pending_opening: false,
+                ..
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn dial_upgrade_error_resets_closed_outbound_state() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // first instruct the handler to open a connection and then close it right after
+        // so the handler is in state `Closed { pending_opening: true }`
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        handler.on_behaviour_event(NotifsHandlerIn::Close { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: true
+            }
+        ));
+
+        // inject dial failure to an already closed substream and verify outbound state is reset
+        handler.on_connection_event(handler::ConnectionEvent::DialUpgradeError(
+            handler::DialUpgradeError {
+                info: 0,
+                error: ConnectionHandlerUpgrErr::Timeout,
+            },
+        ));
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: false
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn dial_upgrade_error_resets_open_desired_state() {
+        let mut handler = notifs_handler();
+        let (io, mut io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        // verify that the substream is in (partly) opened state
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote { .. }
+        ));
+        futures::future::poll_fn(|cx| {
+            let mut buf = Vec::with_capacity(512);
+            assert!(std::matches!(
+                Pin::new(&mut io2).poll_read(cx, &mut buf),
+                Poll::Pending
+            ));
+            Poll::Ready(())
+        })
+        .await;
+
+        // first instruct the handler to open a connection and then close it right after
+        // so the handler is in state `Closed { pending_opening: true }`
+        handler.on_behaviour_event(NotifsHandlerIn::Open { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Opening {
+                in_substream: Some(_),
+                ..
+            }
+        ));
+
+        handler.on_behaviour_event(NotifsHandlerIn::Close { protocol_index: 0 });
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::Closed {
+                pending_opening: true
+            }
+        ));
+
+        let (io, _io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::NotSent,
+            ),
+        };
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote {
+                pending_opening: true,
+                ..
+            }
+        ));
+
+        // inject dial failure to an already closed substream and verify outbound state is reset
+        handler.on_connection_event(handler::ConnectionEvent::DialUpgradeError(
+            handler::DialUpgradeError {
+                info: 0,
+                error: ConnectionHandlerUpgrErr::Timeout,
+            },
+        ));
+        assert!(std::matches!(
+            handler.protocols[0].state,
+            State::OpenDesiredByRemote {
+                pending_opening: false,
+                ..
+            }
+        ));
+    }
+
+    #[tokio::test]
+    async fn sync_notifications_clogged() {
+        let mut handler = notifs_handler();
+        let (io, _) = MockSubstream::negotiated().await;
+        let codec = UviBytes::default();
+
+        let (async_tx, async_rx) = futures::channel::mpsc::channel(ASYNC_NOTIFICATIONS_BUFFER_SIZE);
+        let (sync_tx, sync_rx) = futures::channel::mpsc::channel(1);
+        let notifications_sink = NotificationsSink {
+            inner: Arc::new(NotificationsSinkInner {
+                peer_id: PeerId::random(),
+                async_channel: FuturesMutex::new(async_tx),
+                sync_channel: Mutex::new(Some(sync_tx)),
+            }),
+            metrics: None,
+        };
+
+        handler.protocols[0].state = State::Open {
+            notifications_sink_rx: stream::select(async_rx.fuse(), sync_rx.fuse()).peekable(),
+            out_substream: Some(NotificationsOutSubstream::new(Framed::new(io, codec))),
+            in_substream: None,
+        };
+
+        notifications_sink.send_sync_notification(vec![1, 3, 3, 7]);
+        notifications_sink.send_sync_notification(vec![1, 3, 3, 8]);
+        notifications_sink.send_sync_notification(vec![1, 3, 3, 9]);
+        notifications_sink.send_sync_notification(vec![1, 3, 4, 0]);
+
+        futures::future::poll_fn(|cx| {
+            assert!(std::matches!(
+                handler.poll(cx),
+                Poll::Ready(ConnectionHandlerEvent::Close(
+                    NotifsHandlerError::SyncNotificationsClogged,
+                ))
+            ));
+            Poll::Ready(())
+        })
+        .await;
+    }
+
+    #[tokio::test]
+    async fn close_desired_by_remote() {
+        let mut handler = notifs_handler();
+        let (io, io2) = MockSubstream::negotiated().await;
+        let mut codec = UviBytes::default();
+        codec.set_max_len(usize::MAX);
+
+        let notif_in = NotificationsInOpen {
+            handshake: b"hello, world".to_vec(),
+            negotiated_fallback: None,
+            substream: NotificationsInSubstream::new(
+                Framed::new(io, codec),
+                NotificationsInSubstreamHandshake::PendingSend(vec![1, 2, 3, 4]),
+            ),
+        };
+
+        // add new inbound substream but close it immediately and verify that correct events are
+        // emitted
+        handler.on_connection_event(handler::ConnectionEvent::FullyNegotiatedInbound(
+            handler::FullyNegotiatedInbound {
+                protocol: (notif_in, 0),
+                info: (),
+            },
+        ));
+        drop(io2);
+
+        futures::future::poll_fn(|cx| {
+            assert!(std::matches!(
+                handler.poll(cx),
+                Poll::Ready(ConnectionHandlerEvent::Custom(
+                    NotifsHandlerOut::OpenDesiredByRemote {
+                        protocol_index: 0,
+                        ..
+                    },
+                ))
+            ));
+            assert!(std::matches!(
+                handler.poll(cx),
+                Poll::Ready(ConnectionHandlerEvent::Custom(
+                    NotifsHandlerOut::CloseDesired { protocol_index: 0 },
+                ))
+            ));
+            Poll::Ready(())
+        })
+        .await;
+    }
+}
diff --git a/libp2p/src/protocol/notifications/service/metrics.rs b/libp2p/src/protocol/notifications/service/metrics.rs
new file mode 100644
index 000000000..35056be8d
--- /dev/null
+++ b/libp2p/src/protocol/notifications/service/metrics.rs
@@ -0,0 +1,133 @@
+// 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::types::ProtocolName;
+
+use prometheus_endpoint::{
+    self as prometheus, CounterVec, HistogramOpts, HistogramVec, Opts, PrometheusError, Registry,
+    U64,
+};
+
+use std::sync::Arc;
+
+/// Notification metrics.
+#[derive(Debug, Clone)]
+pub struct Metrics {
+    // Total number of opened substreams.
+    pub notifications_streams_opened_total: CounterVec<U64>,
+
+    /// Total number of closed substreams.
+    pub notifications_streams_closed_total: CounterVec<U64>,
+
+    /// In/outbound notification sizes.
+    pub notifications_sizes: HistogramVec,
+}
+
+impl Metrics {
+    fn register(registry: &Registry) -> Result<Self, PrometheusError> {
+        Ok(Self {
+            notifications_sizes: prometheus::register(
+                HistogramVec::new(
+                    HistogramOpts {
+                        common_opts: Opts::new(
+                            "substrate_sub_libp2p_notifications_sizes",
+                            "Sizes of the notifications send to and received from all nodes",
+                        ),
+                        buckets: prometheus::exponential_buckets(64.0, 4.0, 8)
+                            .expect("parameters are always valid values; qed"),
+                    },
+                    &["direction", "protocol"],
+                )?,
+                registry,
+            )?,
+            notifications_streams_closed_total: prometheus::register(
+                CounterVec::new(
+                    Opts::new(
+                        "substrate_sub_libp2p_notifications_streams_closed_total",
+                        "Total number of notification substreams that have been closed",
+                    ),
+                    &["protocol"],
+                )?,
+                registry,
+            )?,
+            notifications_streams_opened_total: prometheus::register(
+                CounterVec::new(
+                    Opts::new(
+                        "substrate_sub_libp2p_notifications_streams_opened_total",
+                        "Total number of notification substreams that have been opened",
+                    ),
+                    &["protocol"],
+                )?,
+                registry,
+            )?,
+        })
+    }
+}
+
+/// Register metrics.
+pub fn register(registry: &Registry) -> Result<Metrics, PrometheusError> {
+    Metrics::register(registry)
+}
+
+/// Register opened substream to Prometheus.
+pub fn register_substream_opened(metrics: &Option<Metrics>, protocol: &ProtocolName) {
+    if let Some(metrics) = metrics {
+        metrics
+            .notifications_streams_opened_total
+            .with_label_values(&[&protocol])
+            .inc();
+    }
+}
+
+/// Register closed substream to Prometheus.
+pub fn register_substream_closed(metrics: &Option<Metrics>, protocol: &ProtocolName) {
+    if let Some(metrics) = metrics {
+        metrics
+            .notifications_streams_closed_total
+            .with_label_values(&[&protocol[..]])
+            .inc();
+    }
+}
+
+/// Register sent notification to Prometheus.
+pub fn register_notification_sent(
+    metrics: &Option<Arc<Metrics>>,
+    protocol: &ProtocolName,
+    size: usize,
+) {
+    if let Some(metrics) = metrics {
+        metrics
+            .notifications_sizes
+            .with_label_values(&["out", protocol])
+            .observe(size as f64);
+    }
+}
+
+/// Register received notification to Prometheus.
+pub fn register_notification_received(
+    metrics: &Option<Metrics>,
+    protocol: &ProtocolName,
+    size: usize,
+) {
+    if let Some(metrics) = metrics {
+        metrics
+            .notifications_sizes
+            .with_label_values(&["in", protocol])
+            .observe(size as f64);
+    }
+}
diff --git a/libp2p/src/protocol/notifications/service/mod.rs b/libp2p/src/protocol/notifications/service/mod.rs
new file mode 100644
index 000000000..198324587
--- /dev/null
+++ b/libp2p/src/protocol/notifications/service/mod.rs
@@ -0,0 +1,675 @@
+// 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/>.
+
+//! Notification service implementation.
+
+use crate::{
+    error,
+    protocol::notifications::handler::NotificationsSink,
+    service::traits::{
+        Direction, MessageSink, NotificationEvent, NotificationService, ValidationResult,
+    },
+    types::ProtocolName,
+};
+
+use futures::{
+    stream::{FuturesUnordered, Stream},
+    StreamExt,
+};
+use libp2p::PeerId;
+use parking_lot::Mutex;
+use tokio::sync::{mpsc, oneshot};
+use tokio_stream::wrappers::ReceiverStream;
+
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+
+use std::{collections::HashMap, fmt::Debug, sync::Arc};
+
+pub mod metrics;
+
+#[cfg(test)]
+mod tests;
+
+/// Logging target for the file.
+const LOG_TARGET: &str = "sub-libp2p";
+
+/// Default command queue size.
+const COMMAND_QUEUE_SIZE: usize = 64;
+
+/// Type representing subscribers of a notification protocol.
+type Subscribers = Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>;
+
+/// Type represending a distributable message sink.
+/// Detached message sink must carry the protocol name for registering metrics.
+///
+/// See documentation for [`PeerContext`] for more details.
+type NotificationSink = Arc<Mutex<(NotificationsSink, ProtocolName)>>;
+
+#[async_trait::async_trait]
+impl MessageSink for NotificationSink {
+    /// Send synchronous `notification` to the peer associated with this [`MessageSink`].
+    fn send_sync_notification(&self, notification: Vec<u8>) {
+        let sink = self.lock();
+
+        metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification.len());
+        sink.0.send_sync_notification(notification);
+    }
+
+    /// Send an asynchronous `notification` to the peer associated with this [`MessageSink`],
+    /// allowing sender to exercise backpressure.
+    ///
+    /// Returns an error if the peer does not exist.
+    async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error> {
+        // notification sink must be cloned because the lock cannot be held across `.await`
+        // this makes the implementation less efficient but not prohibitively so as the same
+        // method is also used by `NetworkService` when sending notifications.
+        let notification_len = notification.len();
+        let sink = self.lock().clone();
+        let permit = sink
+            .0
+            .reserve_notification()
+            .await
+            .map_err(|_| error::Error::ConnectionClosed)?;
+
+        permit
+            .send(notification)
+            .map_err(|_| error::Error::ChannelClosed)
+            .map(|res| {
+                metrics::register_notification_sent(sink.0.metrics(), &sink.1, notification_len);
+                res
+            })
+    }
+}
+
+/// Inner notification event to deal with `NotificationsSinks` without exposing that
+/// implementation detail to [`NotificationService`] consumers.
+#[derive(Debug)]
+enum InnerNotificationEvent {
+    /// Validate inbound substream.
+    ValidateInboundSubstream {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Received handshake.
+        handshake: Vec<u8>,
+
+        /// `oneshot::Sender` for sending validation result back to `Notifications`
+        result_tx: oneshot::Sender<ValidationResult>,
+    },
+
+    /// Notification substream open to `peer`.
+    NotificationStreamOpened {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Direction of the substream.
+        direction: Direction,
+
+        /// Received handshake.
+        handshake: Vec<u8>,
+
+        /// Negotiated fallback.
+        negotiated_fallback: Option<ProtocolName>,
+
+        /// Notification sink.
+        sink: NotificationsSink,
+    },
+
+    /// Substream was closed.
+    NotificationStreamClosed {
+        /// Peer ID.
+        peer: PeerId,
+    },
+
+    /// Notification was received from the substream.
+    NotificationReceived {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Received notification.
+        notification: Vec<u8>,
+    },
+
+    /// Notification sink has been replaced.
+    NotificationSinkReplaced {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Notification sink.
+        sink: NotificationsSink,
+    },
+}
+
+/// Notification commands.
+///
+/// Sent by the installed protocols to `Notifications` to open/close/modify substreams.
+#[derive(Debug)]
+pub enum NotificationCommand {
+    /// Instruct `Notifications` to open a substream to peer.
+    #[allow(unused)]
+    OpenSubstream(PeerId),
+
+    /// Instruct `Notifications` to close the substream to peer.
+    #[allow(unused)]
+    CloseSubstream(PeerId),
+
+    /// Set handshake for the notifications protocol.
+    SetHandshake(Vec<u8>),
+}
+
+/// Context assigned to each peer.
+///
+/// Contains `NotificationsSink` used by [`NotificationService`] to send notifications
+/// and an additional, distributable `NotificationsSink` which the protocol may acquire
+/// if it wishes to send notifications through `NotificationsSink` directly.
+///
+/// The distributable `NoticationsSink` is wrapped in an `Arc<Mutex<>>` to allow
+/// `NotificationsService` to swap the underlying sink in case it's replaced.
+#[derive(Debug, Clone)]
+struct PeerContext {
+    /// Sink for sending notificaitons.
+    sink: NotificationsSink,
+
+    /// Distributable notification sink.
+    shared_sink: NotificationSink,
+}
+
+/// Handle that is passed on to the notifications protocol.
+#[derive(Debug)]
+pub struct NotificationHandle {
+    /// Protocol name.
+    protocol: ProtocolName,
+
+    /// TX channel for sending commands to `Notifications`.
+    tx: mpsc::Sender<NotificationCommand>,
+
+    /// RX channel for receiving events from `Notifications`.
+    rx: TracingUnboundedReceiver<InnerNotificationEvent>,
+
+    /// All subscribers of `NotificationEvent`s.
+    subscribers: Subscribers,
+
+    /// Connected peers.
+    peers: HashMap<PeerId, PeerContext>,
+}
+
+impl NotificationHandle {
+    /// Create new [`NotificationHandle`].
+    fn new(
+        protocol: ProtocolName,
+        tx: mpsc::Sender<NotificationCommand>,
+        rx: TracingUnboundedReceiver<InnerNotificationEvent>,
+        subscribers: Arc<Mutex<Vec<TracingUnboundedSender<InnerNotificationEvent>>>>,
+    ) -> Self {
+        Self {
+            protocol,
+            tx,
+            rx,
+            subscribers,
+            peers: HashMap::new(),
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl NotificationService for NotificationHandle {
+    /// Instruct `Notifications` to open a new substream for `peer`.
+    async fn open_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
+        todo!("support for opening substreams not implemented yet");
+    }
+
+    /// Instruct `Notifications` to close substream for `peer`.
+    async fn close_substream(&mut self, _peer: PeerId) -> Result<(), ()> {
+        todo!("support for closing substreams not implemented yet, call `NetworkService::disconnect_peer()` instead");
+    }
+
+    /// Send synchronous `notification` to `peer`.
+    fn send_sync_notification(&self, peer: &PeerId, notification: Vec<u8>) {
+        if let Some(info) = self.peers.get(peer) {
+            metrics::register_notification_sent(
+                info.sink.metrics(),
+                &self.protocol,
+                notification.len(),
+            );
+
+            info.sink.send_sync_notification(notification);
+        }
+    }
+
+    /// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
+    async fn send_async_notification(
+        &self,
+        peer: &PeerId,
+        notification: Vec<u8>,
+    ) -> Result<(), error::Error> {
+        let notification_len = notification.len();
+        let sink = &self
+            .peers
+            .get(peer)
+            .ok_or_else(|| error::Error::PeerDoesntExist(*peer))?
+            .sink;
+
+        sink.reserve_notification()
+            .await
+            .map_err(|_| error::Error::ConnectionClosed)?
+            .send(notification)
+            .map_err(|_| error::Error::ChannelClosed)
+            .map(|res| {
+                metrics::register_notification_sent(
+                    sink.metrics(),
+                    &self.protocol,
+                    notification_len,
+                );
+                res
+            })
+    }
+
+    /// Set handshake for the notification protocol replacing the old handshake.
+    async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
+        log::trace!(target: LOG_TARGET, "{}: set handshake to {handshake:?}", self.protocol);
+
+        self.tx
+            .send(NotificationCommand::SetHandshake(handshake))
+            .await
+            .map_err(|_| ())
+    }
+
+    /// Non-blocking variant of `set_handshake()` that attempts to update the handshake
+    /// and returns an error if the channel is blocked.
+    ///
+    /// Technically the function can return an error if the channel to `Notifications` is closed
+    /// but that doesn't happen under normal operation.
+    fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()> {
+        self.tx
+            .try_send(NotificationCommand::SetHandshake(handshake))
+            .map_err(|_| ())
+    }
+
+    /// Get next event from the `Notifications` event stream.
+    async fn next_event(&mut self) -> Option<NotificationEvent> {
+        loop {
+            match self.rx.next().await? {
+                InnerNotificationEvent::ValidateInboundSubstream {
+                    peer,
+                    handshake,
+                    result_tx,
+                } => {
+                    return Some(NotificationEvent::ValidateInboundSubstream {
+                        peer,
+                        handshake,
+                        result_tx,
+                    })
+                }
+                InnerNotificationEvent::NotificationStreamOpened {
+                    peer,
+                    handshake,
+                    negotiated_fallback,
+                    direction,
+                    sink,
+                } => {
+                    self.peers.insert(
+                        peer,
+                        PeerContext {
+                            sink: sink.clone(),
+                            shared_sink: Arc::new(Mutex::new((sink, self.protocol.clone()))),
+                        },
+                    );
+                    return Some(NotificationEvent::NotificationStreamOpened {
+                        peer,
+                        handshake,
+                        direction,
+                        negotiated_fallback,
+                    });
+                }
+                InnerNotificationEvent::NotificationStreamClosed { peer } => {
+                    self.peers.remove(&peer);
+                    return Some(NotificationEvent::NotificationStreamClosed { peer });
+                }
+                InnerNotificationEvent::NotificationReceived { peer, notification } => {
+                    return Some(NotificationEvent::NotificationReceived { peer, notification })
+                }
+                InnerNotificationEvent::NotificationSinkReplaced { peer, sink } => {
+                    match self.peers.get_mut(&peer) {
+                        None => log::error!(
+                            "{}: notification sink replaced for {peer} but peer does not exist",
+                            self.protocol
+                        ),
+                        Some(context) => {
+                            context.sink = sink.clone();
+                            *context.shared_sink.lock() = (sink.clone(), self.protocol.clone());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    // Clone [`NotificationService`]
+    fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()> {
+        let mut subscribers = self.subscribers.lock();
+        let (event_tx, event_rx) = tracing_unbounded("mpsc-notification-to-protocol", 100_000);
+        subscribers.push(event_tx);
+
+        Ok(Box::new(NotificationHandle {
+            protocol: self.protocol.clone(),
+            tx: self.tx.clone(),
+            rx: event_rx,
+            peers: self.peers.clone(),
+            subscribers: self.subscribers.clone(),
+        }))
+    }
+
+    /// Get protocol name.
+    fn protocol(&self) -> &ProtocolName {
+        &self.protocol
+    }
+
+    /// Get message sink of the peer.
+    fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>> {
+        match self.peers.get(peer) {
+            Some(context) => Some(Box::new(context.shared_sink.clone())),
+            None => None,
+        }
+    }
+}
+
+/// Channel pair which allows `Notifications` to interact with a protocol.
+#[derive(Debug)]
+pub struct ProtocolHandlePair {
+    /// Protocol name.
+    protocol: ProtocolName,
+
+    /// Subscribers of the notification protocol events.
+    subscribers: Subscribers,
+
+    // Receiver for notification commands received from the protocol implementation.
+    rx: mpsc::Receiver<NotificationCommand>,
+}
+
+impl ProtocolHandlePair {
+    /// Create new [`ProtocolHandlePair`].
+    fn new(
+        protocol: ProtocolName,
+        subscribers: Subscribers,
+        rx: mpsc::Receiver<NotificationCommand>,
+    ) -> Self {
+        Self {
+            protocol,
+            subscribers,
+            rx,
+        }
+    }
+
+    /// Consume `self` and split [`ProtocolHandlePair`] into a handle which allows it to send events
+    /// to the protocol and a stream of commands received from the protocol.
+    pub(crate) fn split(
+        self,
+    ) -> (
+        ProtocolHandle,
+        Box<dyn Stream<Item = NotificationCommand> + Send + Unpin>,
+    ) {
+        (
+            ProtocolHandle::new(self.protocol, self.subscribers),
+            Box::new(ReceiverStream::new(self.rx)),
+        )
+    }
+}
+
+/// Handle that is passed on to `Notifications` and allows it to directly communicate
+/// with the protocol.
+#[derive(Debug, Clone)]
+pub struct ProtocolHandle {
+    /// Protocol name.
+    protocol: ProtocolName,
+
+    /// Subscribers of the notification protocol.
+    subscribers: Subscribers,
+
+    /// Number of connected peers.
+    num_peers: usize,
+
+    /// Delegate validation to `Peerset`.
+    delegate_to_peerset: bool,
+
+    /// Prometheus metrics.
+    metrics: Option<metrics::Metrics>,
+}
+
+pub enum ValidationCallResult {
+    WaitForValidation(oneshot::Receiver<ValidationResult>),
+    Delegated,
+}
+
+impl ProtocolHandle {
+    /// Create new [`ProtocolHandle`].
+    fn new(protocol: ProtocolName, subscribers: Subscribers) -> Self {
+        Self {
+            protocol,
+            subscribers,
+            num_peers: 0usize,
+            metrics: None,
+            delegate_to_peerset: false,
+        }
+    }
+
+    /// Set metrics.
+    pub fn set_metrics(&mut self, metrics: Option<metrics::Metrics>) {
+        self.metrics = metrics;
+    }
+
+    /// Delegate validation to `Peerset`.
+    ///
+    /// Protocols that do not do any validation themselves and only rely on `Peerset` handling
+    /// validation can disable protocol-side validation entirely by delegating all validation to
+    /// `Peerset`.
+    pub fn delegate_to_peerset(&mut self, delegate: bool) {
+        self.delegate_to_peerset = delegate;
+    }
+
+    /// Report to the protocol that a substream has been opened and it must be validated by the
+    /// protocol.
+    ///
+    /// Return `oneshot::Receiver` which allows `Notifications` to poll for the validation result
+    /// from protocol.
+    #[allow(clippy::result_unit_err)]
+    pub fn report_incoming_substream(
+        &self,
+        peer: PeerId,
+        handshake: Vec<u8>,
+    ) -> Result<ValidationCallResult, ()> {
+        let subscribers = self.subscribers.lock();
+
+        log::trace!(
+            target: LOG_TARGET,
+            "{}: report incoming substream for {peer}, handshake {handshake:?}",
+            self.protocol
+        );
+
+        if self.delegate_to_peerset {
+            return Ok(ValidationCallResult::Delegated);
+        }
+
+        // if there is only one subscriber, `Notifications` can wait directly on the
+        // `oneshot::channel()`'s RX half without indirection
+        if subscribers.len() == 1 {
+            let (result_tx, rx) = oneshot::channel();
+            return subscribers[0]
+                .unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
+                    peer,
+                    handshake,
+                    result_tx,
+                })
+                .map(|_| ValidationCallResult::WaitForValidation(rx))
+                .map_err(|_| ());
+        }
+
+        // if there are multiple subscribers, create a task which waits for all of the
+        // validations to finish and returns the combined result to `Notifications`
+        let mut results: FuturesUnordered<_> = subscribers
+            .iter()
+            .filter_map(|subscriber| {
+                let (result_tx, rx) = oneshot::channel();
+
+                subscriber
+                    .unbounded_send(InnerNotificationEvent::ValidateInboundSubstream {
+                        peer,
+                        handshake: handshake.clone(),
+                        result_tx,
+                    })
+                    .is_ok()
+                    .then_some(rx)
+            })
+            .collect();
+
+        let (tx, rx) = oneshot::channel();
+        tokio::spawn(async move {
+            while let Some(event) = results.next().await {
+                match event {
+                    Err(_) | Ok(ValidationResult::Reject) => {
+                        return tx.send(ValidationResult::Reject)
+                    }
+                    Ok(ValidationResult::Accept) => {}
+                }
+            }
+
+            tx.send(ValidationResult::Accept)
+        });
+
+        Ok(ValidationCallResult::WaitForValidation(rx))
+    }
+
+    /// Report to the protocol that a substream has been opened and that it can now use the handle
+    /// to send notifications to the remote peer.
+    #[allow(clippy::result_unit_err)]
+    pub fn report_substream_opened(
+        &mut self,
+        peer: PeerId,
+        direction: Direction,
+        handshake: Vec<u8>,
+        negotiated_fallback: Option<ProtocolName>,
+        sink: NotificationsSink,
+    ) -> Result<(), ()> {
+        metrics::register_substream_opened(&self.metrics, &self.protocol);
+
+        let mut subscribers = self.subscribers.lock();
+        log::trace!(target: LOG_TARGET, "{}: substream opened for {peer:?}", self.protocol);
+
+        subscribers.retain(|subscriber| {
+            subscriber
+                .unbounded_send(InnerNotificationEvent::NotificationStreamOpened {
+                    peer,
+                    direction,
+                    handshake: handshake.clone(),
+                    negotiated_fallback: negotiated_fallback.clone(),
+                    sink: sink.clone(),
+                })
+                .is_ok()
+        });
+        self.num_peers += 1;
+
+        Ok(())
+    }
+
+    /// Substream was closed.
+    #[allow(clippy::result_unit_err)]
+    pub fn report_substream_closed(&mut self, peer: PeerId) -> Result<(), ()> {
+        metrics::register_substream_closed(&self.metrics, &self.protocol);
+
+        let mut subscribers = self.subscribers.lock();
+        log::trace!(target: LOG_TARGET, "{}: substream closed for {peer:?}", self.protocol);
+
+        subscribers.retain(|subscriber| {
+            subscriber
+                .unbounded_send(InnerNotificationEvent::NotificationStreamClosed { peer })
+                .is_ok()
+        });
+        self.num_peers -= 1;
+
+        Ok(())
+    }
+
+    /// Notification was received from the substream.
+    #[allow(clippy::result_unit_err)]
+    pub fn report_notification_received(
+        &mut self,
+        peer: PeerId,
+        notification: Vec<u8>,
+    ) -> Result<(), ()> {
+        metrics::register_notification_received(&self.metrics, &self.protocol, notification.len());
+
+        let mut subscribers = self.subscribers.lock();
+        log::trace!(target: LOG_TARGET, "{}: notification received from {peer:?}", self.protocol);
+
+        subscribers.retain(|subscriber| {
+            subscriber
+                .unbounded_send(InnerNotificationEvent::NotificationReceived {
+                    peer,
+                    notification: notification.clone(),
+                })
+                .is_ok()
+        });
+
+        Ok(())
+    }
+
+    /// Notification sink was replaced.
+    #[allow(clippy::result_unit_err)]
+    pub fn report_notification_sink_replaced(
+        &mut self,
+        peer: PeerId,
+        sink: NotificationsSink,
+    ) -> Result<(), ()> {
+        let mut subscribers = self.subscribers.lock();
+
+        log::trace!(
+            target: LOG_TARGET,
+            "{}: notification sink replaced for {peer:?}",
+            self.protocol
+        );
+
+        subscribers.retain(|subscriber| {
+            subscriber
+                .unbounded_send(InnerNotificationEvent::NotificationSinkReplaced {
+                    peer,
+                    sink: sink.clone(),
+                })
+                .is_ok()
+        });
+
+        Ok(())
+    }
+
+    /// Get the number of connected peers.
+    pub fn num_peers(&self) -> usize {
+        self.num_peers
+    }
+}
+
+/// Create new (protocol, notification) handle pair.
+///
+/// Handle pair allows `Notifications` and the protocol to communicate with each other directly.
+pub fn notification_service(protocol: ProtocolName) -> (ProtocolHandlePair, NotificationHandle) {
+    let (cmd_tx, cmd_rx) = mpsc::channel(COMMAND_QUEUE_SIZE);
+    let (event_tx, event_rx) = tracing_unbounded("mpsc-notification-to-protocol", 100_000);
+    let subscribers = Arc::new(Mutex::new(vec![event_tx]));
+
+    (
+        ProtocolHandlePair::new(protocol.clone(), subscribers.clone(), cmd_rx),
+        NotificationHandle::new(protocol.clone(), cmd_tx, event_rx, subscribers),
+    )
+}
diff --git a/libp2p/src/protocol/notifications/upgrade.rs b/libp2p/src/protocol/notifications/upgrade.rs
new file mode 100644
index 000000000..e9712afd5
--- /dev/null
+++ b/libp2p/src/protocol/notifications/upgrade.rs
@@ -0,0 +1,31 @@
+// 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/>.
+
+#[cfg(test)]
+pub(crate) use self::notifications::{
+    NotificationsInOpen, NotificationsInSubstreamHandshake, NotificationsOutOpen,
+};
+pub use self::{
+    collec::UpgradeCollec,
+    notifications::{
+        NotificationsIn, NotificationsInSubstream, NotificationsOut, NotificationsOutSubstream,
+    },
+};
+
+mod collec;
+mod notifications;
diff --git a/libp2p/src/protocol/notifications/upgrade/collec.rs b/libp2p/src/protocol/notifications/upgrade/collec.rs
new file mode 100644
index 000000000..35b979aa1
--- /dev/null
+++ b/libp2p/src/protocol/notifications/upgrade/collec.rs
@@ -0,0 +1,215 @@
+// 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 futures::prelude::*;
+use libp2p::core::upgrade::{InboundUpgrade, ProtocolName, UpgradeInfo};
+use std::{
+    iter::FromIterator,
+    pin::Pin,
+    task::{Context, Poll},
+    vec,
+};
+
+// TODO: move this to libp2p => https://github.com/libp2p/rust-libp2p/issues/1445
+
+/// Upgrade that combines multiple upgrades of the same type into one. Supports all the protocols
+/// supported by either sub-upgrade.
+#[derive(Debug, Clone)]
+pub struct UpgradeCollec<T>(pub Vec<T>);
+
+impl<T> From<Vec<T>> for UpgradeCollec<T> {
+    fn from(list: Vec<T>) -> Self {
+        Self(list)
+    }
+}
+
+impl<T> FromIterator<T> for UpgradeCollec<T> {
+    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
+        Self(iter.into_iter().collect())
+    }
+}
+
+impl<T: UpgradeInfo> UpgradeInfo for UpgradeCollec<T> {
+    type Info = ProtoNameWithUsize<T::Info>;
+    type InfoIter = vec::IntoIter<Self::Info>;
+
+    fn protocol_info(&self) -> Self::InfoIter {
+        self.0
+            .iter()
+            .enumerate()
+            .flat_map(|(n, p)| {
+                p.protocol_info()
+                    .into_iter()
+                    .map(move |i| ProtoNameWithUsize(i, n))
+            })
+            .collect::<Vec<_>>()
+            .into_iter()
+    }
+}
+
+impl<T, C> InboundUpgrade<C> for UpgradeCollec<T>
+where
+    T: InboundUpgrade<C>,
+{
+    type Output = (T::Output, usize);
+    type Error = (T::Error, usize);
+    type Future = FutWithUsize<T::Future>;
+
+    fn upgrade_inbound(mut self, sock: C, info: Self::Info) -> Self::Future {
+        let fut = self.0.remove(info.1).upgrade_inbound(sock, info.0);
+        FutWithUsize(fut, info.1)
+    }
+}
+
+/// Groups a `ProtocolName` with a `usize`.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ProtoNameWithUsize<T>(T, usize);
+
+impl<T: ProtocolName> ProtocolName for ProtoNameWithUsize<T> {
+    fn protocol_name(&self) -> &[u8] {
+        self.0.protocol_name()
+    }
+}
+
+/// Equivalent to `fut.map_ok(|v| (v, num)).map_err(|e| (e, num))`, where `fut` and `num` are
+/// the two fields of this struct.
+#[pin_project::pin_project]
+pub struct FutWithUsize<T>(#[pin] T, usize);
+
+impl<T: Future<Output = Result<O, E>>, O, E> Future for FutWithUsize<T> {
+    type Output = Result<(O, usize), (E, usize)>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+        let this = self.project();
+        match Future::poll(this.0, cx) {
+            Poll::Ready(Ok(v)) => Poll::Ready(Ok((v, *this.1))),
+            Poll::Ready(Err(e)) => Poll::Ready(Err((e, *this.1))),
+            Poll::Pending => Poll::Pending,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::types::ProtocolName as ProtoName;
+    use libp2p::core::upgrade::{ProtocolName, UpgradeInfo};
+
+    // TODO: move to mocks
+    mockall::mock! {
+        pub ProtocolUpgrade<T> {}
+
+        impl<T: Clone + ProtocolName> UpgradeInfo for ProtocolUpgrade<T> {
+            type Info = T;
+            type InfoIter = vec::IntoIter<T>;
+            fn protocol_info(&self) -> vec::IntoIter<T>;
+        }
+    }
+
+    #[test]
+    fn protocol_info() {
+        let upgrades = (1..=3)
+            .map(|i| {
+                let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
+                upgrade.expect_protocol_info().return_once(move || {
+                    vec![ProtoNameWithUsize(
+                        ProtoName::from(format!("protocol{i}")),
+                        i,
+                    )]
+                    .into_iter()
+                });
+                upgrade
+            })
+            .collect::<Vec<_>>();
+
+        let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
+        let protos = vec![
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1),
+                0,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2),
+                1,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol3".to_string()), 3),
+                2,
+            ),
+        ];
+        let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
+
+        assert_eq!(upgrades, protos,);
+    }
+
+    #[test]
+    fn nested_protocol_info() {
+        let mut upgrades = (1..=2)
+            .map(|i| {
+                let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
+                upgrade.expect_protocol_info().return_once(move || {
+                    vec![ProtoNameWithUsize(
+                        ProtoName::from(format!("protocol{i}")),
+                        i,
+                    )]
+                    .into_iter()
+                });
+                upgrade
+            })
+            .collect::<Vec<_>>();
+
+        upgrades.push({
+            let mut upgrade = MockProtocolUpgrade::<ProtoNameWithUsize<ProtoName>>::new();
+            upgrade.expect_protocol_info().return_once(move || {
+                vec![
+                    ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1),
+                    ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2),
+                    ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3),
+                ]
+                .into_iter()
+            });
+            upgrade
+        });
+
+        let upgrade: UpgradeCollec<_> = upgrades.into_iter().collect::<UpgradeCollec<_>>();
+        let protos = vec![
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol1".to_string()), 1),
+                0,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol2".to_string()), 2),
+                1,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol22".to_string()), 1),
+                2,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol33".to_string()), 2),
+                2,
+            ),
+            ProtoNameWithUsize(
+                ProtoNameWithUsize(ProtoName::from("protocol44".to_string()), 3),
+                2,
+            ),
+        ];
+        let upgrades = upgrade.protocol_info().collect::<Vec<_>>();
+        assert_eq!(upgrades, protos,);
+    }
+}
diff --git a/libp2p/src/protocol/notifications/upgrade/notifications.rs b/libp2p/src/protocol/notifications/upgrade/notifications.rs
new file mode 100644
index 000000000..d9d6f4d3b
--- /dev/null
+++ b/libp2p/src/protocol/notifications/upgrade/notifications.rs
@@ -0,0 +1,754 @@
+// 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/>.
+
+/// Notifications protocol.
+///
+/// The Substrate notifications protocol consists in the following:
+///
+/// - Node A opens a substream to node B and sends a message which contains some
+///   protocol-specific higher-level logic. This message is prefixed with a variable-length
+///   integer message length. This message can be empty, in which case `0` is sent.
+/// - If node B accepts the substream, it sends back a message with the same properties.
+/// - If instead B refuses the connection (which typically happens because no empty slot is
+///   available), then it immediately closes the substream without sending back anything.
+/// - Node A can then send notifications to B, prefixed with a variable-length integer
+///   indicating the length of the message.
+/// - Either node A or node B can signal that it doesn't want this notifications substream
+///   anymore by closing its writing side. The other party should respond by also closing their
+///   own writing side soon after.
+///
+/// Notification substreams are unidirectional. If A opens a substream with B, then B is
+/// encouraged but not required to open a substream to A as well.
+use crate::types::ProtocolName;
+
+use asynchronous_codec::Framed;
+use bytes::BytesMut;
+use futures::prelude::*;
+use libp2p::core::{upgrade, InboundUpgrade, OutboundUpgrade, UpgradeInfo};
+use log::{error, warn};
+use unsigned_varint::codec::UviBytes;
+
+use std::{
+    convert::Infallible,
+    io, mem,
+    pin::Pin,
+    task::{Context, Poll},
+    vec,
+};
+
+/// Maximum allowed size of the two handshake messages, in bytes.
+const MAX_HANDSHAKE_SIZE: usize = 1024;
+
+/// Upgrade that accepts a substream, sends back a status message, then becomes a unidirectional
+/// stream of messages.
+#[derive(Debug, Clone)]
+pub struct NotificationsIn {
+    /// Protocol name to use when negotiating the substream.
+    /// The first one is the main name, while the other ones are fall backs.
+    protocol_names: Vec<ProtocolName>,
+    /// Maximum allowed size for a single notification.
+    max_notification_size: u64,
+}
+
+/// Upgrade that opens a substream, waits for the remote to accept by sending back a status
+/// message, then becomes a unidirectional sink of data.
+#[derive(Debug, Clone)]
+pub struct NotificationsOut {
+    /// Protocol name to use when negotiating the substream.
+    /// The first one is the main name, while the other ones are fall backs.
+    protocol_names: Vec<ProtocolName>,
+    /// Message to send when we start the handshake.
+    initial_message: Vec<u8>,
+    /// Maximum allowed size for a single notification.
+    max_notification_size: u64,
+}
+
+/// A substream for incoming notification messages.
+///
+/// When creating, this struct starts in a state in which we must first send back a handshake
+/// message to the remote. No message will come before this has been done.
+#[pin_project::pin_project]
+pub struct NotificationsInSubstream<TSubstream> {
+    #[pin]
+    socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
+    handshake: NotificationsInSubstreamHandshake,
+}
+
+/// State of the handshake sending back process.
+#[derive(Debug)]
+pub enum NotificationsInSubstreamHandshake {
+    /// Waiting for the user to give us the handshake message.
+    NotSent,
+    /// User gave us the handshake message. Trying to push it in the socket.
+    PendingSend(Vec<u8>),
+    /// Handshake message was pushed in the socket. Still need to flush.
+    Flush,
+    /// Handshake message successfully sent and flushed.
+    Sent,
+    /// Remote has closed their writing side. We close our own writing side in return.
+    ClosingInResponseToRemote,
+    /// Both our side and the remote have closed their writing side.
+    BothSidesClosed,
+}
+
+/// A substream for outgoing notification messages.
+#[pin_project::pin_project]
+pub struct NotificationsOutSubstream<TSubstream> {
+    /// Substream where to send messages.
+    #[pin]
+    socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
+}
+
+#[cfg(test)]
+impl<TSubstream> NotificationsOutSubstream<TSubstream> {
+    pub fn new(socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>) -> Self {
+        Self { socket }
+    }
+}
+
+impl NotificationsIn {
+    /// Builds a new potential upgrade.
+    pub fn new(
+        main_protocol_name: impl Into<ProtocolName>,
+        fallback_names: Vec<ProtocolName>,
+        max_notification_size: u64,
+    ) -> Self {
+        let mut protocol_names = fallback_names;
+        protocol_names.insert(0, main_protocol_name.into());
+
+        Self {
+            protocol_names,
+            max_notification_size,
+        }
+    }
+}
+
+impl UpgradeInfo for NotificationsIn {
+    type Info = ProtocolName;
+    type InfoIter = vec::IntoIter<Self::Info>;
+
+    fn protocol_info(&self) -> Self::InfoIter {
+        self.protocol_names.clone().into_iter()
+    }
+}
+
+impl<TSubstream> InboundUpgrade<TSubstream> for NotificationsIn
+where
+    TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+{
+    type Output = NotificationsInOpen<TSubstream>;
+    type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
+    type Error = NotificationsHandshakeError;
+
+    fn upgrade_inbound(self, mut socket: TSubstream, negotiated_name: Self::Info) -> Self::Future {
+        Box::pin(async move {
+            let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
+            if handshake_len > MAX_HANDSHAKE_SIZE {
+                return Err(NotificationsHandshakeError::TooLarge {
+                    requested: handshake_len,
+                    max: MAX_HANDSHAKE_SIZE,
+                });
+            }
+
+            let mut handshake = vec![0u8; handshake_len];
+            if !handshake.is_empty() {
+                socket.read_exact(&mut handshake).await?;
+            }
+
+            let mut codec = UviBytes::default();
+            codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
+
+            let substream = NotificationsInSubstream {
+                socket: Framed::new(socket, codec),
+                handshake: NotificationsInSubstreamHandshake::NotSent,
+            };
+
+            Ok(NotificationsInOpen {
+                handshake,
+                negotiated_fallback: if negotiated_name == self.protocol_names[0] {
+                    None
+                } else {
+                    Some(negotiated_name)
+                },
+                substream,
+            })
+        })
+    }
+}
+
+/// Yielded by the [`NotificationsIn`] after a successfuly upgrade.
+pub struct NotificationsInOpen<TSubstream> {
+    /// Handshake sent by the remote.
+    pub handshake: Vec<u8>,
+    /// If the negotiated name is not the "main" protocol name but a fallback, contains the
+    /// name of the negotiated fallback.
+    pub negotiated_fallback: Option<ProtocolName>,
+    /// Implementation of `Stream` that allows receives messages from the substream.
+    pub substream: NotificationsInSubstream<TSubstream>,
+}
+
+impl<TSubstream> NotificationsInSubstream<TSubstream>
+where
+    TSubstream: AsyncRead + AsyncWrite + Unpin,
+{
+    #[cfg(test)]
+    pub fn new(
+        socket: Framed<TSubstream, UviBytes<io::Cursor<Vec<u8>>>>,
+        handshake: NotificationsInSubstreamHandshake,
+    ) -> Self {
+        Self { socket, handshake }
+    }
+
+    /// Sends the handshake in order to inform the remote that we accept the substream.
+    pub fn send_handshake(&mut self, message: impl Into<Vec<u8>>) {
+        if !matches!(self.handshake, NotificationsInSubstreamHandshake::NotSent) {
+            error!(target: "sub-libp2p", "Tried to send handshake twice");
+            return;
+        }
+
+        self.handshake = NotificationsInSubstreamHandshake::PendingSend(message.into());
+    }
+
+    /// Equivalent to `Stream::poll_next`, except that it only drives the handshake and is
+    /// guaranteed to not generate any notification.
+    pub fn poll_process(
+        self: Pin<&mut Self>,
+        cx: &mut Context,
+    ) -> Poll<Result<Infallible, io::Error>> {
+        let mut this = self.project();
+
+        loop {
+            match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
+                NotificationsInSubstreamHandshake::PendingSend(msg) => {
+                    match Sink::poll_ready(this.socket.as_mut(), cx) {
+                        Poll::Ready(_) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Flush;
+                            match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
+                                Ok(()) => {}
+                                Err(err) => return Poll::Ready(Err(err)),
+                            }
+                        }
+                        Poll::Pending => {
+                            *this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
+                            return Poll::Pending;
+                        }
+                    }
+                }
+                NotificationsInSubstreamHandshake::Flush => {
+                    match Sink::poll_flush(this.socket.as_mut(), cx)? {
+                        Poll::Ready(()) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Sent
+                        }
+                        Poll::Pending => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Flush;
+                            return Poll::Pending;
+                        }
+                    }
+                }
+
+                st @ NotificationsInSubstreamHandshake::NotSent
+                | st @ NotificationsInSubstreamHandshake::Sent
+                | st @ NotificationsInSubstreamHandshake::ClosingInResponseToRemote
+                | st @ NotificationsInSubstreamHandshake::BothSidesClosed => {
+                    *this.handshake = st;
+                    return Poll::Pending;
+                }
+            }
+        }
+    }
+}
+
+impl<TSubstream> Stream for NotificationsInSubstream<TSubstream>
+where
+    TSubstream: AsyncRead + AsyncWrite + Unpin,
+{
+    type Item = Result<BytesMut, io::Error>;
+
+    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
+        let mut this = self.project();
+
+        // This `Stream` implementation first tries to send back the handshake if necessary.
+        loop {
+            match mem::replace(this.handshake, NotificationsInSubstreamHandshake::Sent) {
+                NotificationsInSubstreamHandshake::NotSent => {
+                    *this.handshake = NotificationsInSubstreamHandshake::NotSent;
+                    return Poll::Pending;
+                }
+                NotificationsInSubstreamHandshake::PendingSend(msg) => {
+                    match Sink::poll_ready(this.socket.as_mut(), cx) {
+                        Poll::Ready(_) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Flush;
+                            match Sink::start_send(this.socket.as_mut(), io::Cursor::new(msg)) {
+                                Ok(()) => {}
+                                Err(err) => return Poll::Ready(Some(Err(err))),
+                            }
+                        }
+                        Poll::Pending => {
+                            *this.handshake = NotificationsInSubstreamHandshake::PendingSend(msg);
+                            return Poll::Pending;
+                        }
+                    }
+                }
+                NotificationsInSubstreamHandshake::Flush => {
+                    match Sink::poll_flush(this.socket.as_mut(), cx)? {
+                        Poll::Ready(()) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Sent
+                        }
+                        Poll::Pending => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Flush;
+                            return Poll::Pending;
+                        }
+                    }
+                }
+
+                NotificationsInSubstreamHandshake::Sent => {
+                    match Stream::poll_next(this.socket.as_mut(), cx) {
+                        Poll::Ready(None) => {
+                            *this.handshake =
+                                NotificationsInSubstreamHandshake::ClosingInResponseToRemote
+                        }
+                        Poll::Ready(Some(msg)) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Sent;
+                            return Poll::Ready(Some(msg));
+                        }
+                        Poll::Pending => {
+                            *this.handshake = NotificationsInSubstreamHandshake::Sent;
+                            return Poll::Pending;
+                        }
+                    }
+                }
+
+                NotificationsInSubstreamHandshake::ClosingInResponseToRemote => {
+                    match Sink::poll_close(this.socket.as_mut(), cx)? {
+                        Poll::Ready(()) => {
+                            *this.handshake = NotificationsInSubstreamHandshake::BothSidesClosed
+                        }
+                        Poll::Pending => {
+                            *this.handshake =
+                                NotificationsInSubstreamHandshake::ClosingInResponseToRemote;
+                            return Poll::Pending;
+                        }
+                    }
+                }
+
+                NotificationsInSubstreamHandshake::BothSidesClosed => return Poll::Ready(None),
+            }
+        }
+    }
+}
+
+impl NotificationsOut {
+    /// Builds a new potential upgrade.
+    pub fn new(
+        main_protocol_name: impl Into<ProtocolName>,
+        fallback_names: Vec<ProtocolName>,
+        initial_message: impl Into<Vec<u8>>,
+        max_notification_size: u64,
+    ) -> Self {
+        let initial_message = initial_message.into();
+        if initial_message.len() > MAX_HANDSHAKE_SIZE {
+            error!(target: "sub-libp2p", "Outbound networking handshake is above allowed protocol limit");
+        }
+
+        let mut protocol_names = fallback_names;
+        protocol_names.insert(0, main_protocol_name.into());
+
+        Self {
+            protocol_names,
+            initial_message,
+            max_notification_size,
+        }
+    }
+}
+
+impl UpgradeInfo for NotificationsOut {
+    type Info = ProtocolName;
+    type InfoIter = vec::IntoIter<Self::Info>;
+
+    fn protocol_info(&self) -> Self::InfoIter {
+        self.protocol_names.clone().into_iter()
+    }
+}
+
+impl<TSubstream> OutboundUpgrade<TSubstream> for NotificationsOut
+where
+    TSubstream: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+{
+    type Output = NotificationsOutOpen<TSubstream>;
+    type Future = Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send>>;
+    type Error = NotificationsHandshakeError;
+
+    fn upgrade_outbound(self, mut socket: TSubstream, negotiated_name: Self::Info) -> Self::Future {
+        Box::pin(async move {
+            upgrade::write_length_prefixed(&mut socket, &self.initial_message).await?;
+
+            // Reading handshake.
+            let handshake_len = unsigned_varint::aio::read_usize(&mut socket).await?;
+            if handshake_len > MAX_HANDSHAKE_SIZE {
+                return Err(NotificationsHandshakeError::TooLarge {
+                    requested: handshake_len,
+                    max: MAX_HANDSHAKE_SIZE,
+                });
+            }
+
+            let mut handshake = vec![0u8; handshake_len];
+            if !handshake.is_empty() {
+                socket.read_exact(&mut handshake).await?;
+            }
+
+            let mut codec = UviBytes::default();
+            codec.set_max_len(usize::try_from(self.max_notification_size).unwrap_or(usize::MAX));
+
+            Ok(NotificationsOutOpen {
+                handshake,
+                negotiated_fallback: if negotiated_name == self.protocol_names[0] {
+                    None
+                } else {
+                    Some(negotiated_name)
+                },
+                substream: NotificationsOutSubstream {
+                    socket: Framed::new(socket, codec),
+                },
+            })
+        })
+    }
+}
+
+/// Yielded by the [`NotificationsOut`] after a successfuly upgrade.
+pub struct NotificationsOutOpen<TSubstream> {
+    /// Handshake returned by the remote.
+    pub handshake: Vec<u8>,
+    /// If the negotiated name is not the "main" protocol name but a fallback, contains the
+    /// name of the negotiated fallback.
+    pub negotiated_fallback: Option<ProtocolName>,
+    /// Implementation of `Sink` that allows sending messages on the substream.
+    pub substream: NotificationsOutSubstream<TSubstream>,
+}
+
+impl<TSubstream> Sink<Vec<u8>> for NotificationsOutSubstream<TSubstream>
+where
+    TSubstream: AsyncRead + AsyncWrite + Unpin,
+{
+    type Error = NotificationsOutError;
+
+    fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        let mut this = self.project();
+        Sink::poll_ready(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
+    }
+
+    fn start_send(self: Pin<&mut Self>, item: Vec<u8>) -> Result<(), Self::Error> {
+        let mut this = self.project();
+        Sink::start_send(this.socket.as_mut(), io::Cursor::new(item))
+            .map_err(NotificationsOutError::Io)
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        let mut this = self.project();
+        Sink::poll_flush(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
+    }
+
+    fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
+        let mut this = self.project();
+        Sink::poll_close(this.socket.as_mut(), cx).map_err(NotificationsOutError::Io)
+    }
+}
+
+/// Error generated by sending on a notifications out substream.
+#[derive(Debug, thiserror::Error)]
+pub enum NotificationsHandshakeError {
+    /// I/O error on the substream.
+    #[error(transparent)]
+    Io(#[from] io::Error),
+
+    /// Initial message or handshake was too large.
+    #[error("Initial message or handshake was too large: {requested}")]
+    TooLarge {
+        /// Size requested by the remote.
+        requested: usize,
+        /// Maximum allowed,
+        max: usize,
+    },
+
+    /// Error while decoding the variable-length integer.
+    #[error(transparent)]
+    VarintDecode(#[from] unsigned_varint::decode::Error),
+}
+
+impl From<unsigned_varint::io::ReadError> for NotificationsHandshakeError {
+    fn from(err: unsigned_varint::io::ReadError) -> Self {
+        match err {
+            unsigned_varint::io::ReadError::Io(err) => Self::Io(err),
+            unsigned_varint::io::ReadError::Decode(err) => Self::VarintDecode(err),
+            _ => {
+                warn!("Unrecognized varint decoding error");
+                Self::Io(From::from(io::ErrorKind::InvalidData))
+            }
+        }
+    }
+}
+
+/// Error generated by sending on a notifications out substream.
+#[derive(Debug, thiserror::Error)]
+pub enum NotificationsOutError {
+    /// I/O error on the substream.
+    #[error(transparent)]
+    Io(#[from] io::Error),
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{NotificationsIn, NotificationsInOpen, NotificationsOut, NotificationsOutOpen};
+    use futures::{channel::oneshot, prelude::*};
+    use libp2p::core::upgrade;
+    use tokio::net::{TcpListener, TcpStream};
+    use tokio_util::compat::TokioAsyncReadCompatExt;
+
+    #[tokio::test]
+    async fn basic_works() {
+        const PROTO_NAME: &str = "/test/proto/1";
+        let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
+
+        let client = tokio::spawn(async move {
+            let socket = TcpStream::connect(listener_addr_rx.await.unwrap())
+                .await
+                .unwrap();
+            let NotificationsOutOpen {
+                handshake,
+                mut substream,
+                ..
+            } = upgrade::apply_outbound(
+                socket.compat(),
+                NotificationsOut::new(PROTO_NAME, Vec::new(), &b"initial message"[..], 1024 * 1024),
+                upgrade::Version::V1,
+            )
+            .await
+            .unwrap();
+
+            assert_eq!(handshake, b"hello world");
+            substream.send(b"test message".to_vec()).await.unwrap();
+        });
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        listener_addr_tx
+            .send(listener.local_addr().unwrap())
+            .unwrap();
+
+        let (socket, _) = listener.accept().await.unwrap();
+        let NotificationsInOpen {
+            handshake,
+            mut substream,
+            ..
+        } = upgrade::apply_inbound(
+            socket.compat(),
+            NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(handshake, b"initial message");
+        substream.send_handshake(&b"hello world"[..]);
+
+        let msg = substream.next().await.unwrap().unwrap();
+        assert_eq!(msg.as_ref(), b"test message");
+
+        client.await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn empty_handshake() {
+        // Check that everything still works when the handshake messages are empty.
+
+        const PROTO_NAME: &str = "/test/proto/1";
+        let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
+
+        let client = tokio::spawn(async move {
+            let socket = TcpStream::connect(listener_addr_rx.await.unwrap())
+                .await
+                .unwrap();
+            let NotificationsOutOpen {
+                handshake,
+                mut substream,
+                ..
+            } = upgrade::apply_outbound(
+                socket.compat(),
+                NotificationsOut::new(PROTO_NAME, Vec::new(), vec![], 1024 * 1024),
+                upgrade::Version::V1,
+            )
+            .await
+            .unwrap();
+
+            assert!(handshake.is_empty());
+            substream.send(Default::default()).await.unwrap();
+        });
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        listener_addr_tx
+            .send(listener.local_addr().unwrap())
+            .unwrap();
+
+        let (socket, _) = listener.accept().await.unwrap();
+        let NotificationsInOpen {
+            handshake,
+            mut substream,
+            ..
+        } = upgrade::apply_inbound(
+            socket.compat(),
+            NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
+        )
+        .await
+        .unwrap();
+
+        assert!(handshake.is_empty());
+        substream.send_handshake(vec![]);
+
+        let msg = substream.next().await.unwrap().unwrap();
+        assert!(msg.as_ref().is_empty());
+
+        client.await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn refused() {
+        const PROTO_NAME: &str = "/test/proto/1";
+        let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
+
+        let client = tokio::spawn(async move {
+            let socket = TcpStream::connect(listener_addr_rx.await.unwrap())
+                .await
+                .unwrap();
+            let outcome = upgrade::apply_outbound(
+                socket.compat(),
+                NotificationsOut::new(PROTO_NAME, Vec::new(), &b"hello"[..], 1024 * 1024),
+                upgrade::Version::V1,
+            )
+            .await;
+
+            // Despite the protocol negotiation being successfully conducted on the listener
+            // side, we have to receive an error here because the listener didn't send the
+            // handshake.
+            assert!(outcome.is_err());
+        });
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        listener_addr_tx
+            .send(listener.local_addr().unwrap())
+            .unwrap();
+
+        let (socket, _) = listener.accept().await.unwrap();
+        let NotificationsInOpen {
+            handshake,
+            substream,
+            ..
+        } = upgrade::apply_inbound(
+            socket.compat(),
+            NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(handshake, b"hello");
+
+        // We successfully upgrade to the protocol, but then close the substream.
+        drop(substream);
+
+        client.await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn large_initial_message_refused() {
+        const PROTO_NAME: &str = "/test/proto/1";
+        let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
+
+        let client = tokio::spawn(async move {
+            let socket = TcpStream::connect(listener_addr_rx.await.unwrap())
+                .await
+                .unwrap();
+            let ret = upgrade::apply_outbound(
+                socket.compat(),
+                // We check that an initial message that is too large gets refused.
+                NotificationsOut::new(
+                    PROTO_NAME,
+                    Vec::new(),
+                    (0..32768).map(|_| 0).collect::<Vec<_>>(),
+                    1024 * 1024,
+                ),
+                upgrade::Version::V1,
+            )
+            .await;
+            assert!(ret.is_err());
+        });
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        listener_addr_tx
+            .send(listener.local_addr().unwrap())
+            .unwrap();
+
+        let (socket, _) = listener.accept().await.unwrap();
+        let ret = upgrade::apply_inbound(
+            socket.compat(),
+            NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
+        )
+        .await;
+        assert!(ret.is_err());
+
+        client.await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn large_handshake_refused() {
+        const PROTO_NAME: &str = "/test/proto/1";
+        let (listener_addr_tx, listener_addr_rx) = oneshot::channel();
+
+        let client = tokio::spawn(async move {
+            let socket = TcpStream::connect(listener_addr_rx.await.unwrap())
+                .await
+                .unwrap();
+            let ret = upgrade::apply_outbound(
+                socket.compat(),
+                NotificationsOut::new(PROTO_NAME, Vec::new(), &b"initial message"[..], 1024 * 1024),
+                upgrade::Version::V1,
+            )
+            .await;
+            assert!(ret.is_err());
+        });
+
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        listener_addr_tx
+            .send(listener.local_addr().unwrap())
+            .unwrap();
+
+        let (socket, _) = listener.accept().await.unwrap();
+        let NotificationsInOpen {
+            handshake,
+            mut substream,
+            ..
+        } = upgrade::apply_inbound(
+            socket.compat(),
+            NotificationsIn::new(PROTO_NAME, Vec::new(), 1024 * 1024),
+        )
+        .await
+        .unwrap();
+        assert_eq!(handshake, b"initial message");
+
+        // We check that a handshake that is too large gets refused.
+        substream.send_handshake((0..32768).map(|_| 0).collect::<Vec<_>>());
+        let _ = substream.next().await;
+
+        client.await.unwrap();
+    }
+}
diff --git a/libp2p/src/protocol_controller.rs b/libp2p/src/protocol_controller.rs
new file mode 100644
index 000000000..5123390ee
--- /dev/null
+++ b/libp2p/src/protocol_controller.rs
@@ -0,0 +1,2392 @@
+// 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/>.
+
+//! Protocol Controller. Generic implementation of peer management for protocols.
+//! Responsible for accepting/rejecting incoming connections and initiating outgoing connections,
+//! respecting the inbound and outbound peer slot counts. Communicates with `PeerStore` to get and
+//! update peer reputation values and sends commands to `Notifications`.
+//!
+//! Due to asynchronous nature of communication between `ProtocolController` and `Notifications`,
+//! `ProtocolController` has an imperfect view of the states of the peers. To reduce this
+//! desynchronization, the following measures are taken:
+//!
+//! 1. Network peer events from `Notifications` are prioritized over actions from external API and
+//!    internal actions by `ProtocolController` (like slot allocation).
+//! 2. `Notifications` ignores all commands from `ProtocolController` after sending "incoming"
+//!    request until receiving the answer to this "incoming" request.
+//! 3. After sending a "connect" message, `ProtocolController` switches the state of the peer from
+//!    `Outbound` to `Inbound` if it receives an "incoming" request from `Notifications` for this
+//!    peer.
+//!
+//! These measures do not eliminate confusing commands from `ProtocolController` completely,
+//! so `Notifications` must correctly handle seemingly inconsistent commands, like a "connect"
+//! command for the peer it thinks is already connected, and a "drop" command for a peer that
+//! was previously dropped.
+//!
+//! Even though this does not guarantee that `ProtocolController` and `Notifications` have the same
+//! view of the peers' states at any given moment, the eventual consistency is maintained.
+
+use futures::{channel::oneshot, future::Either, FutureExt, StreamExt};
+use libp2p::PeerId;
+use log::{debug, error, trace, warn};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+use sp_arithmetic::traits::SaturatedConversion;
+use std::{
+    collections::{HashMap, HashSet},
+    time::{Duration, Instant},
+};
+use wasm_timer::Delay;
+
+use crate::peer_store::PeerStoreProvider;
+
+/// Log target for this file.
+pub const LOG_TARGET: &str = "peerset";
+
+/// `Notifications` protocol index. For historical reasons it's called `SetId`, because it
+/// used to refer to a set of peers in a peerset for this protocol.
+///
+/// Can be constructed using the `From<usize>` trait implementation based on the index of the
+/// protocol in `Notifications`.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct SetId(usize);
+
+impl SetId {
+    /// Const conversion function for initialization of hardcoded peerset indices.
+    pub const fn from(id: usize) -> Self {
+        Self(id)
+    }
+}
+
+impl From<usize> for SetId {
+    fn from(id: usize) -> Self {
+        Self(id)
+    }
+}
+
+impl From<SetId> for usize {
+    fn from(id: SetId) -> Self {
+        id.0
+    }
+}
+
+/// Configuration for a set of nodes for a specific protocol.
+#[derive(Debug)]
+pub struct ProtoSetConfig {
+    /// Maximum number of incoming links to peers.
+    pub in_peers: u32,
+
+    /// Maximum number of outgoing links to peers.
+    pub out_peers: u32,
+
+    /// Lists of nodes we should always be connected to.
+    ///
+    /// > **Note**: Keep in mind that the networking has to know an address for these nodes,
+    /// >           otherwise it will not be able to connect to them.
+    pub reserved_nodes: HashSet<PeerId>,
+
+    /// If true, we only accept nodes in [`ProtoSetConfig::reserved_nodes`].
+    pub reserved_only: bool,
+}
+
+/// Message that is sent by [`ProtocolController`] to `Notifications`.
+#[derive(Debug, PartialEq)]
+pub enum Message {
+    /// Request to open a connection to the given peer. From the point of view of the
+    /// `ProtocolController`, we are immediately connected.
+    Connect {
+        /// Set id to connect on.
+        set_id: SetId,
+        /// Peer to connect to.
+        peer_id: PeerId,
+    },
+
+    /// Drop the connection to the given peer, or cancel the connection attempt after a `Connect`.
+    Drop {
+        /// Set id to disconnect on.
+        set_id: SetId,
+        /// Peer to disconnect from.
+        peer_id: PeerId,
+    },
+
+    /// Equivalent to `Connect` for the peer corresponding to this incoming index.
+    Accept(IncomingIndex),
+
+    /// Equivalent to `Drop` for the peer corresponding to this incoming index.
+    Reject(IncomingIndex),
+}
+
+/// Opaque identifier for an incoming connection. Allocated by the network.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct IncomingIndex(pub u64);
+
+impl From<u64> for IncomingIndex {
+    fn from(val: u64) -> Self {
+        Self(val)
+    }
+}
+
+/// External API actions.
+#[derive(Debug)]
+enum Action {
+    /// Add a reserved peer or mark already connected peer as reserved.
+    AddReservedPeer(PeerId),
+    /// Remove a reserved peer.
+    RemoveReservedPeer(PeerId),
+    /// Update reserved peers to match the provided set.
+    SetReservedPeers(HashSet<PeerId>),
+    /// Set/unset reserved-only mode.
+    SetReservedOnly(bool),
+    /// Disconnect a peer.
+    DisconnectPeer(PeerId),
+    /// Get the list of reserved peers.
+    GetReservedPeers(oneshot::Sender<Vec<PeerId>>),
+}
+
+/// Network events from `Notifications`.
+#[derive(Debug)]
+enum Event {
+    /// Incoming connection from the peer.
+    IncomingConnection(PeerId, IncomingIndex),
+    /// Connection with the peer dropped.
+    Dropped(PeerId),
+}
+
+/// Shared handle to [`ProtocolController`]. Distributed around the code outside of the
+/// protocol implementation.
+#[derive(Debug, Clone)]
+pub struct ProtocolHandle {
+    /// Actions from outer API.
+    actions_tx: TracingUnboundedSender<Action>,
+    /// Connection events from `Notifications`. We prioritize them over actions.
+    events_tx: TracingUnboundedSender<Event>,
+}
+
+impl ProtocolHandle {
+    /// Adds a new reserved peer. [`ProtocolController`] will make an effort
+    /// to always remain connected to this peer.
+    ///
+    /// Has no effect if the node was already a reserved peer.
+    ///
+    /// > **Note**: Keep in mind that the networking has to know an address for this node,
+    /// > otherwise it will not be able to connect to it.
+    pub fn add_reserved_peer(&self, peer_id: PeerId) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::AddReservedPeer(peer_id));
+    }
+
+    /// Demotes reserved peer to non-reserved. Does not disconnect the peer.
+    ///
+    /// Has no effect if the node was not a reserved peer.
+    pub fn remove_reserved_peer(&self, peer_id: PeerId) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::RemoveReservedPeer(peer_id));
+    }
+
+    /// Set reserved peers to the new set.
+    pub fn set_reserved_peers(&self, peer_ids: HashSet<PeerId>) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::SetReservedPeers(peer_ids));
+    }
+
+    /// Sets whether or not [`ProtocolController`] only has connections with nodes marked
+    /// as reserved for the given set.
+    pub fn set_reserved_only(&self, reserved: bool) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::SetReservedOnly(reserved));
+    }
+
+    /// Disconnect peer. You should remove the `PeerId` from the `PeerStore` first
+    /// to not connect to the peer again during the next slot allocation.
+    pub fn disconnect_peer(&self, peer_id: PeerId) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::DisconnectPeer(peer_id));
+    }
+
+    /// Get the list of reserved peers.
+    pub fn reserved_peers(&self, pending_response: oneshot::Sender<Vec<PeerId>>) {
+        let _ = self
+            .actions_tx
+            .unbounded_send(Action::GetReservedPeers(pending_response));
+    }
+
+    /// Notify about incoming connection. [`ProtocolController`] will either accept or reject it.
+    pub fn incoming_connection(&self, peer_id: PeerId, incoming_index: IncomingIndex) {
+        let _ = self
+            .events_tx
+            .unbounded_send(Event::IncomingConnection(peer_id, incoming_index));
+    }
+
+    /// Notify that connection was dropped (either refused or disconnected).
+    pub fn dropped(&self, peer_id: PeerId) {
+        let _ = self.events_tx.unbounded_send(Event::Dropped(peer_id));
+    }
+}
+
+/// Direction of a connection
+#[derive(Clone, Copy, Debug)]
+enum Direction {
+    Inbound,
+    Outbound,
+}
+
+/// Status of a connection with a peer.
+#[derive(Clone, Debug, Default)]
+enum PeerState {
+    /// We are connected to the peer.
+    Connected(Direction),
+    /// We are not connected.
+    #[default]
+    NotConnected,
+}
+
+impl PeerState {
+    /// Returns true if we are connected with the node.
+    fn is_connected(&self) -> bool {
+        matches!(self, PeerState::Connected(_))
+    }
+}
+
+/// Worker side of [`ProtocolHandle`] responsible for all the logic.
+#[derive(Debug)]
+pub struct ProtocolController {
+    /// Set id to use when sending connect/drop requests to `Notifications`.
+    // Will likely be replaced by `ProtocolName` in the future.
+    set_id: SetId,
+    /// Receiver for outer API messages from [`ProtocolHandle`].
+    actions_rx: TracingUnboundedReceiver<Action>,
+    /// Receiver for connection events from `Notifications` sent via [`ProtocolHandle`].
+    events_rx: TracingUnboundedReceiver<Event>,
+    /// Number of occupied slots for incoming connections (not counting reserved nodes).
+    num_in: u32,
+    /// Number of occupied slots for outgoing connections (not counting reserved nodes).
+    num_out: u32,
+    /// Maximum number of slots for incoming connections (not counting reserved nodes).
+    max_in: u32,
+    /// Maximum number of slots for outgoing connections (not counting reserved nodes).
+    max_out: u32,
+    /// Connected regular nodes.
+    nodes: HashMap<PeerId, Direction>,
+    /// Reserved nodes. Should be always connected and do not occupy peer slots.
+    reserved_nodes: HashMap<PeerId, PeerState>,
+    /// Connect only to reserved nodes.
+    reserved_only: bool,
+    /// Next time to allocate slots. This is done once per second.
+    next_periodic_alloc_slots: Instant,
+    /// Outgoing channel for messages to `Notifications`.
+    to_notifications: TracingUnboundedSender<Message>,
+    /// `PeerStore` handle for checking peer reputation values and getting connection candidates
+    /// with highest reputation.
+    peer_store: Box<dyn PeerStoreProvider>,
+}
+
+impl ProtocolController {
+    /// Construct new [`ProtocolController`].
+    pub fn new(
+        set_id: SetId,
+        config: ProtoSetConfig,
+        to_notifications: TracingUnboundedSender<Message>,
+        peer_store: Box<dyn PeerStoreProvider>,
+    ) -> (ProtocolHandle, ProtocolController) {
+        let (actions_tx, actions_rx) = tracing_unbounded("mpsc_api_protocol", 10_000);
+        let (events_tx, events_rx) = tracing_unbounded("mpsc_notifications_protocol", 10_000);
+        let handle = ProtocolHandle {
+            actions_tx,
+            events_tx,
+        };
+        peer_store.register_protocol(handle.clone());
+        let reserved_nodes = config
+            .reserved_nodes
+            .iter()
+            .map(|p| (*p, PeerState::NotConnected))
+            .collect();
+        let controller = ProtocolController {
+            set_id,
+            actions_rx,
+            events_rx,
+            num_in: 0,
+            num_out: 0,
+            max_in: config.in_peers,
+            max_out: config.out_peers,
+            nodes: HashMap::new(),
+            reserved_nodes,
+            reserved_only: config.reserved_only,
+            next_periodic_alloc_slots: Instant::now(),
+            to_notifications,
+            peer_store,
+        };
+        (handle, controller)
+    }
+
+    /// Drive [`ProtocolController`]. This function returns when all instances of
+    /// [`ProtocolHandle`] are dropped.
+    pub async fn run(mut self) {
+        while self.next_action().await {}
+    }
+
+    /// Perform one action. Returns `true` if it should be called again.
+    ///
+    /// Intended for tests only. Use `run` for driving [`ProtocolController`].
+    pub async fn next_action(&mut self) -> bool {
+        let either = loop {
+            let mut next_alloc_slots = Delay::new_at(self.next_periodic_alloc_slots).fuse();
+
+            // See the module doc for why we use `select_biased!`.
+            futures::select_biased! {
+                event = self.events_rx.next() => match event {
+                    Some(event) => break Either::Left(event),
+                    None => return false,
+                },
+                action = self.actions_rx.next() => match action {
+                    Some(action) => break Either::Right(action),
+                    None => return false,
+                },
+                _ = next_alloc_slots => {
+                    self.alloc_slots();
+                    self.next_periodic_alloc_slots = Instant::now() + Duration::new(1, 0);
+                },
+            }
+        };
+
+        match either {
+            Either::Left(event) => self.process_event(event),
+            Either::Right(action) => self.process_action(action),
+        }
+
+        true
+    }
+
+    /// Process connection event.
+    fn process_event(&mut self, event: Event) {
+        match event {
+            Event::IncomingConnection(peer_id, index) => {
+                self.on_incoming_connection(peer_id, index)
+            }
+            Event::Dropped(peer_id) => self.on_peer_dropped(peer_id),
+        }
+    }
+
+    /// Process action command.
+    fn process_action(&mut self, action: Action) {
+        match action {
+            Action::AddReservedPeer(peer_id) => self.on_add_reserved_peer(peer_id),
+            Action::RemoveReservedPeer(peer_id) => self.on_remove_reserved_peer(peer_id),
+            Action::SetReservedPeers(peer_ids) => self.on_set_reserved_peers(peer_ids),
+            Action::SetReservedOnly(reserved_only) => self.on_set_reserved_only(reserved_only),
+            Action::DisconnectPeer(peer_id) => self.on_disconnect_peer(peer_id),
+            Action::GetReservedPeers(pending_response) => {
+                self.on_get_reserved_peers(pending_response)
+            }
+        }
+    }
+
+    /// Send "accept" message to `Notifications`.
+    fn accept_connection(&mut self, peer_id: PeerId, incoming_index: IncomingIndex) {
+        trace!(
+            target: LOG_TARGET,
+            "Accepting {peer_id} ({incoming_index:?}) on {:?} ({}/{} num_in/max_in).",
+            self.set_id,
+            self.num_in,
+            self.max_in,
+        );
+
+        let _ = self
+            .to_notifications
+            .unbounded_send(Message::Accept(incoming_index));
+    }
+
+    /// Send "reject" message to `Notifications`.
+    fn reject_connection(&mut self, peer_id: PeerId, incoming_index: IncomingIndex) {
+        trace!(
+            target: LOG_TARGET,
+            "Rejecting {peer_id} ({incoming_index:?}) on {:?} ({}/{} num_in/max_in).",
+            self.set_id,
+            self.num_in,
+            self.max_in,
+        );
+
+        let _ = self
+            .to_notifications
+            .unbounded_send(Message::Reject(incoming_index));
+    }
+
+    /// Send "connect" message to `Notifications`.
+    fn start_connection(&mut self, peer_id: PeerId) {
+        trace!(
+            target: LOG_TARGET,
+            "Connecting to {peer_id} on {:?} ({}/{} num_out/max_out).",
+            self.set_id,
+            self.num_out,
+            self.max_out,
+        );
+
+        let _ = self.to_notifications.unbounded_send(Message::Connect {
+            set_id: self.set_id,
+            peer_id,
+        });
+    }
+
+    /// Send "drop" message to `Notifications`.
+    fn drop_connection(&mut self, peer_id: PeerId) {
+        trace!(
+            target: LOG_TARGET,
+            "Dropping {peer_id} on {:?} ({}/{} num_in/max_in, {}/{} num_out/max_out).",
+            self.set_id,
+            self.num_in,
+            self.max_in,
+            self.num_out,
+            self.max_out,
+        );
+
+        let _ = self.to_notifications.unbounded_send(Message::Drop {
+            set_id: self.set_id,
+            peer_id,
+        });
+    }
+
+    /// Report peer disconnect event to `PeerStore` for it to update peer's reputation accordingly.
+    /// Should only be called if the remote node disconnected us, not the other way around.
+    fn report_disconnect(&mut self, peer_id: PeerId) {
+        self.peer_store.report_disconnect(peer_id);
+    }
+
+    /// Ask `Peerset` if the peer has a reputation value not sufficent for connection with it.
+    fn is_banned(&self, peer_id: &PeerId) -> bool {
+        self.peer_store.is_banned(peer_id)
+    }
+
+    /// Add the peer to the set of reserved peers. [`ProtocolController`] will try to always
+    /// maintain connections with such peers.
+    fn on_add_reserved_peer(&mut self, peer_id: PeerId) {
+        if self.reserved_nodes.contains_key(&peer_id) {
+            warn!(
+                target: LOG_TARGET,
+                "Trying to add an already reserved node {peer_id} as reserved on {:?}.",
+                self.set_id,
+            );
+            return;
+        }
+
+        // Get the peer out of non-reserved peers if it's there.
+        let state = match self.nodes.remove(&peer_id) {
+            Some(direction) => {
+                trace!(
+                    target: LOG_TARGET,
+                    "Marking previously connected node {} ({:?}) as reserved on {:?}.",
+                    peer_id,
+                    direction,
+                    self.set_id
+                );
+                PeerState::Connected(direction)
+            }
+            None => {
+                trace!(target: LOG_TARGET, "Adding reserved node {peer_id} on {:?}.", self.set_id,);
+                PeerState::NotConnected
+            }
+        };
+
+        self.reserved_nodes.insert(peer_id, state.clone());
+
+        // Discount occupied slots or connect to the node.
+        match state {
+            PeerState::Connected(Direction::Inbound) => self.num_in -= 1,
+            PeerState::Connected(Direction::Outbound) => self.num_out -= 1,
+            PeerState::NotConnected => self.alloc_slots(),
+        }
+    }
+
+    /// Remove the peer from the set of reserved peers. The peer is either moved to the set of
+    /// regular nodes or disconnected.
+    fn on_remove_reserved_peer(&mut self, peer_id: PeerId) {
+        let state = match self.reserved_nodes.remove(&peer_id) {
+            Some(state) => state,
+            None => {
+                warn!(
+                    target: LOG_TARGET,
+                    "Trying to remove unknown reserved node {peer_id} from {:?}.", self.set_id,
+                );
+                return;
+            }
+        };
+
+        if let PeerState::Connected(direction) = state {
+            // Disconnect if we're at (or over) the regular node limit
+            let disconnect = self.reserved_only
+                || match direction {
+                    Direction::Inbound => self.num_in >= self.max_in,
+                    Direction::Outbound => self.num_out >= self.max_out,
+                };
+
+            if disconnect {
+                // Disconnect the node.
+                trace!(
+                    target: LOG_TARGET,
+                    "Disconnecting previously reserved node {peer_id} ({direction:?}) on {:?}.",
+                    self.set_id,
+                );
+                self.drop_connection(peer_id);
+            } else {
+                // Count connections as of regular node.
+                trace!(
+                    target: LOG_TARGET,
+                    "Making a connected reserved node {peer_id} ({:?}) on {:?} a regular one.",
+                    direction,
+                    self.set_id,
+                );
+
+                match direction {
+                    Direction::Inbound => self.num_in += 1,
+                    Direction::Outbound => self.num_out += 1,
+                }
+
+                // Put the node into the list of regular nodes.
+                let prev = self.nodes.insert(peer_id, direction);
+                assert!(
+                    prev.is_none(),
+                    "Corrupted state: reserved node was also non-reserved."
+                );
+            }
+        } else {
+            trace!(
+                target: LOG_TARGET,
+                "Removed disconnected reserved node {peer_id} from {:?}.",
+                self.set_id,
+            );
+        }
+    }
+
+    /// Replace the set of reserved peers.
+    fn on_set_reserved_peers(&mut self, peer_ids: HashSet<PeerId>) {
+        // Determine the difference between the current group and the new list.
+        let current = self.reserved_nodes.keys().cloned().collect();
+        let to_insert = peer_ids.difference(&current).cloned().collect::<Vec<_>>();
+        let to_remove = current.difference(&peer_ids).cloned().collect::<Vec<_>>();
+
+        for node in to_insert {
+            self.on_add_reserved_peer(node);
+        }
+
+        for node in to_remove {
+            self.on_remove_reserved_peer(node);
+        }
+    }
+
+    /// Change "reserved only" flag. In "reserved only" mode we connect and accept connections to
+    /// reserved nodes only.
+    fn on_set_reserved_only(&mut self, reserved_only: bool) {
+        trace!(target: LOG_TARGET, "Set reserved only to `{reserved_only}` on {:?}", self.set_id);
+
+        self.reserved_only = reserved_only;
+
+        if !reserved_only {
+            return self.alloc_slots();
+        }
+
+        // Disconnect all non-reserved peers.
+        self.nodes
+            .iter()
+            .map(|(k, v)| (*k, *v))
+            .collect::<Vec<(_, _)>>()
+            .iter()
+            .for_each(|(peer_id, direction)| {
+                // Update counters in the loop for `drop_connection` to report the correct number.
+                match direction {
+                    Direction::Inbound => self.num_in -= 1,
+                    Direction::Outbound => self.num_out -= 1,
+                }
+                self.drop_connection(*peer_id)
+            });
+        self.nodes.clear();
+    }
+
+    /// Get the list of reserved peers.
+    fn on_get_reserved_peers(&self, pending_response: oneshot::Sender<Vec<PeerId>>) {
+        let _ = pending_response.send(self.reserved_nodes.keys().cloned().collect());
+    }
+
+    /// Disconnect the peer.
+    fn on_disconnect_peer(&mut self, peer_id: PeerId) {
+        // Don't do anything if the node is reserved.
+        if self.reserved_nodes.contains_key(&peer_id) {
+            debug!(
+                target: LOG_TARGET,
+                "Ignoring request to disconnect reserved peer {peer_id} from {:?}.", self.set_id,
+            );
+            return;
+        }
+
+        match self.nodes.remove(&peer_id) {
+            Some(direction) => {
+                trace!(
+                    target: LOG_TARGET,
+                    "Disconnecting peer {peer_id} ({direction:?}) from {:?}.",
+                    self.set_id
+                );
+                match direction {
+                    Direction::Inbound => self.num_in -= 1,
+                    Direction::Outbound => self.num_out -= 1,
+                }
+                self.drop_connection(peer_id);
+            }
+            None => {
+                debug!(
+                    target: LOG_TARGET,
+                    "Trying to disconnect unknown peer {peer_id} from {:?}.", self.set_id,
+                );
+            }
+        }
+    }
+
+    /// Indicate that we received an incoming connection. Must be answered either with
+    /// a corresponding `Accept` or `Reject`, except if we were already connected to this peer.
+    ///
+    /// Note that this mechanism is orthogonal to `Connect`/`Drop`. Accepting an incoming
+    /// connection implicitly means `Connect`, but incoming connections aren't cancelled by
+    /// `dropped`.
+    // Implementation note: because of concurrency issues, `ProtocolController` has an imperfect
+    // view of the peers' states, and may issue commands for a peer after `Notifications` received
+    // an incoming request for that peer. In this case, `Notifications` ignores all the commands
+    // until it receives a response for the incoming request to `ProtocolController`, so we must
+    // ensure we handle this incoming request correctly.
+    fn on_incoming_connection(&mut self, peer_id: PeerId, incoming_index: IncomingIndex) {
+        trace!(
+            target: LOG_TARGET,
+            "Incoming connection from peer {peer_id} ({incoming_index:?}) on {:?}.",
+            self.set_id,
+        );
+
+        if self.reserved_only && !self.reserved_nodes.contains_key(&peer_id) {
+            self.reject_connection(peer_id, incoming_index);
+            return;
+        }
+
+        // Check if the node is reserved first.
+        if let Some(state) = self.reserved_nodes.get_mut(&peer_id) {
+            match state {
+                PeerState::Connected(ref mut direction) => {
+                    // We are accepting an incoming connection, so ensure the direction is inbound.
+                    // (See the implementation note above.)
+                    *direction = Direction::Inbound;
+                    self.accept_connection(peer_id, incoming_index);
+                }
+                PeerState::NotConnected => {
+                    if self.peer_store.is_banned(&peer_id) {
+                        self.reject_connection(peer_id, incoming_index);
+                    } else {
+                        *state = PeerState::Connected(Direction::Inbound);
+                        self.accept_connection(peer_id, incoming_index);
+                    }
+                }
+            }
+            return;
+        }
+
+        // If we're already connected, pretend we are not connected and decide on the node again.
+        // (See the note above.)
+        if let Some(direction) = self.nodes.remove(&peer_id) {
+            trace!(
+                target: LOG_TARGET,
+                "Handling incoming connection from peer {} we think we already connected as {:?} on {:?}.",
+                peer_id,
+                direction,
+                self.set_id
+            );
+            match direction {
+                Direction::Inbound => self.num_in -= 1,
+                Direction::Outbound => self.num_out -= 1,
+            }
+        }
+
+        if self.num_in >= self.max_in {
+            self.reject_connection(peer_id, incoming_index);
+            return;
+        }
+
+        if self.is_banned(&peer_id) {
+            self.reject_connection(peer_id, incoming_index);
+            return;
+        }
+
+        self.num_in += 1;
+        self.nodes.insert(peer_id, Direction::Inbound);
+        self.accept_connection(peer_id, incoming_index);
+    }
+
+    /// Indicate that a connection with the peer was dropped.
+    fn on_peer_dropped(&mut self, peer_id: PeerId) {
+        self.on_peer_dropped_inner(peer_id)
+            .unwrap_or_else(|peer_id| {
+                // We do not assert here, because due to asynchronous nature of communication
+                // between `ProtocolController` and `Notifications` we can receive `Action::Dropped`
+                // for a peer we already disconnected ourself.
+                trace!(
+                    target: LOG_TARGET,
+                    "Received `Action::Dropped` for not connected peer {peer_id} on {:?}.",
+                    self.set_id,
+                )
+            });
+    }
+
+    /// Indicate that a connection with the peer was dropped.
+    /// Returns `Err(PeerId)` if the peer wasn't connected or is not known to us.
+    fn on_peer_dropped_inner(&mut self, peer_id: PeerId) -> Result<(), PeerId> {
+        if self.drop_reserved_peer(&peer_id)? || self.drop_regular_peer(&peer_id) {
+            // The peer found and disconnected.
+            self.report_disconnect(peer_id);
+            Ok(())
+        } else {
+            // The peer was not found in neither regular or reserved lists.
+            Err(peer_id)
+        }
+    }
+
+    /// Try dropping the peer as a reserved peer. Return `Ok(true)` if the peer was found and
+    /// disconnected, `Ok(false)` if it wasn't found, `Err(PeerId)`, if the peer found, but not in
+    /// connected state.
+    fn drop_reserved_peer(&mut self, peer_id: &PeerId) -> Result<bool, PeerId> {
+        let Some(state) = self.reserved_nodes.get_mut(peer_id) else {
+            return Ok(false);
+        };
+
+        if let PeerState::Connected(direction) = state {
+            trace!(
+                target: LOG_TARGET,
+                "Reserved peer {peer_id} ({direction:?}) dropped from {:?}.",
+                self.set_id,
+            );
+            *state = PeerState::NotConnected;
+            Ok(true)
+        } else {
+            Err(*peer_id)
+        }
+    }
+
+    /// Try dropping the peer as a regular peer. Return `true` if the peer was found and
+    /// disconnected, `false` if it wasn't found.
+    fn drop_regular_peer(&mut self, peer_id: &PeerId) -> bool {
+        let Some(direction) = self.nodes.remove(peer_id) else {
+            return false;
+        };
+
+        trace!(
+            target: LOG_TARGET,
+            "Peer {peer_id} ({direction:?}) dropped from {:?}.",
+            self.set_id,
+        );
+
+        match direction {
+            Direction::Inbound => self.num_in -= 1,
+            Direction::Outbound => self.num_out -= 1,
+        }
+
+        true
+    }
+
+    /// Initiate outgoing connections trying to connect all reserved nodes and fill in all outgoing
+    /// slots.
+    fn alloc_slots(&mut self) {
+        // Try connecting to reserved nodes first, ignoring nodes with outstanding events/actions.
+        self.reserved_nodes
+            .iter_mut()
+            .filter_map(|(peer_id, state)| {
+                (!state.is_connected() && !self.peer_store.is_banned(peer_id)).then(|| {
+                    *state = PeerState::Connected(Direction::Outbound);
+                    peer_id
+                })
+            })
+            .cloned()
+            .collect::<Vec<_>>()
+            .into_iter()
+            .for_each(|peer_id| {
+                self.start_connection(peer_id);
+            });
+
+        // Nothing more to do if we're in reserved-only mode or don't have slots available.
+        if self.reserved_only || self.num_out >= self.max_out {
+            return;
+        }
+
+        // Fill available slots.
+        let available_slots = (self.max_out - self.num_out).saturated_into();
+
+        // Ignore reserved nodes (connected above), already connected nodes, and nodes with
+        // outstanding events/actions.
+        let ignored = self
+            .reserved_nodes
+            .keys()
+            .collect::<HashSet<&PeerId>>()
+            .union(&self.nodes.keys().collect::<HashSet<&PeerId>>())
+            .cloned()
+            .collect();
+
+        let candidates = self
+            .peer_store
+            .outgoing_candidates(available_slots, ignored)
+            .into_iter()
+            .filter_map(|peer_id| {
+                (!self.reserved_nodes.contains_key(&peer_id) && !self.nodes.contains_key(&peer_id))
+                    .then_some(peer_id)
+                    .or_else(|| {
+                        error!(
+                            target: LOG_TARGET,
+                            "`PeerStore` returned a node we asked to ignore: {peer_id}.",
+                        );
+                        debug_assert!(false, "`PeerStore` returned a node we asked to ignore.");
+                        None
+                    })
+            })
+            .collect::<Vec<_>>();
+
+        if candidates.len() > available_slots {
+            error!(
+                target: LOG_TARGET,
+                "`PeerStore` returned more nodes than there are slots available.",
+            );
+            debug_assert!(
+                false,
+                "`PeerStore` returned more nodes than there are slots available."
+            );
+        }
+
+        candidates
+            .into_iter()
+            .take(available_slots)
+            .for_each(|peer_id| {
+                self.num_out += 1;
+                self.nodes.insert(peer_id, Direction::Outbound);
+                self.start_connection(peer_id);
+            })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{peer_store::PeerStoreProvider, ReputationChange};
+    use libp2p::PeerId;
+    use sc_network_common::role::ObservedRole;
+    use sc_utils::mpsc::{tracing_unbounded, TryRecvError};
+    use std::collections::HashSet;
+
+    mockall::mock! {
+        #[derive(Debug)]
+        pub PeerStoreHandle {}
+
+        impl PeerStoreProvider for PeerStoreHandle {
+            fn is_banned(&self, peer_id: &PeerId) -> bool;
+            fn register_protocol(&self, protocol_handle: ProtocolHandle);
+            fn report_disconnect(&mut self, peer_id: PeerId);
+            fn set_peer_role(&mut self, peer_id: &PeerId, role: ObservedRole);
+            fn report_peer(&mut self, peer_id: PeerId, change: ReputationChange);
+            fn peer_reputation(&self, peer_id: &PeerId) -> i32;
+            fn peer_role(&self, peer_id: &PeerId) -> Option<ObservedRole>;
+            fn outgoing_candidates<'a>(&self, count: usize, ignored: HashSet<&'a PeerId>) -> Vec<PeerId>;
+        }
+    }
+
+    #[test]
+    fn reserved_nodes_are_connected_dropped_and_accepted() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        // Add first reserved node via config.
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            out_peers: 0,
+            reserved_nodes: std::iter::once(reserved1).collect(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(4).return_const(false);
+        peer_store
+            .expect_report_disconnect()
+            .times(2)
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Add second reserved node at runtime (this currently calls `alloc_slots` internally).
+        controller.on_add_reserved_peer(reserved2);
+
+        // Initiate connections (currently, `alloc_slots` is also called internally in
+        // `on_add_reserved_peer` above).
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+
+        // Reserved peers do not occupy slots.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Drop connections to be able to accept reserved nodes.
+        controller.on_peer_dropped(reserved1);
+        controller.on_peer_dropped(reserved2);
+
+        // Incoming connection from `reserved1`.
+        let incoming1 = IncomingIndex(1);
+        controller.on_incoming_connection(reserved1, incoming1);
+        assert_eq!(rx.try_recv().unwrap(), Message::Accept(incoming1));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Incoming connection from `reserved2`.
+        let incoming2 = IncomingIndex(2);
+        controller.on_incoming_connection(reserved2, incoming2);
+        assert_eq!(rx.try_recv().unwrap(), Message::Accept(incoming2));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Reserved peers do not occupy slots.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn banned_reserved_nodes_are_not_connected_and_not_accepted() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        // Add first reserved node via config.
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            out_peers: 0,
+            reserved_nodes: std::iter::once(reserved1).collect(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(6).return_const(true);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Add second reserved node at runtime (this currently calls `alloc_slots` internally).
+        controller.on_add_reserved_peer(reserved2);
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        // No slots occupied.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // No commands are generated.
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Incoming connection from `reserved1`.
+        let incoming1 = IncomingIndex(1);
+        controller.on_incoming_connection(reserved1, incoming1);
+        assert_eq!(rx.try_recv().unwrap(), Message::Reject(incoming1));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Incoming connection from `reserved2`.
+        let incoming2 = IncomingIndex(2);
+        controller.on_incoming_connection(reserved2, incoming2);
+        assert_eq!(rx.try_recv().unwrap(), Message::Reject(incoming2));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // No slots occupied.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn we_try_to_reconnect_to_dropped_reserved_nodes() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        // Add first reserved node via config.
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            out_peers: 0,
+            reserved_nodes: std::iter::once(reserved1).collect(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(4).return_const(false);
+        peer_store
+            .expect_report_disconnect()
+            .times(2)
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Add second reserved node at runtime (this calls `alloc_slots` internally).
+        controller.on_add_reserved_peer(reserved2);
+
+        // Initiate connections (actually redundant, see previous comment).
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+
+        // Drop both reserved nodes.
+        controller.on_peer_dropped(reserved1);
+        controller.on_peer_dropped(reserved2);
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+
+        // No slots occupied.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn nodes_supplied_by_peer_store_are_connected() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let candidates = vec![peer1, peer2];
+
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            // Less slots than candidates.
+            out_peers: 2,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        // Only first two peers are connected (we only have 2 slots).
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer2
+        }));
+
+        // Outgoing slots occupied.
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+
+        // No more nodes are connected.
+        controller.alloc_slots();
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // No more slots occupied.
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn both_reserved_nodes_and_nodes_supplied_by_peer_store_are_connected() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+        let regular1 = PeerId::random();
+        let regular2 = PeerId::random();
+        let outgoing_candidates = vec![regular1, regular2];
+        let reserved_nodes = [reserved1, reserved2].iter().cloned().collect();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes,
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(2).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 4);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: regular1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: regular2
+        }));
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn if_slots_are_freed_we_try_to_allocate_them_again() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let peer3 = PeerId::random();
+        let candidates1 = vec![peer1, peer2];
+        let candidates2 = vec![peer3];
+
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            // Less slots than candidates.
+            out_peers: 2,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(candidates1);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(candidates2);
+        peer_store
+            .expect_report_disconnect()
+            .times(2)
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        // Only first two peers are connected (we only have 2 slots).
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer2
+        }));
+
+        // Outgoing slots occupied.
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+
+        // No more nodes are connected.
+        controller.alloc_slots();
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // No more slots occupied.
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+
+        // Drop peers.
+        controller.on_peer_dropped(peer1);
+        controller.on_peer_dropped(peer2);
+
+        // Slots are freed.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        // Peers are connected.
+        assert_eq!(messages.len(), 1);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer3
+        }));
+
+        // Outgoing slots occupied.
+        assert_eq!(controller.num_out, 1);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn in_reserved_only_mode_no_peers_are_requested_from_peer_store_and_connected() {
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            // Make sure we have slots available.
+            out_peers: 2,
+            reserved_nodes: HashSet::new(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        // No nodes are connected.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+    }
+
+    #[test]
+    fn in_reserved_only_mode_no_regular_peers_are_accepted() {
+        let config = ProtoSetConfig {
+            // Make sure we have slots available.
+            in_peers: 2,
+            out_peers: 0,
+            reserved_nodes: HashSet::new(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        let peer = PeerId::random();
+        let incoming_index = IncomingIndex(1);
+        controller.on_incoming_connection(peer, incoming_index);
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        // Peer is rejected.
+        assert_eq!(messages.len(), 1);
+        assert!(messages.contains(&Message::Reject(incoming_index)));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn disabling_reserved_only_mode_allows_to_connect_to_peers() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let candidates = vec![peer1, peer2];
+
+        let config = ProtoSetConfig {
+            in_peers: 0,
+            // Make sure we have slots available.
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+
+        // No nodes are connected.
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Disable reserved-only mode (this also connects to peers).
+        controller.on_set_reserved_only(false);
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer2
+        }));
+        assert_eq!(controller.num_out, 2);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn enabling_reserved_only_mode_disconnects_regular_peers() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+        let regular1 = PeerId::random();
+        let regular2 = PeerId::random();
+        let outgoing_candidates = vec![regular1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [reserved1, reserved2].iter().cloned().collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(3).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Connect `regular1` as outbound.
+        controller.alloc_slots();
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 3);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: regular1
+        }));
+        assert_eq!(controller.num_out, 1);
+        assert_eq!(controller.num_in, 0);
+
+        // Connect `regular2` as inbound.
+        let incoming_index = IncomingIndex(1);
+        controller.on_incoming_connection(regular2, incoming_index);
+        assert_eq!(rx.try_recv().unwrap(), Message::Accept(incoming_index));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.num_out, 1);
+        assert_eq!(controller.num_in, 1);
+
+        // Switch to reserved-only mode.
+        controller.on_set_reserved_only(true);
+
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Drop {
+            set_id: SetId::from(0),
+            peer_id: regular1
+        }));
+        assert!(messages.contains(&Message::Drop {
+            set_id: SetId::from(0),
+            peer_id: regular2
+        }));
+        assert_eq!(controller.nodes.len(), 0);
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn removed_disconnected_reserved_node_is_forgotten() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [reserved1, reserved2].iter().cloned().collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert_eq!(controller.reserved_nodes.len(), 2);
+        assert_eq!(controller.nodes.len(), 0);
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        controller.on_remove_reserved_peer(reserved1);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.reserved_nodes.len(), 1);
+        assert!(!controller.reserved_nodes.contains_key(&reserved1));
+        assert_eq!(controller.nodes.len(), 0);
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+    }
+
+    #[test]
+    fn removed_connected_reserved_node_is_disconnected_in_reserved_only_mode() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [reserved1, reserved2].iter().cloned().collect(),
+            reserved_only: true,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(2).return_const(false);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Initiate connections.
+        controller.alloc_slots();
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved1
+        }));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+        assert_eq!(controller.reserved_nodes.len(), 2);
+        assert!(controller.reserved_nodes.contains_key(&reserved1));
+        assert!(controller.reserved_nodes.contains_key(&reserved2));
+        assert!(controller.nodes.is_empty());
+
+        // Remove reserved node
+        controller.on_remove_reserved_peer(reserved1);
+        assert_eq!(
+            rx.try_recv().unwrap(),
+            Message::Drop {
+                set_id: SetId::from(0),
+                peer_id: reserved1
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.reserved_nodes.len(), 1);
+        assert!(controller.reserved_nodes.contains_key(&reserved2));
+        assert!(controller.nodes.is_empty());
+    }
+
+    #[test]
+    fn removed_connected_reserved_nodes_become_regular_in_non_reserved_mode() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [peer1, peer2].iter().cloned().collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(2).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(Vec::new());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `peer1` as inbound, `peer2` as outbound.
+        controller.on_incoming_connection(peer1, IncomingIndex(1));
+        controller.alloc_slots();
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer2
+        }));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Remove reserved nodes (and make them regular)
+        controller.on_remove_reserved_peer(peer1);
+        controller.on_remove_reserved_peer(peer2);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.nodes.len(), 2);
+        assert!(matches!(
+            controller.nodes.get(&peer1),
+            Some(Direction::Inbound)
+        ));
+        assert!(matches!(
+            controller.nodes.get(&peer2),
+            Some(Direction::Outbound)
+        ));
+        assert_eq!(controller.num_out, 1);
+        assert_eq!(controller.num_in, 1);
+    }
+
+    #[test]
+    fn regular_nodes_stop_occupying_slots_when_become_reserved() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let outgoing_candidates = vec![peer1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `peer1` as outbound & `peer2` as inbound.
+        controller.alloc_slots();
+        controller.on_incoming_connection(peer2, IncomingIndex(1));
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert_eq!(controller.num_in, 1);
+        assert_eq!(controller.num_out, 1);
+
+        controller.on_add_reserved_peer(peer1);
+        controller.on_add_reserved_peer(peer2);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.num_in, 0);
+        assert_eq!(controller.num_out, 0);
+    }
+
+    #[test]
+    fn disconnecting_regular_peers_work() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let outgoing_candidates = vec![peer1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `peer1` as outbound & `peer2` as inbound.
+        controller.alloc_slots();
+        controller.on_incoming_connection(peer2, IncomingIndex(1));
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert_eq!(controller.nodes.len(), 2);
+        assert!(matches!(
+            controller.nodes.get(&peer1),
+            Some(Direction::Outbound)
+        ));
+        assert!(matches!(
+            controller.nodes.get(&peer2),
+            Some(Direction::Inbound)
+        ));
+        assert_eq!(controller.num_in, 1);
+        assert_eq!(controller.num_out, 1);
+
+        controller.on_disconnect_peer(peer1);
+        assert_eq!(
+            rx.try_recv().unwrap(),
+            Message::Drop {
+                set_id: SetId::from(0),
+                peer_id: peer1
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.nodes.len(), 1);
+        assert!(!controller.nodes.contains_key(&peer1));
+        assert_eq!(controller.num_in, 1);
+        assert_eq!(controller.num_out, 0);
+
+        controller.on_disconnect_peer(peer2);
+        assert_eq!(
+            rx.try_recv().unwrap(),
+            Message::Drop {
+                set_id: SetId::from(0),
+                peer_id: peer2
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.nodes.len(), 0);
+        assert_eq!(controller.num_in, 0);
+        assert_eq!(controller.num_out, 0);
+    }
+
+    #[test]
+    fn disconnecting_reserved_peers_is_a_noop() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [reserved1, reserved2].iter().cloned().collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(2).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(Vec::new());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `reserved1` as inbound & `reserved2` as outbound.
+        controller.on_incoming_connection(reserved1, IncomingIndex(1));
+        controller.alloc_slots();
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::Connected(Direction::Inbound))
+        ));
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved2),
+            Some(PeerState::Connected(Direction::Outbound))
+        ));
+
+        controller.on_disconnect_peer(reserved1);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::Connected(Direction::Inbound))
+        ));
+
+        controller.on_disconnect_peer(reserved2);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved2),
+            Some(PeerState::Connected(Direction::Outbound))
+        ));
+    }
+
+    #[test]
+    fn dropping_regular_peers_work() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let outgoing_candidates = vec![peer1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+        peer_store
+            .expect_report_disconnect()
+            .times(2)
+            .return_const(());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `peer1` as outbound & `peer2` as inbound.
+        controller.alloc_slots();
+        controller.on_incoming_connection(peer2, IncomingIndex(1));
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: peer1
+        }));
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert_eq!(controller.nodes.len(), 2);
+        assert!(matches!(
+            controller.nodes.get(&peer1),
+            Some(Direction::Outbound)
+        ));
+        assert!(matches!(
+            controller.nodes.get(&peer2),
+            Some(Direction::Inbound)
+        ));
+        assert_eq!(controller.num_in, 1);
+        assert_eq!(controller.num_out, 1);
+
+        controller.on_peer_dropped(peer1);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.nodes.len(), 1);
+        assert!(!controller.nodes.contains_key(&peer1));
+        assert_eq!(controller.num_in, 1);
+        assert_eq!(controller.num_out, 0);
+
+        controller.on_peer_dropped(peer2);
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert_eq!(controller.nodes.len(), 0);
+        assert_eq!(controller.num_in, 0);
+        assert_eq!(controller.num_out, 0);
+    }
+
+    #[test]
+    fn incoming_request_for_connected_reserved_node_switches_it_to_inbound() {
+        let reserved1 = PeerId::random();
+        let reserved2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: [reserved1, reserved2].iter().cloned().collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(2).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(Vec::new());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `reserved1` as inbound & `reserved2` as outbound.
+        controller.on_incoming_connection(reserved1, IncomingIndex(1));
+        controller.alloc_slots();
+        let mut messages = Vec::new();
+        while let Ok(message) = rx.try_recv() {
+            messages.push(message);
+        }
+        assert_eq!(messages.len(), 2);
+        assert!(messages.contains(&Message::Accept(IncomingIndex(1))));
+        assert!(messages.contains(&Message::Connect {
+            set_id: SetId::from(0),
+            peer_id: reserved2
+        }));
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::Connected(Direction::Inbound))
+        ));
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved2),
+            Some(PeerState::Connected(Direction::Outbound))
+        ));
+
+        // Incoming request for `reserved1`.
+        controller.on_incoming_connection(reserved1, IncomingIndex(2));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(2))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::Connected(Direction::Inbound))
+        ));
+
+        // Incoming request for `reserved2`.
+        controller.on_incoming_connection(reserved2, IncomingIndex(3));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(3))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved2),
+            Some(PeerState::Connected(Direction::Inbound))
+        ));
+    }
+
+    #[test]
+    fn incoming_request_for_connected_regular_node_switches_it_to_inbound() {
+        let regular1 = PeerId::random();
+        let regular2 = PeerId::random();
+        let outgoing_candidates = vec![regular1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().times(3).return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Connect `regular1` as outbound.
+        controller.alloc_slots();
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Connect {
+                set_id: SetId::from(0),
+                peer_id: regular1
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular1).unwrap(),
+            Direction::Outbound,
+        ));
+
+        // Connect `regular2` as inbound.
+        controller.on_incoming_connection(regular2, IncomingIndex(0));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(0))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular2).unwrap(),
+            Direction::Inbound,
+        ));
+
+        // Incoming request for `regular1`.
+        controller.on_incoming_connection(regular1, IncomingIndex(1));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(1))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular1).unwrap(),
+            Direction::Inbound,
+        ));
+
+        // Incoming request for `regular2`.
+        controller.on_incoming_connection(regular2, IncomingIndex(2));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(2))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular2).unwrap(),
+            Direction::Inbound,
+        ));
+    }
+
+    #[test]
+    fn incoming_request_for_connected_node_is_rejected_if_its_banned() {
+        let regular1 = PeerId::random();
+        let regular2 = PeerId::random();
+        let outgoing_candidates = vec![regular1];
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+        peer_store.expect_is_banned().times(2).return_const(true);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Connect `regular1` as outbound.
+        controller.alloc_slots();
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Connect {
+                set_id: SetId::from(0),
+                peer_id: regular1
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular1).unwrap(),
+            Direction::Outbound,
+        ));
+
+        // Connect `regular2` as inbound.
+        controller.on_incoming_connection(regular2, IncomingIndex(0));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(0))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular2).unwrap(),
+            Direction::Inbound,
+        ));
+
+        // Incoming request for `regular1`.
+        controller.on_incoming_connection(regular1, IncomingIndex(1));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Reject(IncomingIndex(1))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(!controller.nodes.contains_key(&regular1));
+
+        // Incoming request for `regular2`.
+        controller.on_incoming_connection(regular2, IncomingIndex(2));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Reject(IncomingIndex(2))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(!controller.nodes.contains_key(&regular2));
+    }
+
+    #[test]
+    fn incoming_request_for_connected_node_is_rejected_if_no_slots_available() {
+        let regular1 = PeerId::random();
+        let regular2 = PeerId::random();
+        let outgoing_candidates = vec![regular1];
+
+        let config = ProtoSetConfig {
+            in_peers: 1,
+            out_peers: 1,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(outgoing_candidates);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert_eq!(controller.num_out, 0);
+        assert_eq!(controller.num_in, 0);
+
+        // Connect `regular1` as outbound.
+        controller.alloc_slots();
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Connect {
+                set_id: SetId::from(0),
+                peer_id: regular1
+            }
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular1).unwrap(),
+            Direction::Outbound,
+        ));
+
+        // Connect `regular2` as inbound.
+        controller.on_incoming_connection(regular2, IncomingIndex(0));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Accept(IncomingIndex(0))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(matches!(
+            controller.nodes.get(&regular2).unwrap(),
+            Direction::Inbound,
+        ));
+
+        controller.max_in = 0;
+
+        // Incoming request for `regular1`.
+        controller.on_incoming_connection(regular1, IncomingIndex(1));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Reject(IncomingIndex(1))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(!controller.nodes.contains_key(&regular1));
+
+        // Incoming request for `regular2`.
+        controller.on_incoming_connection(regular2, IncomingIndex(2));
+        assert_eq!(
+            rx.try_recv().ok().unwrap(),
+            Message::Reject(IncomingIndex(2))
+        );
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+        assert!(!controller.nodes.contains_key(&regular2));
+    }
+
+    #[test]
+    fn incoming_peers_that_exceed_slots_are_rejected() {
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 1,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(false);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Connect `peer1` as inbound.
+        controller.on_incoming_connection(peer1, IncomingIndex(1));
+        assert_eq!(rx.try_recv().unwrap(), Message::Accept(IncomingIndex(1)));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+
+        // Incoming requests for `peer2`.
+        controller.on_incoming_connection(peer2, IncomingIndex(2));
+        assert_eq!(rx.try_recv().unwrap(), Message::Reject(IncomingIndex(2)));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+    }
+
+    #[test]
+    fn banned_regular_incoming_node_is_rejected() {
+        let peer1 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: HashSet::new(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(true);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+
+        // Incoming request.
+        controller.on_incoming_connection(peer1, IncomingIndex(1));
+        assert_eq!(rx.try_recv().unwrap(), Message::Reject(IncomingIndex(1)));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+    }
+
+    #[test]
+    fn banned_reserved_incoming_node_is_rejected() {
+        let reserved1 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: std::iter::once(reserved1).collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(true);
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert!(controller.reserved_nodes.contains_key(&reserved1));
+
+        // Incoming request.
+        controller.on_incoming_connection(reserved1, IncomingIndex(1));
+        assert_eq!(rx.try_recv().unwrap(), Message::Reject(IncomingIndex(1)));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+    }
+
+    #[test]
+    fn we_dont_connect_to_banned_reserved_node() {
+        let reserved1 = PeerId::random();
+
+        let config = ProtoSetConfig {
+            in_peers: 10,
+            out_peers: 10,
+            reserved_nodes: std::iter::once(reserved1).collect(),
+            reserved_only: false,
+        };
+        let (tx, mut rx) = tracing_unbounded("mpsc_test_to_notifications", 100);
+
+        let mut peer_store = MockPeerStoreHandle::new();
+        peer_store
+            .expect_register_protocol()
+            .once()
+            .return_const(());
+        peer_store.expect_is_banned().once().return_const(true);
+        peer_store
+            .expect_outgoing_candidates()
+            .once()
+            .return_const(Vec::new());
+
+        let (_handle, mut controller) =
+            ProtocolController::new(SetId::from(0), config, tx, Box::new(peer_store));
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::NotConnected)
+        ));
+
+        // Initiate connectios
+        controller.alloc_slots();
+        assert!(matches!(
+            controller.reserved_nodes.get(&reserved1),
+            Some(PeerState::NotConnected)
+        ));
+        assert_eq!(rx.try_recv().unwrap_err(), TryRecvError::Empty);
+    }
+}
diff --git a/libp2p/src/request_responses.rs b/libp2p/src/request_responses.rs
new file mode 100644
index 000000000..1a76661bc
--- /dev/null
+++ b/libp2p/src/request_responses.rs
@@ -0,0 +1,1795 @@
+// 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/>.
+
+//! Collection of request-response protocols.
+//!
+//! The [`RequestResponsesBehaviour`] struct defined in this module provides support for zero or
+//! more so-called "request-response" protocols.
+//!
+//! A request-response protocol works in the following way:
+//!
+//! - For every emitted request, a new substream is open and the protocol is negotiated. If the
+//! remote supports the protocol, the size of the request is sent as a LEB128 number, followed
+//! with the request itself. The remote then sends the size of the response as a LEB128 number,
+//! followed with the response.
+//!
+//! - Requests have a certain time limit before they time out. This time includes the time it
+//! takes to send/receive the request and response.
+//!
+//! - If provided, a ["requests processing"](ProtocolConfig::inbound_queue) channel
+//! is used to handle incoming requests.
+
+use crate::{
+    peer_store::{PeerStoreProvider, BANNED_THRESHOLD},
+    types::ProtocolName,
+    ReputationChange,
+};
+
+use futures::{channel::oneshot, prelude::*};
+use libp2p::{
+    core::{Endpoint, Multiaddr},
+    request_response::{self, Behaviour, Codec, Message, ProtocolSupport, ResponseChannel},
+    swarm::{
+        behaviour::{ConnectionClosed, FromSwarm},
+        handler::multi::MultiHandler,
+        ConnectionDenied, ConnectionId, NetworkBehaviour, PollParameters, THandler,
+        THandlerInEvent, THandlerOutEvent, ToSwarm,
+    },
+    PeerId,
+};
+
+use std::{
+    collections::{hash_map::Entry, HashMap},
+    io, iter,
+    ops::Deref,
+    pin::Pin,
+    task::{Context, Poll},
+    time::{Duration, Instant},
+};
+
+pub use libp2p::request_response::{Config, InboundFailure, OutboundFailure, RequestId};
+
+/// Error in a request.
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum RequestFailure {
+    #[error("We are not currently connected to the requested peer.")]
+    NotConnected,
+    #[error("Given protocol hasn't been registered.")]
+    UnknownProtocol,
+    #[error("Remote has closed the substream before answering, thereby signaling that it considers the request as valid, but refused to answer it.")]
+    Refused,
+    #[error("The remote replied, but the local node is no longer interested in the response.")]
+    Obsolete,
+    #[error("Problem on the network: {0}")]
+    Network(OutboundFailure),
+}
+
+/// Configuration for a single request-response protocol.
+#[derive(Debug, Clone)]
+pub struct ProtocolConfig {
+    /// Name of the protocol on the wire. Should be something like `/foo/bar`.
+    pub name: ProtocolName,
+
+    /// Fallback on the wire protocol names to support.
+    pub fallback_names: Vec<ProtocolName>,
+
+    /// Maximum allowed size, in bytes, of a request.
+    ///
+    /// Any request larger than this value will be declined as a way to avoid allocating too
+    /// much memory for it.
+    pub max_request_size: u64,
+
+    /// Maximum allowed size, in bytes, of a response.
+    ///
+    /// Any response larger than this value will be declined as a way to avoid allocating too
+    /// much memory for it.
+    pub max_response_size: u64,
+
+    /// Duration after which emitted requests are considered timed out.
+    ///
+    /// If you expect the response to come back quickly, you should set this to a smaller duration.
+    pub request_timeout: Duration,
+
+    /// Channel on which the networking service will send incoming requests.
+    ///
+    /// Every time a peer sends a request to the local node using this protocol, the networking
+    /// service will push an element on this channel. The receiving side of this channel then has
+    /// to pull this element, process the request, and send back the response to send back to the
+    /// peer.
+    ///
+    /// The size of the channel has to be carefully chosen. If the channel is full, the networking
+    /// service will discard the incoming request send back an error to the peer. Consequently,
+    /// the channel being full is an indicator that the node is overloaded.
+    ///
+    /// You can typically set the size of the channel to `T / d`, where `T` is the
+    /// `request_timeout` and `d` is the expected average duration of CPU and I/O it takes to
+    /// build a response.
+    ///
+    /// Can be `None` if the local node does not support answering incoming requests.
+    /// If this is `None`, then the local node will not advertise support for this protocol towards
+    /// other peers. If this is `Some` but the channel is closed, then the local node will
+    /// advertise support for this protocol, but any incoming request will lead to an error being
+    /// sent back.
+    pub inbound_queue: Option<async_channel::Sender<IncomingRequest>>,
+}
+
+/// A single request received by a peer on a request-response protocol.
+#[derive(Debug)]
+pub struct IncomingRequest {
+    /// Who sent the request.
+    pub peer: PeerId,
+
+    /// Request sent by the remote. Will always be smaller than
+    /// [`ProtocolConfig::max_request_size`].
+    pub payload: Vec<u8>,
+
+    /// Channel to send back the response.
+    ///
+    /// There are two ways to indicate that handling the request failed:
+    ///
+    /// 1. Drop `pending_response` and thus not changing the reputation of the peer.
+    ///
+    /// 2. Sending an `Err(())` via `pending_response`, optionally including reputation changes for
+    /// the given peer.
+    pub pending_response: oneshot::Sender<OutgoingResponse>,
+}
+
+/// Response for an incoming request to be send by a request protocol handler.
+#[derive(Debug)]
+pub struct OutgoingResponse {
+    /// The payload of the response.
+    ///
+    /// `Err(())` if none is available e.g. due an error while handling the request.
+    pub result: Result<Vec<u8>, ()>,
+
+    /// Reputation changes accrued while handling the request. To be applied to the reputation of
+    /// the peer sending the request.
+    pub reputation_changes: Vec<ReputationChange>,
+
+    /// If provided, the `oneshot::Sender` will be notified when the request has been sent to the
+    /// peer.
+    ///
+    /// > **Note**: Operating systems typically maintain a buffer of a few dozen kilobytes of
+    /// >        outgoing data for each TCP socket, and it is not possible for a user
+    /// >        application to inspect this buffer. This channel here is not actually notified
+    /// >        when the response has been fully sent out, but rather when it has fully been
+    /// >        written to the buffer managed by the operating system.
+    pub sent_feedback: Option<oneshot::Sender<()>>,
+}
+
+/// Information stored about a pending request.
+struct PendingRequest {
+    started_at: Instant,
+    response_tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+    fallback_request: Option<(Vec<u8>, ProtocolName)>,
+}
+
+/// When sending a request, what to do on a disconnected recipient.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub enum IfDisconnected {
+    /// Try to connect to the peer.
+    TryConnect,
+    /// Just fail if the destination is not yet connected.
+    ImmediateError,
+}
+
+/// Convenience functions for `IfDisconnected`.
+impl IfDisconnected {
+    /// Shall we connect to a disconnected peer?
+    pub fn should_connect(self) -> bool {
+        match self {
+            Self::TryConnect => true,
+            Self::ImmediateError => false,
+        }
+    }
+}
+
+/// Event generated by the [`RequestResponsesBehaviour`].
+#[derive(Debug)]
+pub enum Event {
+    /// A remote sent a request and either we have successfully answered it or an error happened.
+    ///
+    /// This event is generated for statistics purposes.
+    InboundRequest {
+        /// Peer which has emitted the request.
+        peer: PeerId,
+        /// Name of the protocol in question.
+        protocol: ProtocolName,
+        /// Whether handling the request was successful or unsuccessful.
+        ///
+        /// When successful contains the time elapsed between when we received the request and when
+        /// we sent back the response. When unsuccessful contains the failure reason.
+        result: Result<Duration, ResponseFailure>,
+    },
+
+    /// A request initiated using [`RequestResponsesBehaviour::send_request`] has succeeded or
+    /// failed.
+    ///
+    /// This event is generated for statistics purposes.
+    RequestFinished {
+        /// Peer that we send a request to.
+        peer: PeerId,
+        /// Name of the protocol in question.
+        protocol: ProtocolName,
+        /// Duration the request took.
+        duration: Duration,
+        /// Result of the request.
+        result: Result<(), RequestFailure>,
+    },
+
+    /// A request protocol handler issued reputation changes for the given peer.
+    ReputationChanges {
+        /// Peer whose reputation needs to be adjust.
+        peer: PeerId,
+        /// Reputation changes.
+        changes: Vec<ReputationChange>,
+    },
+}
+
+/// Combination of a protocol name and a request id.
+///
+/// Uniquely identifies an inbound or outbound request among all handled protocols. Note however
+/// that uniqueness is only guaranteed between two inbound and likewise between two outbound
+/// requests. There is no uniqueness guarantee in a set of both inbound and outbound
+/// [`ProtocolRequestId`]s.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+struct ProtocolRequestId {
+    protocol: ProtocolName,
+    request_id: RequestId,
+}
+
+impl From<(ProtocolName, RequestId)> for ProtocolRequestId {
+    fn from((protocol, request_id): (ProtocolName, RequestId)) -> Self {
+        Self {
+            protocol,
+            request_id,
+        }
+    }
+}
+
+/// Implementation of `NetworkBehaviour` that provides support for request-response protocols.
+pub struct RequestResponsesBehaviour {
+    /// The multiple sub-protocols, by name.
+    ///
+    /// Contains the underlying libp2p request-response [`Behaviour`], plus an optional
+    /// "response builder" used to build responses for incoming requests.
+    protocols: HashMap<
+        ProtocolName,
+        (
+            Behaviour<GenericCodec>,
+            Option<async_channel::Sender<IncomingRequest>>,
+        ),
+    >,
+
+    /// Pending requests, passed down to a request-response [`Behaviour`], awaiting a reply.
+    pending_requests: HashMap<ProtocolRequestId, PendingRequest>,
+
+    /// Whenever an incoming request arrives, a `Future` is added to this list and will yield the
+    /// start time and the response to send back to the remote.
+    pending_responses: stream::FuturesUnordered<
+        Pin<Box<dyn Future<Output = Option<RequestProcessingOutcome>> + Send>>,
+    >,
+
+    /// Whenever an incoming request arrives, the arrival [`Instant`] is recorded here.
+    pending_responses_arrival_time: HashMap<ProtocolRequestId, Instant>,
+
+    /// Whenever a response is received on `pending_responses`, insert a channel to be notified
+    /// when the request has been sent out.
+    send_feedback: HashMap<ProtocolRequestId, oneshot::Sender<()>>,
+
+    /// Primarily used to get a reputation of a node.
+    peer_store: Box<dyn PeerStoreProvider>,
+}
+
+/// Generated by the response builder and waiting to be processed.
+struct RequestProcessingOutcome {
+    peer: PeerId,
+    request_id: RequestId,
+    protocol: ProtocolName,
+    inner_channel: ResponseChannel<Result<Vec<u8>, ()>>,
+    response: OutgoingResponse,
+}
+
+impl RequestResponsesBehaviour {
+    /// Creates a new behaviour. Must be passed a list of supported protocols. Returns an error if
+    /// the same protocol is passed twice.
+    pub fn new(
+        list: impl Iterator<Item = ProtocolConfig>,
+        peer_store: Box<dyn PeerStoreProvider>,
+    ) -> Result<Self, RegisterError> {
+        let mut protocols = HashMap::new();
+        for protocol in list {
+            let mut cfg = Config::default();
+            cfg.set_connection_keep_alive(Duration::from_secs(10));
+            cfg.set_request_timeout(protocol.request_timeout);
+
+            let protocol_support = if protocol.inbound_queue.is_some() {
+                ProtocolSupport::Full
+            } else {
+                ProtocolSupport::Outbound
+            };
+
+            let rq_rp = Behaviour::new(
+                GenericCodec {
+                    max_request_size: protocol.max_request_size,
+                    max_response_size: protocol.max_response_size,
+                },
+                iter::once(protocol.name.as_bytes().to_vec())
+                    .chain(
+                        protocol
+                            .fallback_names
+                            .iter()
+                            .map(|name| name.as_bytes().to_vec()),
+                    )
+                    .zip(iter::repeat(protocol_support)),
+                cfg,
+            );
+
+            match protocols.entry(protocol.name) {
+                Entry::Vacant(e) => e.insert((rq_rp, protocol.inbound_queue)),
+                Entry::Occupied(e) => {
+                    return Err(RegisterError::DuplicateProtocol(e.key().clone()))
+                }
+            };
+        }
+
+        Ok(Self {
+            protocols,
+            pending_requests: Default::default(),
+            pending_responses: Default::default(),
+            pending_responses_arrival_time: Default::default(),
+            send_feedback: Default::default(),
+            peer_store,
+        })
+    }
+
+    /// Initiates sending a request.
+    ///
+    /// If there is no established connection to the target peer, the behavior is determined by the
+    /// choice of `connect`.
+    ///
+    /// An error is returned if the protocol doesn't match one that has been registered.
+    pub fn send_request(
+        &mut self,
+        target: &PeerId,
+        protocol_name: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        log::trace!(target: "sub-libp2p", "send request to {target} ({protocol_name:?}), {} bytes", request.len());
+
+        if let Some((protocol, _)) = self.protocols.get_mut(protocol_name.deref()) {
+            Self::send_request_inner(
+                protocol,
+                &mut self.pending_requests,
+                target,
+                protocol_name,
+                request,
+                fallback_request,
+                pending_response,
+                connect,
+            )
+        } else if pending_response
+            .send(Err(RequestFailure::UnknownProtocol))
+            .is_err()
+        {
+            log::debug!(
+                target: "sub-libp2p",
+                "Unknown protocol {:?}. At the same time local \
+                 node is no longer interested in the result.",
+                protocol_name,
+            );
+        }
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    fn send_request_inner(
+        behaviour: &mut Behaviour<GenericCodec>,
+        pending_requests: &mut HashMap<ProtocolRequestId, PendingRequest>,
+        target: &PeerId,
+        protocol_name: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        if behaviour.is_connected(target) || connect.should_connect() {
+            let request_id = behaviour.send_request(target, request);
+            let prev_req_id = pending_requests.insert(
+                (protocol_name.to_string().into(), request_id).into(),
+                PendingRequest {
+                    started_at: Instant::now(),
+                    response_tx: pending_response,
+                    fallback_request,
+                },
+            );
+            debug_assert!(prev_req_id.is_none(), "Expect request id to be unique.");
+        } else if pending_response
+            .send(Err(RequestFailure::NotConnected))
+            .is_err()
+        {
+            log::debug!(
+                target: "sub-libp2p",
+                "Not connected to peer {:?}. At the same time local \
+                 node is no longer interested in the result.",
+                target,
+            );
+        }
+    }
+}
+
+impl NetworkBehaviour for RequestResponsesBehaviour {
+    type ConnectionHandler =
+        MultiHandler<String, <Behaviour<GenericCodec> as NetworkBehaviour>::ConnectionHandler>;
+    type OutEvent = Event;
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _local_addr: &Multiaddr,
+        _remote_addr: &Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        Ok(())
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        _connection_id: ConnectionId,
+        _maybe_peer: Option<PeerId>,
+        _addresses: &[Multiaddr],
+        _effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        Ok(Vec::new())
+    }
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        let iter = self.protocols.iter_mut().filter_map(|(p, (r, _))| {
+            if let Ok(handler) = r.handle_established_inbound_connection(
+                connection_id,
+                peer,
+                local_addr,
+                remote_addr,
+            ) {
+                Some((p.to_string(), handler))
+            } else {
+                None
+            }
+        });
+
+        Ok(MultiHandler::try_from_iter(iter).expect(
+            "Protocols are in a HashMap and there can be at most one handler per protocol name, \
+			 which is the only possible error; qed",
+        ))
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        let iter = self.protocols.iter_mut().filter_map(|(p, (r, _))| {
+            if let Ok(handler) =
+                r.handle_established_outbound_connection(connection_id, peer, addr, role_override)
+            {
+                Some((p.to_string(), handler))
+            } else {
+                None
+            }
+        });
+
+        Ok(MultiHandler::try_from_iter(iter).expect(
+            "Protocols are in a HashMap and there can be at most one handler per protocol name, \
+			 which is the only possible error; qed",
+        ))
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        match event {
+            FromSwarm::ConnectionEstablished(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ConnectionEstablished(e));
+                }
+            }
+            FromSwarm::ConnectionClosed(ConnectionClosed {
+                peer_id,
+                connection_id,
+                endpoint,
+                handler,
+                remaining_established,
+            }) => {
+                for (p_name, p_handler) in handler.into_iter() {
+                    if let Some((proto, _)) = self.protocols.get_mut(p_name.as_str()) {
+                        proto.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed {
+                            peer_id,
+                            connection_id,
+                            endpoint,
+                            handler: p_handler,
+                            remaining_established,
+                        }));
+                    } else {
+                        log::error!(
+                          target: "sub-libp2p",
+                          "on_swarm_event/connection_closed: no request-response instance registered for protocol {:?}",
+                          p_name,
+                        )
+                    }
+                }
+            }
+            FromSwarm::DialFailure(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::DialFailure(e));
+                }
+            }
+            FromSwarm::ListenerClosed(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ListenerClosed(e));
+                }
+            }
+            FromSwarm::ListenFailure(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ListenFailure(e));
+                }
+            }
+            FromSwarm::ListenerError(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ListenerError(e));
+                }
+            }
+            FromSwarm::ExpiredExternalAddr(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ExpiredExternalAddr(e));
+                }
+            }
+            FromSwarm::NewListener(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::NewListener(e));
+                }
+            }
+            FromSwarm::ExpiredListenAddr(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::ExpiredListenAddr(e));
+                }
+            }
+            FromSwarm::NewExternalAddr(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::NewExternalAddr(e));
+                }
+            }
+            FromSwarm::AddressChange(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::AddressChange(e));
+                }
+            }
+            FromSwarm::NewListenAddr(e) => {
+                for (p, _) in self.protocols.values_mut() {
+                    NetworkBehaviour::on_swarm_event(p, FromSwarm::NewListenAddr(e));
+                }
+            }
+        }
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        let p_name = event.0;
+        if let Some((proto, _)) = self.protocols.get_mut(p_name.as_str()) {
+            proto.on_connection_handler_event(peer_id, connection_id, event.1)
+        } else {
+            log::warn!(
+                target: "sub-libp2p",
+                "on_connection_handler_event: no request-response instance registered for protocol {:?}",
+                p_name
+            );
+        }
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+        params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        'poll_all: loop {
+            // Poll to see if any response is ready to be sent back.
+            while let Poll::Ready(Some(outcome)) = self.pending_responses.poll_next_unpin(cx) {
+                let RequestProcessingOutcome {
+                    peer,
+                    request_id,
+                    protocol: protocol_name,
+                    inner_channel,
+                    response:
+                        OutgoingResponse {
+                            result,
+                            reputation_changes,
+                            sent_feedback,
+                        },
+                } = match outcome {
+                    Some(outcome) => outcome,
+                    // The response builder was too busy or handling the request failed. This is
+                    // later on reported as a `InboundFailure::Omission`.
+                    None => continue,
+                };
+
+                if let Ok(payload) = result {
+                    if let Some((protocol, _)) = self.protocols.get_mut(&*protocol_name) {
+                        log::trace!(target: "sub-libp2p", "send response to {peer} ({protocol_name:?}), {} bytes", payload.len());
+
+                        if protocol.send_response(inner_channel, Ok(payload)).is_err() {
+                            // Note: Failure is handled further below when receiving
+                            // `InboundFailure` event from request-response [`Behaviour`].
+                            log::debug!(
+                                target: "sub-libp2p",
+                                "Failed to send response for {:?} on protocol {:?} due to a \
+                                 timeout or due to the connection to the peer being closed. \
+                                 Dropping response",
+                                request_id, protocol_name,
+                            );
+                        } else if let Some(sent_feedback) = sent_feedback {
+                            self.send_feedback
+                                .insert((protocol_name, request_id).into(), sent_feedback);
+                        }
+                    }
+                }
+
+                if !reputation_changes.is_empty() {
+                    return Poll::Ready(ToSwarm::GenerateEvent(Event::ReputationChanges {
+                        peer,
+                        changes: reputation_changes,
+                    }));
+                }
+            }
+
+            let mut fallback_requests = vec![];
+
+            // Poll request-responses protocols.
+            for (protocol, (ref mut behaviour, ref mut resp_builder)) in &mut self.protocols {
+                'poll_protocol: while let Poll::Ready(ev) = behaviour.poll(cx, params) {
+                    let ev = match ev {
+                        // Main events we are interested in.
+                        ToSwarm::GenerateEvent(ev) => ev,
+
+                        // Other events generated by the underlying behaviour are transparently
+                        // passed through.
+                        ToSwarm::Dial { opts } => {
+                            if opts.get_peer_id().is_none() {
+                                log::error!(
+									"The request-response isn't supposed to start dialing addresses"
+								);
+                            }
+                            return Poll::Ready(ToSwarm::Dial { opts });
+                        }
+                        ToSwarm::NotifyHandler {
+                            peer_id,
+                            handler,
+                            event,
+                        } => {
+                            return Poll::Ready(ToSwarm::NotifyHandler {
+                                peer_id,
+                                handler,
+                                event: ((*protocol).to_string(), event),
+                            })
+                        }
+                        ToSwarm::ReportObservedAddr { address, score } => {
+                            return Poll::Ready(ToSwarm::ReportObservedAddr { address, score })
+                        }
+                        ToSwarm::CloseConnection {
+                            peer_id,
+                            connection,
+                        } => {
+                            return Poll::Ready(ToSwarm::CloseConnection {
+                                peer_id,
+                                connection,
+                            })
+                        }
+                    };
+
+                    match ev {
+                        // Received a request from a remote.
+                        request_response::Event::Message {
+                            peer,
+                            message:
+                                Message::Request {
+                                    request_id,
+                                    request,
+                                    channel,
+                                    ..
+                                },
+                        } => {
+                            self.pending_responses_arrival_time
+                                .insert((protocol.clone(), request_id).into(), Instant::now());
+
+                            let reputation = self.peer_store.peer_reputation(&peer);
+
+                            if reputation < BANNED_THRESHOLD {
+                                log::debug!(
+                                    target: "sub-libp2p",
+                                    "Cannot handle requests from a node with a low reputation {}: {}",
+                                    peer,
+                                    reputation,
+                                );
+                                continue 'poll_protocol;
+                            }
+
+                            let (tx, rx) = oneshot::channel();
+
+                            // Submit the request to the "response builder" passed by the user at
+                            // initialization.
+                            if let Some(resp_builder) = resp_builder {
+                                // If the response builder is too busy, silently drop `tx`. This
+                                // will be reported by the corresponding request-response
+                                // [`Behaviour`] through an `InboundFailure::Omission` event.
+                                // Note that we use `async_channel::bounded` and not `mpsc::channel`
+                                // because the latter allocates an extra slot for every cloned
+                                // sender.
+                                let _ = resp_builder.try_send(IncomingRequest {
+                                    peer,
+                                    payload: request,
+                                    pending_response: tx,
+                                });
+                            } else {
+                                debug_assert!(false, "Received message on outbound-only protocol.");
+                            }
+
+                            let protocol = protocol.clone();
+
+                            self.pending_responses.push(Box::pin(async move {
+                                // The `tx` created above can be dropped if we are not capable of
+                                // processing this request, which is reflected as a
+                                // `InboundFailure::Omission` event.
+                                rx.await.map_or(None, |response| {
+                                    Some(RequestProcessingOutcome {
+                                        peer,
+                                        request_id,
+                                        protocol,
+                                        inner_channel: channel,
+                                        response,
+                                    })
+                                })
+                            }));
+
+                            // This `continue` makes sure that `pending_responses` gets polled
+                            // after we have added the new element.
+                            continue 'poll_all;
+                        }
+
+                        // Received a response from a remote to one of our requests.
+                        request_response::Event::Message {
+                            peer,
+                            message:
+                                Message::Response {
+                                    request_id,
+                                    response,
+                                },
+                            ..
+                        } => {
+                            let (started, delivered) = match self
+                                .pending_requests
+                                .remove(&(protocol.clone(), request_id).into())
+                            {
+                                Some(PendingRequest {
+                                    started_at,
+                                    response_tx,
+                                    ..
+                                }) => {
+                                    log::trace!(
+                                        target: "sub-libp2p",
+                                        "received response from {peer} ({protocol:?}), {} bytes",
+                                        response.as_ref().map_or(0usize, |response| response.len()),
+                                    );
+
+                                    let delivered = response_tx
+                                        .send(
+                                            response
+                                                .map_err(|()| RequestFailure::Refused)
+                                                .map(|resp| (resp, protocol.clone())),
+                                        )
+                                        .map_err(|_| RequestFailure::Obsolete);
+                                    (started_at, delivered)
+                                }
+                                None => {
+                                    log::warn!(
+                                        target: "sub-libp2p",
+                                        "Received `RequestResponseEvent::Message` with unexpected request id {:?}",
+                                        request_id,
+                                    );
+                                    debug_assert!(false);
+                                    continue;
+                                }
+                            };
+
+                            let out = Event::RequestFinished {
+                                peer,
+                                protocol: protocol.clone(),
+                                duration: started.elapsed(),
+                                result: delivered,
+                            };
+
+                            return Poll::Ready(ToSwarm::GenerateEvent(out));
+                        }
+
+                        // One of our requests has failed.
+                        request_response::Event::OutboundFailure {
+                            peer,
+                            request_id,
+                            error,
+                            ..
+                        } => {
+                            let started = match self
+                                .pending_requests
+                                .remove(&(protocol.clone(), request_id).into())
+                            {
+                                Some(PendingRequest {
+                                    started_at,
+                                    response_tx,
+                                    fallback_request,
+                                }) => {
+                                    // Try using the fallback request if the protocol was not
+                                    // supported.
+                                    if let OutboundFailure::UnsupportedProtocols = error {
+                                        if let Some((fallback_request, fallback_protocol)) =
+                                            fallback_request
+                                        {
+                                            log::trace!(
+                                                target: "sub-libp2p",
+                                                "Request with id {:?} failed. Trying the fallback protocol. {}",
+                                                request_id,
+                                                fallback_protocol.deref()
+                                            );
+                                            fallback_requests.push((
+                                                peer,
+                                                fallback_protocol,
+                                                fallback_request,
+                                                response_tx,
+                                            ));
+                                            continue;
+                                        }
+                                    }
+
+                                    if response_tx
+                                        .send(Err(RequestFailure::Network(error.clone())))
+                                        .is_err()
+                                    {
+                                        log::debug!(
+                                            target: "sub-libp2p",
+                                            "Request with id {:?} failed. At the same time local \
+                                             node is no longer interested in the result.",
+                                            request_id,
+                                        );
+                                    }
+                                    started_at
+                                }
+                                None => {
+                                    log::warn!(
+                                        target: "sub-libp2p",
+                                        "Received `RequestResponseEvent::Message` with unexpected request id {:?}",
+                                        request_id,
+                                    );
+                                    debug_assert!(false);
+                                    continue;
+                                }
+                            };
+
+                            let out = Event::RequestFinished {
+                                peer,
+                                protocol: protocol.clone(),
+                                duration: started.elapsed(),
+                                result: Err(RequestFailure::Network(error)),
+                            };
+
+                            return Poll::Ready(ToSwarm::GenerateEvent(out));
+                        }
+
+                        // An inbound request failed, either while reading the request or due to
+                        // failing to send a response.
+                        request_response::Event::InboundFailure {
+                            request_id,
+                            peer,
+                            error,
+                            ..
+                        } => {
+                            self.pending_responses_arrival_time
+                                .remove(&(protocol.clone(), request_id).into());
+                            self.send_feedback
+                                .remove(&(protocol.clone(), request_id).into());
+                            let out = Event::InboundRequest {
+                                peer,
+                                protocol: protocol.clone(),
+                                result: Err(ResponseFailure::Network(error)),
+                            };
+                            return Poll::Ready(ToSwarm::GenerateEvent(out));
+                        }
+
+                        // A response to an inbound request has been sent.
+                        request_response::Event::ResponseSent { request_id, peer } => {
+                            let arrival_time = self
+                                .pending_responses_arrival_time
+                                .remove(&(protocol.clone(), request_id).into())
+                                .map(|t| t.elapsed())
+                                .expect(
+                                    "Time is added for each inbound request on arrival and only \
+									 removed on success (`ResponseSent`) or failure \
+									 (`InboundFailure`). One can not receive a success event for a \
+									 request that either never arrived, or that has previously \
+									 failed; qed.",
+                                );
+
+                            if let Some(send_feedback) = self
+                                .send_feedback
+                                .remove(&(protocol.clone(), request_id).into())
+                            {
+                                let _ = send_feedback.send(());
+                            }
+
+                            let out = Event::InboundRequest {
+                                peer,
+                                protocol: protocol.clone(),
+                                result: Ok(arrival_time),
+                            };
+
+                            return Poll::Ready(ToSwarm::GenerateEvent(out));
+                        }
+                    };
+                }
+            }
+
+            // Send out fallback requests.
+            for (peer, protocol, request, pending_response) in fallback_requests.drain(..) {
+                if let Some((behaviour, _)) = self.protocols.get_mut(&protocol) {
+                    Self::send_request_inner(
+                        behaviour,
+                        &mut self.pending_requests,
+                        &peer,
+                        protocol,
+                        request,
+                        None,
+                        pending_response,
+                        // We can error if not connected because the
+                        // previous attempt would have tried to establish a
+                        // connection already or errored and we wouldn't have gotten here.
+                        IfDisconnected::ImmediateError,
+                    );
+                }
+            }
+
+            break Poll::Pending;
+        }
+    }
+}
+
+/// Error when registering a protocol.
+#[derive(Debug, thiserror::Error)]
+pub enum RegisterError {
+    /// A protocol has been specified multiple times.
+    #[error("{0}")]
+    DuplicateProtocol(ProtocolName),
+}
+
+/// Error when processing a request sent by a remote.
+#[derive(Debug, thiserror::Error)]
+pub enum ResponseFailure {
+    /// Problem on the network.
+    #[error("Problem on the network: {0}")]
+    Network(InboundFailure),
+}
+
+/// Implements the libp2p [`Codec`] trait. Defines how streams of bytes are turned
+/// into requests and responses and vice-versa.
+#[derive(Debug, Clone)]
+#[doc(hidden)] // Needs to be public in order to satisfy the Rust compiler.
+pub struct GenericCodec {
+    max_request_size: u64,
+    max_response_size: u64,
+}
+
+#[async_trait::async_trait]
+impl Codec for GenericCodec {
+    type Protocol = Vec<u8>;
+    type Request = Vec<u8>;
+    type Response = Result<Vec<u8>, ()>;
+
+    async fn read_request<T>(
+        &mut self,
+        _: &Self::Protocol,
+        mut io: &mut T,
+    ) -> io::Result<Self::Request>
+    where
+        T: AsyncRead + Unpin + Send,
+    {
+        // Read the length.
+        let length = unsigned_varint::aio::read_usize(&mut io)
+            .await
+            .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
+        if length > usize::try_from(self.max_request_size).unwrap_or(usize::MAX) {
+            return Err(io::Error::new(
+                io::ErrorKind::InvalidInput,
+                format!(
+                    "Request size exceeds limit: {} > {}",
+                    length, self.max_request_size
+                ),
+            ));
+        }
+
+        // Read the payload.
+        let mut buffer = vec![0; length];
+        io.read_exact(&mut buffer).await?;
+        Ok(buffer)
+    }
+
+    async fn read_response<T>(
+        &mut self,
+        _: &Self::Protocol,
+        mut io: &mut T,
+    ) -> io::Result<Self::Response>
+    where
+        T: AsyncRead + Unpin + Send,
+    {
+        // Note that this function returns a `Result<Result<...>>`. Returning an `Err` is
+        // considered as a protocol error and will result in the entire connection being closed.
+        // Returning `Ok(Err(_))` signifies that a response has successfully been fetched, and
+        // that this response is an error.
+
+        // Read the length.
+        let length = match unsigned_varint::aio::read_usize(&mut io).await {
+            Ok(l) => l,
+            Err(unsigned_varint::io::ReadError::Io(err))
+                if matches!(err.kind(), io::ErrorKind::UnexpectedEof) =>
+            {
+                return Ok(Err(()))
+            }
+            Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidInput, err)),
+        };
+
+        if length > usize::try_from(self.max_response_size).unwrap_or(usize::MAX) {
+            return Err(io::Error::new(
+                io::ErrorKind::InvalidInput,
+                format!(
+                    "Response size exceeds limit: {} > {}",
+                    length, self.max_response_size
+                ),
+            ));
+        }
+
+        // Read the payload.
+        let mut buffer = vec![0; length];
+        io.read_exact(&mut buffer).await?;
+        Ok(Ok(buffer))
+    }
+
+    async fn write_request<T>(
+        &mut self,
+        _: &Self::Protocol,
+        io: &mut T,
+        req: Self::Request,
+    ) -> io::Result<()>
+    where
+        T: AsyncWrite + Unpin + Send,
+    {
+        // TODO: check the length?
+        // Write the length.
+        {
+            let mut buffer = unsigned_varint::encode::usize_buffer();
+            io.write_all(unsigned_varint::encode::usize(req.len(), &mut buffer))
+                .await?;
+        }
+
+        // Write the payload.
+        io.write_all(&req).await?;
+
+        io.close().await?;
+        Ok(())
+    }
+
+    async fn write_response<T>(
+        &mut self,
+        _: &Self::Protocol,
+        io: &mut T,
+        res: Self::Response,
+    ) -> io::Result<()>
+    where
+        T: AsyncWrite + Unpin + Send,
+    {
+        // If `res` is an `Err`, we jump to closing the substream without writing anything on it.
+        if let Ok(res) = res {
+            // TODO: check the length?
+            // Write the length.
+            {
+                let mut buffer = unsigned_varint::encode::usize_buffer();
+                io.write_all(unsigned_varint::encode::usize(res.len(), &mut buffer))
+                    .await?;
+            }
+
+            // Write the payload.
+            io.write_all(&res).await?;
+        }
+
+        io.close().await?;
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::mock::MockPeerStore;
+    use assert_matches::assert_matches;
+    use futures::{channel::oneshot, executor::LocalPool, task::Spawn};
+    use libp2p::{
+        core::{
+            transport::{MemoryTransport, Transport},
+            upgrade,
+        },
+        identity::Keypair,
+        noise,
+        swarm::{Executor, Swarm, SwarmBuilder, SwarmEvent},
+        Multiaddr,
+    };
+    use std::{iter, time::Duration};
+
+    struct TokioExecutor(tokio::runtime::Runtime);
+    impl Executor for TokioExecutor {
+        fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) {
+            let res = self.0.spawn(f);
+            drop(res);
+        }
+    }
+
+    fn build_swarm(
+        list: impl Iterator<Item = ProtocolConfig>,
+    ) -> (Swarm<RequestResponsesBehaviour>, Multiaddr) {
+        let keypair = Keypair::generate_ed25519();
+
+        let transport = MemoryTransport::new()
+            .upgrade(upgrade::Version::V1)
+            .authenticate(noise::Config::new(&keypair).unwrap())
+            .multiplex(libp2p::yamux::Config::default())
+            .boxed();
+
+        let behaviour = RequestResponsesBehaviour::new(list, Box::new(MockPeerStore {})).unwrap();
+
+        let runtime = tokio::runtime::Runtime::new().unwrap();
+        let mut swarm = SwarmBuilder::with_executor(
+            transport,
+            behaviour,
+            keypair.public().to_peer_id(),
+            TokioExecutor(runtime),
+        )
+        .build();
+        let listen_addr: Multiaddr = format!("/memory/{}", rand::random::<u64>())
+            .parse()
+            .unwrap();
+
+        swarm.listen_on(listen_addr.clone()).unwrap();
+        (swarm, listen_addr)
+    }
+
+    #[test]
+    fn basic_request_response_works() {
+        let protocol_name = ProtocolName::from("/test/req-resp/1");
+        let mut pool = LocalPool::new();
+
+        // Build swarms whose behaviour is [`RequestResponsesBehaviour`].
+        let mut swarms = (0..2)
+            .map(|_| {
+                let (tx, mut rx) = async_channel::bounded::<IncomingRequest>(64);
+
+                pool.spawner()
+                    .spawn_obj(
+                        async move {
+                            while let Some(rq) = rx.next().await {
+                                let (fb_tx, fb_rx) = oneshot::channel();
+                                assert_eq!(rq.payload, b"this is a request");
+                                let _ = rq.pending_response.send(super::OutgoingResponse {
+                                    result: Ok(b"this is a response".to_vec()),
+                                    reputation_changes: Vec::new(),
+                                    sent_feedback: Some(fb_tx),
+                                });
+                                fb_rx.await.unwrap();
+                            }
+                        }
+                        .boxed()
+                        .into(),
+                    )
+                    .unwrap();
+
+                let protocol_config = ProtocolConfig {
+                    name: protocol_name.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 1024 * 1024,
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: Some(tx),
+                };
+
+                build_swarm(iter::once(protocol_config))
+            })
+            .collect::<Vec<_>>();
+
+        // Ask `swarm[0]` to dial `swarm[1]`. There isn't any discovery mechanism in place in
+        // this test, so they wouldn't connect to each other.
+        {
+            let dial_addr = swarms[1].1.clone();
+            Swarm::dial(&mut swarms[0].0, dial_addr).unwrap();
+        }
+
+        let (mut swarm, _) = swarms.remove(0);
+        // Running `swarm[0]` in the background.
+        pool.spawner()
+            .spawn_obj({
+                async move {
+                    loop {
+                        if let SwarmEvent::Behaviour(Event::InboundRequest { result, .. }) =
+                            swarm.select_next_some().await
+                        {
+                            result.unwrap();
+                        }
+                    }
+                }
+                .boxed()
+                .into()
+            })
+            .unwrap();
+
+        // Remove and run the remaining swarm.
+        let (mut swarm, _) = swarms.remove(0);
+        pool.run_until(async move {
+            let mut response_receiver = None;
+
+            loop {
+                match swarm.select_next_some().await {
+                    SwarmEvent::ConnectionEstablished { peer_id, .. } => {
+                        let (sender, receiver) = oneshot::channel();
+                        swarm.behaviour_mut().send_request(
+                            &peer_id,
+                            protocol_name.clone(),
+                            b"this is a request".to_vec(),
+                            None,
+                            sender,
+                            IfDisconnected::ImmediateError,
+                        );
+                        assert!(response_receiver.is_none());
+                        response_receiver = Some(receiver);
+                    }
+                    SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) => {
+                        result.unwrap();
+                        break;
+                    }
+                    _ => {}
+                }
+            }
+
+            assert_eq!(
+                response_receiver.unwrap().await.unwrap().unwrap(),
+                (b"this is a response".to_vec(), protocol_name)
+            );
+        });
+    }
+
+    #[test]
+    fn max_response_size_exceeded() {
+        let protocol_name = ProtocolName::from("/test/req-resp/1");
+        let mut pool = LocalPool::new();
+
+        // Build swarms whose behaviour is [`RequestResponsesBehaviour`].
+        let mut swarms = (0..2)
+            .map(|_| {
+                let (tx, mut rx) = async_channel::bounded::<IncomingRequest>(64);
+
+                pool.spawner()
+                    .spawn_obj(
+                        async move {
+                            while let Some(rq) = rx.next().await {
+                                assert_eq!(rq.payload, b"this is a request");
+                                let _ = rq.pending_response.send(super::OutgoingResponse {
+                                    result: Ok(b"this response exceeds the limit".to_vec()),
+                                    reputation_changes: Vec::new(),
+                                    sent_feedback: None,
+                                });
+                            }
+                        }
+                        .boxed()
+                        .into(),
+                    )
+                    .unwrap();
+
+                let protocol_config = ProtocolConfig {
+                    name: protocol_name.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 8, // <-- important for the test
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: Some(tx),
+                };
+
+                build_swarm(iter::once(protocol_config))
+            })
+            .collect::<Vec<_>>();
+
+        // Ask `swarm[0]` to dial `swarm[1]`. There isn't any discovery mechanism in place in
+        // this test, so they wouldn't connect to each other.
+        {
+            let dial_addr = swarms[1].1.clone();
+            Swarm::dial(&mut swarms[0].0, dial_addr).unwrap();
+        }
+
+        // Running `swarm[0]` in the background until a `InboundRequest` event happens,
+        // which is a hint about the test having ended.
+        let (mut swarm, _) = swarms.remove(0);
+        pool.spawner()
+            .spawn_obj({
+                async move {
+                    loop {
+                        if let SwarmEvent::Behaviour(Event::InboundRequest { result, .. }) =
+                            swarm.select_next_some().await
+                        {
+                            result.unwrap();
+                            break;
+                        }
+                    }
+                }
+                .boxed()
+                .into()
+            })
+            .unwrap();
+
+        // Remove and run the remaining swarm.
+        let (mut swarm, _) = swarms.remove(0);
+        pool.run_until(async move {
+            let mut response_receiver = None;
+
+            loop {
+                match swarm.select_next_some().await {
+                    SwarmEvent::ConnectionEstablished { peer_id, .. } => {
+                        let (sender, receiver) = oneshot::channel();
+                        swarm.behaviour_mut().send_request(
+                            &peer_id,
+                            protocol_name.clone(),
+                            b"this is a request".to_vec(),
+                            None,
+                            sender,
+                            IfDisconnected::ImmediateError,
+                        );
+                        assert!(response_receiver.is_none());
+                        response_receiver = Some(receiver);
+                    }
+                    SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) => {
+                        assert!(result.is_err());
+                        break;
+                    }
+                    _ => {}
+                }
+            }
+
+            match response_receiver.unwrap().await.unwrap().unwrap_err() {
+                RequestFailure::Network(OutboundFailure::ConnectionClosed) => {}
+                _ => panic!(),
+            }
+        });
+    }
+
+    /// A [`RequestId`] is a unique identifier among either all inbound or all outbound requests for
+    /// a single [`RequestResponsesBehaviour`] behaviour. It is not guaranteed to be unique across
+    /// multiple [`RequestResponsesBehaviour`] behaviours. Thus when handling [`RequestId`] in the
+    /// context of multiple [`RequestResponsesBehaviour`] behaviours, one needs to couple the
+    /// protocol name with the [`RequestId`] to get a unique request identifier.
+    ///
+    /// This test ensures that two requests on different protocols can be handled concurrently
+    /// without a [`RequestId`] collision.
+    ///
+    /// See [`ProtocolRequestId`] for additional information.
+    #[test]
+    fn request_id_collision() {
+        let protocol_name_1 = ProtocolName::from("/test/req-resp-1/1");
+        let protocol_name_2 = ProtocolName::from("/test/req-resp-2/1");
+        let mut pool = LocalPool::new();
+
+        let mut swarm_1 = {
+            let protocol_configs = vec![
+                ProtocolConfig {
+                    name: protocol_name_1.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 1024 * 1024,
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: None,
+                },
+                ProtocolConfig {
+                    name: protocol_name_2.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 1024 * 1024,
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: None,
+                },
+            ];
+
+            build_swarm(protocol_configs.into_iter()).0
+        };
+
+        let (mut swarm_2, mut swarm_2_handler_1, mut swarm_2_handler_2, listen_add_2) = {
+            let (tx_1, rx_1) = async_channel::bounded(64);
+            let (tx_2, rx_2) = async_channel::bounded(64);
+
+            let protocol_configs = vec![
+                ProtocolConfig {
+                    name: protocol_name_1.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 1024 * 1024,
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: Some(tx_1),
+                },
+                ProtocolConfig {
+                    name: protocol_name_2.clone(),
+                    fallback_names: Vec::new(),
+                    max_request_size: 1024,
+                    max_response_size: 1024 * 1024,
+                    request_timeout: Duration::from_secs(30),
+                    inbound_queue: Some(tx_2),
+                },
+            ];
+
+            let (swarm, listen_addr) = build_swarm(protocol_configs.into_iter());
+
+            (swarm, rx_1, rx_2, listen_addr)
+        };
+
+        // Ask swarm 1 to dial swarm 2. There isn't any discovery mechanism in place in this test,
+        // so they wouldn't connect to each other.
+        swarm_1.dial(listen_add_2).unwrap();
+
+        // Run swarm 2 in the background, receiving two requests.
+        pool.spawner()
+            .spawn_obj(
+                async move {
+                    loop {
+                        if let SwarmEvent::Behaviour(Event::InboundRequest { result, .. }) =
+                            swarm_2.select_next_some().await
+                        {
+                            result.unwrap();
+                        }
+                    }
+                }
+                .boxed()
+                .into(),
+            )
+            .unwrap();
+
+        // Handle both requests sent by swarm 1 to swarm 2 in the background.
+        //
+        // Make sure both requests overlap, by answering the first only after receiving the
+        // second.
+        pool.spawner()
+            .spawn_obj(
+                async move {
+                    let protocol_1_request = swarm_2_handler_1.next().await;
+                    let protocol_2_request = swarm_2_handler_2.next().await;
+
+                    protocol_1_request
+                        .unwrap()
+                        .pending_response
+                        .send(OutgoingResponse {
+                            result: Ok(b"this is a response".to_vec()),
+                            reputation_changes: Vec::new(),
+                            sent_feedback: None,
+                        })
+                        .unwrap();
+                    protocol_2_request
+                        .unwrap()
+                        .pending_response
+                        .send(OutgoingResponse {
+                            result: Ok(b"this is a response".to_vec()),
+                            reputation_changes: Vec::new(),
+                            sent_feedback: None,
+                        })
+                        .unwrap();
+                }
+                .boxed()
+                .into(),
+            )
+            .unwrap();
+
+        // Have swarm 1 send two requests to swarm 2 and await responses.
+        pool.run_until(async move {
+            let mut response_receivers = None;
+            let mut num_responses = 0;
+
+            loop {
+                match swarm_1.select_next_some().await {
+                    SwarmEvent::ConnectionEstablished { peer_id, .. } => {
+                        let (sender_1, receiver_1) = oneshot::channel();
+                        let (sender_2, receiver_2) = oneshot::channel();
+                        swarm_1.behaviour_mut().send_request(
+                            &peer_id,
+                            protocol_name_1.clone(),
+                            b"this is a request".to_vec(),
+                            None,
+                            sender_1,
+                            IfDisconnected::ImmediateError,
+                        );
+                        swarm_1.behaviour_mut().send_request(
+                            &peer_id,
+                            protocol_name_2.clone(),
+                            b"this is a request".to_vec(),
+                            None,
+                            sender_2,
+                            IfDisconnected::ImmediateError,
+                        );
+                        assert!(response_receivers.is_none());
+                        response_receivers = Some((receiver_1, receiver_2));
+                    }
+                    SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) => {
+                        num_responses += 1;
+                        result.unwrap();
+                        if num_responses == 2 {
+                            break;
+                        }
+                    }
+                    _ => {}
+                }
+            }
+            let (response_receiver_1, response_receiver_2) = response_receivers.unwrap();
+            assert_eq!(
+                response_receiver_1.await.unwrap().unwrap(),
+                (b"this is a response".to_vec(), protocol_name_1)
+            );
+            assert_eq!(
+                response_receiver_2.await.unwrap().unwrap(),
+                (b"this is a response".to_vec(), protocol_name_2)
+            );
+        });
+    }
+
+    #[test]
+    fn request_fallback() {
+        let protocol_name_1 = ProtocolName::from("/test/req-resp/2");
+        let protocol_name_1_fallback = ProtocolName::from("/test/req-resp/1");
+        let protocol_name_2 = ProtocolName::from("/test/another");
+        let mut pool = LocalPool::new();
+
+        let protocol_config_1 = ProtocolConfig {
+            name: protocol_name_1.clone(),
+            fallback_names: Vec::new(),
+            max_request_size: 1024,
+            max_response_size: 1024 * 1024,
+            request_timeout: Duration::from_secs(30),
+            inbound_queue: None,
+        };
+        let protocol_config_1_fallback = ProtocolConfig {
+            name: protocol_name_1_fallback.clone(),
+            fallback_names: Vec::new(),
+            max_request_size: 1024,
+            max_response_size: 1024 * 1024,
+            request_timeout: Duration::from_secs(30),
+            inbound_queue: None,
+        };
+        let protocol_config_2 = ProtocolConfig {
+            name: protocol_name_2.clone(),
+            fallback_names: Vec::new(),
+            max_request_size: 1024,
+            max_response_size: 1024 * 1024,
+            request_timeout: Duration::from_secs(30),
+            inbound_queue: None,
+        };
+
+        // This swarm only speaks protocol_name_1_fallback and protocol_name_2.
+        // It only responds to requests.
+        let mut older_swarm = {
+            let (tx_1, mut rx_1) = async_channel::bounded::<IncomingRequest>(64);
+            let (tx_2, mut rx_2) = async_channel::bounded::<IncomingRequest>(64);
+            let mut protocol_config_1_fallback = protocol_config_1_fallback.clone();
+            protocol_config_1_fallback.inbound_queue = Some(tx_1);
+
+            let mut protocol_config_2 = protocol_config_2.clone();
+            protocol_config_2.inbound_queue = Some(tx_2);
+
+            pool.spawner()
+                .spawn_obj(
+                    async move {
+                        for _ in 0..2 {
+                            if let Some(rq) = rx_1.next().await {
+                                let (fb_tx, fb_rx) = oneshot::channel();
+                                assert_eq!(rq.payload, b"request on protocol /test/req-resp/1");
+                                let _ = rq.pending_response.send(super::OutgoingResponse {
+                                    result: Ok(
+                                        b"this is a response on protocol /test/req-resp/1".to_vec()
+                                    ),
+                                    reputation_changes: Vec::new(),
+                                    sent_feedback: Some(fb_tx),
+                                });
+                                fb_rx.await.unwrap();
+                            }
+                        }
+
+                        if let Some(rq) = rx_2.next().await {
+                            let (fb_tx, fb_rx) = oneshot::channel();
+                            assert_eq!(rq.payload, b"request on protocol /test/other");
+                            let _ = rq.pending_response.send(super::OutgoingResponse {
+                                result: Ok(b"this is a response on protocol /test/other".to_vec()),
+                                reputation_changes: Vec::new(),
+                                sent_feedback: Some(fb_tx),
+                            });
+                            fb_rx.await.unwrap();
+                        }
+                    }
+                    .boxed()
+                    .into(),
+                )
+                .unwrap();
+
+            build_swarm(vec![protocol_config_1_fallback, protocol_config_2].into_iter())
+        };
+
+        // This swarm speaks all protocols.
+        let mut new_swarm = build_swarm(
+            vec![
+                protocol_config_1.clone(),
+                protocol_config_1_fallback.clone(),
+                protocol_config_2.clone(),
+            ]
+            .into_iter(),
+        );
+
+        {
+            let dial_addr = older_swarm.1.clone();
+            Swarm::dial(&mut new_swarm.0, dial_addr).unwrap();
+        }
+
+        // Running `older_swarm`` in the background.
+        pool.spawner()
+            .spawn_obj({
+                async move {
+                    loop {
+                        _ = older_swarm.0.select_next_some().await;
+                    }
+                }
+                .boxed()
+                .into()
+            })
+            .unwrap();
+
+        // Run the newer swarm. Attempt to make requests on all protocols.
+        let (mut swarm, _) = new_swarm;
+        let mut older_peer_id = None;
+
+        pool.run_until(async move {
+            let mut response_receiver = None;
+            // Try the new protocol with a fallback.
+            loop {
+                match swarm.select_next_some().await {
+                    SwarmEvent::ConnectionEstablished { peer_id, .. } => {
+                        older_peer_id = Some(peer_id);
+                        let (sender, receiver) = oneshot::channel();
+                        swarm.behaviour_mut().send_request(
+                            &peer_id,
+                            protocol_name_1.clone(),
+                            b"request on protocol /test/req-resp/2".to_vec(),
+                            Some((
+                                b"request on protocol /test/req-resp/1".to_vec(),
+                                protocol_config_1_fallback.name.clone(),
+                            )),
+                            sender,
+                            IfDisconnected::ImmediateError,
+                        );
+                        response_receiver = Some(receiver);
+                    }
+                    SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) => {
+                        result.unwrap();
+                        break;
+                    }
+                    _ => {}
+                }
+            }
+            assert_eq!(
+                response_receiver.unwrap().await.unwrap().unwrap(),
+                (
+                    b"this is a response on protocol /test/req-resp/1".to_vec(),
+                    protocol_name_1_fallback.clone()
+                )
+            );
+            // Try the old protocol with a useless fallback.
+            let (sender, response_receiver) = oneshot::channel();
+            swarm.behaviour_mut().send_request(
+                older_peer_id.as_ref().unwrap(),
+                protocol_name_1_fallback.clone(),
+                b"request on protocol /test/req-resp/1".to_vec(),
+                Some((
+                    b"dummy request, will fail if processed".to_vec(),
+                    protocol_config_1_fallback.name.clone(),
+                )),
+                sender,
+                IfDisconnected::ImmediateError,
+            );
+            loop {
+                if let SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) =
+                    swarm.select_next_some().await
+                {
+                    result.unwrap();
+                    break;
+                }
+            }
+            assert_eq!(
+                response_receiver.await.unwrap().unwrap(),
+                (
+                    b"this is a response on protocol /test/req-resp/1".to_vec(),
+                    protocol_name_1_fallback.clone()
+                )
+            );
+            // Try the new protocol with no fallback. Should fail.
+            let (sender, response_receiver) = oneshot::channel();
+            swarm.behaviour_mut().send_request(
+                older_peer_id.as_ref().unwrap(),
+                protocol_name_1.clone(),
+                b"request on protocol /test/req-resp-2".to_vec(),
+                None,
+                sender,
+                IfDisconnected::ImmediateError,
+            );
+            loop {
+                if let SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) =
+                    swarm.select_next_some().await
+                {
+                    assert_matches!(
+                        result.unwrap_err(),
+                        RequestFailure::Network(OutboundFailure::UnsupportedProtocols)
+                    );
+                    break;
+                }
+            }
+            assert!(response_receiver.await.unwrap().is_err());
+            // Try the other protocol with no fallback.
+            let (sender, response_receiver) = oneshot::channel();
+            swarm.behaviour_mut().send_request(
+                older_peer_id.as_ref().unwrap(),
+                protocol_name_2.clone(),
+                b"request on protocol /test/other".to_vec(),
+                None,
+                sender,
+                IfDisconnected::ImmediateError,
+            );
+            loop {
+                if let SwarmEvent::Behaviour(Event::RequestFinished { result, .. }) =
+                    swarm.select_next_some().await
+                {
+                    result.unwrap();
+                    break;
+                }
+            }
+            assert_eq!(
+                response_receiver.await.unwrap().unwrap(),
+                (
+                    b"this is a response on protocol /test/other".to_vec(),
+                    protocol_name_2.clone()
+                )
+            );
+        });
+    }
+}
diff --git a/libp2p/src/service.rs b/libp2p/src/service.rs
new file mode 100644
index 000000000..bf64539e7
--- /dev/null
+++ b/libp2p/src/service.rs
@@ -0,0 +1,1776 @@
+// 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/>.
+
+//! Main entry point of the sc-network crate.
+//!
+//! There are two main structs in this module: [`NetworkWorker`] and [`NetworkService`].
+//! The [`NetworkWorker`] *is* the network. Network is driven by [`NetworkWorker::run`] future that
+//! terminates only when all instances of the control handles [`NetworkService`] were dropped.
+//! The [`NetworkService`] is merely a shared version of the [`NetworkWorker`]. You can obtain an
+//! `Arc<NetworkService>` by calling [`NetworkWorker::service`].
+//!
+//! The methods of the [`NetworkService`] are implemented by sending a message over a channel,
+//! which is then processed by [`NetworkWorker::next_action`].
+
+use crate::{
+    behaviour::{self, Behaviour, BehaviourOut},
+    config::{parse_addr, FullNetworkConfiguration, MultiaddrWithPeerId, Params, TransportConfig},
+    discovery::DiscoveryConfig,
+    error::Error,
+    event::{DhtEvent, Event},
+    network_state::{
+        NetworkState, NotConnectedPeer as NetworkStateNotConnectedPeer, Peer as NetworkStatePeer,
+    },
+    peer_store::{PeerStoreHandle, PeerStoreProvider},
+    protocol::{self, NotifsHandlerError, Protocol, Ready},
+    protocol_controller::{self, ProtoSetConfig, ProtocolController, SetId},
+    request_responses::{IfDisconnected, RequestFailure},
+    service::{
+        signature::{Signature, SigningError},
+        traits::{
+            NetworkDHTProvider, NetworkEventStream, NetworkNotification, NetworkPeers,
+            NetworkRequest, NetworkSigner, NetworkStateInfo, NetworkStatus, NetworkStatusProvider,
+            NotificationSender as NotificationSenderT, NotificationSenderError,
+            NotificationSenderReady as NotificationSenderReadyT,
+        },
+    },
+    transport,
+    types::ProtocolName,
+    ReputationChange,
+};
+
+use codec::DecodeAll;
+use either::Either;
+use futures::{channel::oneshot, prelude::*};
+#[allow(deprecated)]
+use libp2p::{
+    connection_limits::Exceeded,
+    core::{upgrade, ConnectedPoint, Endpoint},
+    identify::Info as IdentifyInfo,
+    kad::record::Key as KademliaKey,
+    multiaddr,
+    ping::Failure as PingFailure,
+    swarm::{
+        AddressScore, ConnectionError, ConnectionId, ConnectionLimits, DialError, Executor,
+        ListenError, NetworkBehaviour, Swarm, SwarmBuilder, SwarmEvent, THandlerErr,
+    },
+    Multiaddr, PeerId,
+};
+use log::{debug, error, info, trace, warn};
+use metrics::{Histogram, MetricSources, Metrics};
+use parking_lot::Mutex;
+
+use sc_network_common::role::{ObservedRole, Roles};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+
+use std::{
+    cmp,
+    collections::{HashMap, HashSet},
+    fs, iter,
+    num::NonZeroUsize,
+    pin::Pin,
+    str,
+    sync::{
+        atomic::{AtomicUsize, Ordering},
+        Arc,
+    },
+};
+
+pub use behaviour::{InboundFailure, OutboundFailure, ResponseFailure};
+pub use libp2p::identity::{DecodingError, Keypair, PublicKey};
+pub use protocol::NotificationsSink;
+
+mod metrics;
+mod out_events;
+
+pub mod signature;
+pub mod traits;
+
+/// Substrate network service. Handles network IO and manages connectivity.
+pub struct NetworkService {
+    /// Number of peers we're connected to.
+    num_connected: Arc<AtomicUsize>,
+    /// The local external addresses.
+    external_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+    /// Listen addresses. Do **NOT** include a trailing `/p2p/` with our `PeerId`.
+    listen_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+    /// Local copy of the `PeerId` of the local node.
+    local_peer_id: PeerId,
+    /// The `KeyPair` that defines the `PeerId` of the local node.
+    local_identity: Keypair,
+    /// Bandwidth logging system. Can be queried to know the average bandwidth consumed.
+    bandwidth: Arc<transport::BandwidthSinks>,
+    /// Used to query and report reputation changes.
+    peer_store_handle: PeerStoreHandle,
+    /// Channel that sends messages to the actual worker.
+    to_worker: TracingUnboundedSender<ServiceToWorkerMsg>,
+    /// Protocol name -> `SetId` mapping for notification protocols. The map never changes after
+    /// initialization.
+    notification_protocol_ids: HashMap<ProtocolName, SetId>,
+    /// Handles to manage peer connections on notification protocols. The vector never changes
+    /// after initialization.
+    protocol_handles: Vec<protocol_controller::ProtocolHandle>,
+}
+
+impl NetworkWorker {
+    /// Creates the network service.
+    ///
+    /// Returns a `NetworkWorker` that implements `Future` and must be regularly polled in order
+    /// for the network processing to advance. From it, you can extract a `NetworkService` using
+    /// `worker.service()`. The `NetworkService` can be shared through the codebase.
+    #[allow(clippy::result_large_err)]
+    pub fn new(params: Params) -> Result<Self, Error> {
+        let FullNetworkConfiguration {
+            notification_protocols,
+            request_response_protocols,
+            mut network_config,
+        } = params.network_config;
+
+        // Private and public keys configuration.
+        let local_identity = network_config.node_key.clone().into_keypair()?;
+        let local_public = local_identity.public();
+        let local_peer_id = local_public.to_peer_id();
+
+        network_config
+            .boot_nodes
+            .retain(|boot_node| boot_node.peer_id != local_peer_id);
+        network_config
+            .default_peers_set
+            .reserved_nodes
+            .retain(|reserved_node| {
+                if reserved_node.peer_id == local_peer_id {
+                    warn!(
+                        target: "sub-libp2p",
+                        "Local peer ID used in reserved node, ignoring: {}",
+                        reserved_node,
+                    );
+                    false
+                } else {
+                    true
+                }
+            });
+
+        // Ensure the listen addresses are consistent with the transport.
+        ensure_addresses_consistent_with_transport(
+            network_config.listen_addresses.iter(),
+            &network_config.transport,
+        )?;
+        ensure_addresses_consistent_with_transport(
+            network_config.boot_nodes.iter().map(|x| &x.multiaddr),
+            &network_config.transport,
+        )?;
+        ensure_addresses_consistent_with_transport(
+            network_config
+                .default_peers_set
+                .reserved_nodes
+                .iter()
+                .map(|x| &x.multiaddr),
+            &network_config.transport,
+        )?;
+        for notification_protocol in &notification_protocols {
+            ensure_addresses_consistent_with_transport(
+                notification_protocol
+                    .set_config()
+                    .reserved_nodes
+                    .iter()
+                    .map(|x| &x.multiaddr),
+                &network_config.transport,
+            )?;
+        }
+        ensure_addresses_consistent_with_transport(
+            network_config.public_addresses.iter(),
+            &network_config.transport,
+        )?;
+
+        let (to_worker, from_service) = tracing_unbounded("mpsc_network_worker", 100_000);
+
+        if let Some(path) = &network_config.net_config_path {
+            fs::create_dir_all(path)?;
+        }
+
+        info!(
+            target: "sub-libp2p",
+            "🏷  Local node identity is: {}",
+            local_peer_id.to_base58(),
+        );
+
+        let (transport, bandwidth) = {
+            let config_mem = match network_config.transport {
+                TransportConfig::MemoryOnly => true,
+                TransportConfig::Normal { .. } => false,
+            };
+
+            // The yamux buffer size limit is configured to be equal to the maximum frame size
+            // of all protocols. 10 bytes are added to each limit for the length prefix that
+            // is not included in the upper layer protocols limit but is still present in the
+            // yamux buffer. These 10 bytes correspond to the maximum size required to encode
+            // a variable-length-encoding 64bits number. In other words, we make the
+            // assumption that no notification larger than 2^64 will ever be sent.
+            let yamux_maximum_buffer_size = {
+                let requests_max = request_response_protocols
+                    .iter()
+                    .map(|cfg| usize::try_from(cfg.max_request_size).unwrap_or(usize::MAX));
+                let responses_max = request_response_protocols
+                    .iter()
+                    .map(|cfg| usize::try_from(cfg.max_response_size).unwrap_or(usize::MAX));
+                let notifs_max = notification_protocols
+                    .iter()
+                    .map(|cfg| usize::try_from(cfg.max_notification_size()).unwrap_or(usize::MAX));
+
+                // A "default" max is added to cover all the other protocols: ping, identify,
+                // kademlia, block announces, and transactions.
+                let default_max = cmp::max(
+                    1024 * 1024,
+                    usize::try_from(protocol::BLOCK_ANNOUNCES_TRANSACTIONS_SUBSTREAM_SIZE)
+                        .unwrap_or(usize::MAX),
+                );
+
+                iter::once(default_max)
+                    .chain(requests_max)
+                    .chain(responses_max)
+                    .chain(notifs_max)
+                    .max()
+                    .expect("iterator known to always yield at least one element; qed")
+                    .saturating_add(10)
+            };
+
+            transport::build_transport(
+                local_identity.clone(),
+                config_mem,
+                network_config.yamux_window_size,
+                yamux_maximum_buffer_size,
+            )
+        };
+
+        let (to_notifications, from_protocol_controllers) =
+            tracing_unbounded("mpsc_protocol_controllers_to_notifications", 10_000);
+
+        // We must prepend a hardcoded default peer set to notification protocols.
+        let all_peer_sets_iter = iter::once(&network_config.default_peers_set).chain(
+            notification_protocols
+                .iter()
+                .map(|protocol| protocol.set_config()),
+        );
+
+        let (protocol_handles, protocol_controllers): (Vec<_>, Vec<_>) = all_peer_sets_iter
+            .enumerate()
+            .map(|(set_id, set_config)| {
+                let proto_set_config = ProtoSetConfig {
+                    in_peers: set_config.in_peers,
+                    out_peers: set_config.out_peers,
+                    reserved_nodes: set_config
+                        .reserved_nodes
+                        .iter()
+                        .map(|node| node.peer_id)
+                        .collect(),
+                    reserved_only: set_config.non_reserved_mode.is_reserved_only(),
+                };
+
+                ProtocolController::new(
+                    SetId::from(set_id),
+                    proto_set_config,
+                    to_notifications.clone(),
+                    Box::new(params.peer_store.clone()),
+                )
+            })
+            .unzip();
+
+        // Spawn `ProtocolController` runners.
+        protocol_controllers
+            .into_iter()
+            .for_each(|controller| (params.executor)(controller.run().boxed()));
+
+        // Protocol name to protocol id mapping. The first protocol is always block announce (sync)
+        // protocol, aka default (hardcoded) peer set.
+        let notification_protocol_ids: HashMap<ProtocolName, SetId> = notification_protocols
+            .iter()
+            .enumerate()
+            .map(|(index, protocol)| (protocol.protocol_name().clone(), SetId::from(index)))
+            .collect();
+
+        let known_addresses = {
+            // Collect all reserved nodes and bootnodes addresses.
+            let mut addresses: Vec<_> = network_config
+                .default_peers_set
+                .reserved_nodes
+                .iter()
+                .map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
+                .chain(notification_protocols.iter().flat_map(|protocol| {
+                    protocol
+                        .set_config()
+                        .reserved_nodes
+                        .iter()
+                        .map(|reserved| (reserved.peer_id, reserved.multiaddr.clone()))
+                }))
+                .chain(
+                    network_config
+                        .boot_nodes
+                        .iter()
+                        .map(|bootnode| (bootnode.peer_id, bootnode.multiaddr.clone())),
+                )
+                .collect();
+
+            // Remove possible duplicates.
+            addresses.sort();
+            addresses.dedup();
+
+            addresses
+        };
+
+        // Check for duplicate bootnodes.
+        network_config.boot_nodes.iter().try_for_each(|bootnode| {
+            if let Some(other) = network_config
+                .boot_nodes
+                .iter()
+                .filter(|o| o.multiaddr == bootnode.multiaddr)
+                .find(|o| o.peer_id != bootnode.peer_id)
+            {
+                Err(Error::DuplicateBootnode {
+                    address: bootnode.multiaddr.clone(),
+                    first_id: bootnode.peer_id,
+                    second_id: other.peer_id,
+                })
+            } else {
+                Ok(())
+            }
+        })?;
+
+        // List of bootnode multiaddresses.
+        let mut boot_node_ids = HashMap::<PeerId, Vec<Multiaddr>>::new();
+
+        for bootnode in network_config.boot_nodes.iter() {
+            boot_node_ids
+                .entry(bootnode.peer_id)
+                .or_default()
+                .push(bootnode.multiaddr.clone());
+        }
+
+        let boot_node_ids = Arc::new(boot_node_ids);
+
+        let num_connected = Arc::new(AtomicUsize::new(0));
+        let external_addresses = Arc::new(Mutex::new(HashSet::new()));
+
+        let (protocol, notif_protocol_handles) = Protocol::new(
+            From::from(&params.role),
+            &params.metrics_registry,
+            notification_protocols,
+            params.peer_store.clone(),
+            protocol_handles.clone(),
+            from_protocol_controllers,
+        )?;
+
+        // Build the swarm.
+        let (mut swarm, bandwidth): (Swarm<Behaviour>, _) = {
+            let user_agent = format!(
+                "{} ({})",
+                network_config.client_version, network_config.node_name
+            );
+
+            let discovery_config = {
+                let mut config = DiscoveryConfig::new(local_public.to_peer_id());
+                config.with_permanent_addresses(known_addresses);
+                config.discovery_limit(u64::from(network_config.default_peers_set.out_peers) + 15);
+                config.with_kademlia(
+                    params.genesis_hash,
+                    params.fork_id.as_deref(),
+                    &params.protocol_id,
+                );
+                config.with_dht_random_walk(network_config.enable_dht_random_walk);
+                config.allow_non_globals_in_dht(network_config.allow_non_globals_in_dht);
+                config.use_kademlia_disjoint_query_paths(
+                    network_config.kademlia_disjoint_query_paths,
+                );
+                config.with_kademlia_replication_factor(network_config.kademlia_replication_factor);
+
+                match network_config.transport {
+                    TransportConfig::MemoryOnly => {
+                        config.with_mdns(false);
+                        config.allow_private_ip(false);
+                    }
+                    TransportConfig::Normal {
+                        enable_mdns,
+                        allow_private_ip: allow_private_ipv4,
+                        ..
+                    } => {
+                        config.with_mdns(enable_mdns);
+                        config.allow_private_ip(allow_private_ipv4);
+                    }
+                }
+
+                config
+            };
+
+            let behaviour = {
+                let result = Behaviour::new(
+                    protocol,
+                    user_agent,
+                    local_public,
+                    discovery_config,
+                    request_response_protocols,
+                    params.peer_store.clone(),
+                    external_addresses.clone(),
+                );
+
+                match result {
+                    Ok(b) => b,
+                    Err(crate::request_responses::RegisterError::DuplicateProtocol(proto)) => {
+                        return Err(Error::DuplicateRequestResponseProtocol { protocol: proto })
+                    }
+                }
+            };
+
+            let builder = {
+                struct SpawnImpl<F>(F);
+                impl<F: Fn(Pin<Box<dyn Future<Output = ()> + Send>>)> Executor for SpawnImpl<F> {
+                    fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) {
+                        (self.0)(f)
+                    }
+                }
+                SwarmBuilder::with_executor(
+                    transport,
+                    behaviour,
+                    local_peer_id,
+                    SpawnImpl(params.executor),
+                )
+            };
+            #[allow(deprecated)]
+            let builder = builder
+                .connection_limits(
+                    ConnectionLimits::default()
+                        .with_max_established_per_peer(Some(crate::MAX_CONNECTIONS_PER_PEER as u32))
+                        .with_max_established_incoming(Some(
+                            crate::MAX_CONNECTIONS_ESTABLISHED_INCOMING,
+                        )),
+                )
+                .substream_upgrade_protocol_override(upgrade::Version::V1Lazy)
+                .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>
+                .per_connection_event_buffer_size(24)
+                .max_negotiating_inbound_streams(2048);
+
+            (builder.build(), bandwidth)
+        };
+
+        // Initialize the metrics.
+        let metrics = match &params.metrics_registry {
+            Some(registry) => Some(metrics::register(
+                registry,
+                MetricSources {
+                    bandwidth: bandwidth.clone(),
+                    connected_peers: num_connected.clone(),
+                },
+            )?),
+            None => None,
+        };
+
+        // Listen on multiaddresses.
+        for addr in &network_config.listen_addresses {
+            if let Err(err) = Swarm::<Behaviour>::listen_on(&mut swarm, addr.clone()) {
+                warn!(target: "sub-libp2p", "Can't listen on {} because: {:?}", addr, err)
+            }
+        }
+
+        // Add external addresses.
+        for addr in &network_config.public_addresses {
+            Swarm::<Behaviour>::add_external_address(
+                &mut swarm,
+                addr.clone(),
+                AddressScore::Infinite,
+            );
+        }
+
+        let listen_addresses = Arc::new(Mutex::new(HashSet::new()));
+
+        let service = Arc::new(NetworkService {
+            bandwidth,
+            external_addresses,
+            listen_addresses: listen_addresses.clone(),
+            num_connected: num_connected.clone(),
+            local_peer_id,
+            local_identity,
+            to_worker,
+            notification_protocol_ids,
+            protocol_handles,
+            peer_store_handle: params.peer_store.clone(),
+        });
+
+        Ok(NetworkWorker {
+            listen_addresses,
+            num_connected,
+            network_service: swarm,
+            service,
+            from_service,
+            event_streams: out_events::OutChannels::new(params.metrics_registry.as_ref())?,
+            metrics,
+            boot_node_ids,
+            reported_invalid_boot_nodes: Default::default(),
+            peer_store_handle: params.peer_store,
+            notif_protocol_handles,
+        })
+    }
+
+    /// High-level network status information.
+    pub fn status(&self) -> NetworkStatus {
+        NetworkStatus {
+            num_connected_peers: self.num_connected_peers(),
+            total_bytes_inbound: self.total_bytes_inbound(),
+            total_bytes_outbound: self.total_bytes_outbound(),
+        }
+    }
+
+    /// Returns the total number of bytes received so far.
+    pub fn total_bytes_inbound(&self) -> u64 {
+        self.service.bandwidth.total_inbound()
+    }
+
+    /// Returns the total number of bytes sent so far.
+    pub fn total_bytes_outbound(&self) -> u64 {
+        self.service.bandwidth.total_outbound()
+    }
+
+    /// Returns the number of peers we're connected to.
+    pub fn num_connected_peers(&self) -> usize {
+        self.network_service
+            .behaviour()
+            .user_protocol()
+            .num_sync_peers()
+    }
+
+    /// Adds an address for a node.
+    pub fn add_known_address(&mut self, peer_id: PeerId, addr: Multiaddr) {
+        self.network_service
+            .behaviour_mut()
+            .add_known_address(peer_id, addr);
+    }
+
+    /// Return a `NetworkService` that can be shared through the code base and can be used to
+    /// manipulate the worker.
+    pub fn service(&self) -> &Arc<NetworkService> {
+        &self.service
+    }
+
+    /// Returns the local `PeerId`.
+    pub fn local_peer_id(&self) -> &PeerId {
+        Swarm::<Behaviour>::local_peer_id(&self.network_service)
+    }
+
+    /// Returns the list of addresses we are listening on.
+    ///
+    /// Does **NOT** include a trailing `/p2p/` with our `PeerId`.
+    pub fn listen_addresses(&self) -> impl Iterator<Item = &Multiaddr> {
+        Swarm::<Behaviour>::listeners(&self.network_service)
+    }
+
+    /// Get network state.
+    ///
+    /// **Note**: Use this only for debugging. This API is unstable. There are warnings literally
+    /// everywhere about this. Please don't use this function to retrieve actual information.
+    pub fn network_state(&mut self) -> NetworkState {
+        let swarm = &mut self.network_service;
+        let open = swarm
+            .behaviour_mut()
+            .user_protocol()
+            .open_peers()
+            .cloned()
+            .collect::<Vec<_>>();
+        let connected_peers = {
+            let swarm = &mut *swarm;
+            open.iter()
+				.filter_map(move |peer_id| {
+					let known_addresses = if let Ok(addrs) =
+						NetworkBehaviour::handle_pending_outbound_connection(
+							swarm.behaviour_mut(),
+							ConnectionId::new_unchecked(0), // dummy value
+							Some(*peer_id),
+							&[],
+							Endpoint::Listener,
+						) {
+						addrs.into_iter().collect()
+					} else {
+						error!(target: "sub-libp2p", "Was not able to get known addresses for {:?}", peer_id);
+						return None
+					};
+
+					let endpoint = if let Some(e) =
+						swarm.behaviour_mut().node(peer_id).and_then(|i| i.endpoint())
+					{
+						e.clone().into()
+					} else {
+						error!(target: "sub-libp2p", "Found state inconsistency between custom protocol \
+						and debug information about {:?}", peer_id);
+						return None
+					};
+
+					Some((
+						peer_id.to_base58(),
+						NetworkStatePeer {
+							endpoint,
+							version_string: swarm
+								.behaviour_mut()
+								.node(peer_id)
+								.and_then(|i| i.client_version().map(|s| s.to_owned())),
+							latest_ping_time: swarm
+								.behaviour_mut()
+								.node(peer_id)
+								.and_then(|i| i.latest_ping()),
+							known_addresses,
+						},
+					))
+				})
+				.collect()
+        };
+
+        let not_connected_peers = {
+            let swarm = &mut *swarm;
+            swarm
+				.behaviour_mut()
+				.known_peers()
+				.into_iter()
+				.filter(|p| open.iter().all(|n| n != p))
+				.map(move |peer_id| {
+					let known_addresses = if let Ok(addrs) =
+						NetworkBehaviour::handle_pending_outbound_connection(
+							swarm.behaviour_mut(),
+							ConnectionId::new_unchecked(0), // dummy value
+							Some(peer_id),
+							&[],
+							Endpoint::Listener,
+						) {
+						addrs.into_iter().collect()
+					} else {
+						error!(target: "sub-libp2p", "Was not able to get known addresses for {:?}", peer_id);
+						Default::default()
+					};
+
+					(
+						peer_id.to_base58(),
+						NetworkStateNotConnectedPeer {
+							version_string: swarm
+								.behaviour_mut()
+								.node(&peer_id)
+								.and_then(|i| i.client_version().map(|s| s.to_owned())),
+							latest_ping_time: swarm
+								.behaviour_mut()
+								.node(&peer_id)
+								.and_then(|i| i.latest_ping()),
+							known_addresses,
+						},
+					)
+				})
+				.collect()
+        };
+
+        let peer_id = Swarm::<Behaviour>::local_peer_id(swarm).to_base58();
+        let listened_addresses = swarm.listeners().cloned().collect();
+        let external_addresses = swarm
+            .external_addresses()
+            .map(|r| &r.addr)
+            .cloned()
+            .collect();
+
+        NetworkState {
+            peer_id,
+            listened_addresses,
+            external_addresses,
+            connected_peers,
+            not_connected_peers,
+            // TODO: Check what info we can include here.
+            //       Issue reference: https://github.com/paritytech/substrate/issues/14160.
+            peerset: serde_json::json!(
+                "Unimplemented. See https://github.com/paritytech/substrate/issues/14160."
+            ),
+        }
+    }
+
+    /// Removes a `PeerId` from the list of reserved peers.
+    pub fn remove_reserved_peer(&self, peer: PeerId) {
+        self.service.remove_reserved_peer(peer);
+    }
+
+    /// Adds a `PeerId` and its `Multiaddr` as reserved.
+    pub fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
+        self.service.add_reserved_peer(peer)
+    }
+}
+
+impl NetworkService {
+    /// Get network state.
+    ///
+    /// **Note**: Use this only for debugging. This API is unstable. There are warnings literally
+    /// everywhere about this. Please don't use this function to retrieve actual information.
+    ///
+    /// Returns an error if the `NetworkWorker` is no longer running.
+    pub async fn network_state(&self) -> Result<NetworkState, ()> {
+        let (tx, rx) = oneshot::channel();
+
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::NetworkState {
+                pending_response: tx,
+            });
+
+        match rx.await {
+            Ok(v) => v.map_err(|_| ()),
+            // The channel can only be closed if the network worker no longer exists.
+            Err(_) => Err(()),
+        }
+    }
+
+    /// Utility function to extract `PeerId` from each `Multiaddr` for peer set updates.
+    ///
+    /// Returns an `Err` if one of the given addresses is invalid or contains an
+    /// invalid peer ID (which includes the local peer ID).
+    fn split_multiaddr_and_peer_id(
+        &self,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<Vec<(PeerId, Multiaddr)>, String> {
+        peers
+            .into_iter()
+            .map(|mut addr| {
+                let peer = match addr.pop() {
+                    Some(multiaddr::Protocol::P2p(key)) => PeerId::from_multihash(key)
+                        .map_err(|_| "Invalid PeerId format".to_string())?,
+                    _ => return Err("Missing PeerId from address".to_string()),
+                };
+
+                // Make sure the local peer ID is never added to the PSM
+                // or added as a "known address", even if given.
+                if peer == self.local_peer_id {
+                    Err("Local peer ID in peer set.".to_string())
+                } else {
+                    Ok((peer, addr))
+                }
+            })
+            .collect::<Result<Vec<(PeerId, Multiaddr)>, String>>()
+    }
+}
+
+impl NetworkStateInfo for NetworkService {
+    /// Returns the local external addresses.
+    fn external_addresses(&self) -> Vec<Multiaddr> {
+        self.external_addresses.lock().iter().cloned().collect()
+    }
+
+    /// Returns the listener addresses (without trailing `/p2p/` with our `PeerId`).
+    fn listen_addresses(&self) -> Vec<Multiaddr> {
+        self.listen_addresses.lock().iter().cloned().collect()
+    }
+
+    /// Returns the local Peer ID.
+    fn local_peer_id(&self) -> PeerId {
+        self.local_peer_id
+    }
+}
+
+impl NetworkSigner for NetworkService {
+    fn sign_with_local_identity(&self, msg: impl AsRef<[u8]>) -> Result<Signature, SigningError> {
+        Signature::sign_message(msg.as_ref(), &self.local_identity)
+    }
+}
+
+impl NetworkDHTProvider for NetworkService {
+    /// Start getting a value from the DHT.
+    ///
+    /// This will generate either a `ValueFound` or a `ValueNotFound` event and pass it as an
+    /// item on the [`NetworkWorker`] stream.
+    fn get_value(&self, key: &KademliaKey) {
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::GetValue(key.clone()));
+    }
+
+    /// Start putting a value in the DHT.
+    ///
+    /// This will generate either a `ValuePut` or a `ValuePutFailed` event and pass it as an
+    /// item on the [`NetworkWorker`] stream.
+    fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::PutValue(key, value));
+    }
+}
+
+#[async_trait::async_trait]
+impl NetworkStatusProvider for NetworkService {
+    async fn status(&self) -> Result<NetworkStatus, ()> {
+        let (tx, rx) = oneshot::channel();
+
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::NetworkStatus {
+                pending_response: tx,
+            });
+
+        match rx.await {
+            Ok(v) => v.map_err(|_| ()),
+            // The channel can only be closed if the network worker no longer exists.
+            Err(_) => Err(()),
+        }
+    }
+}
+
+impl NetworkPeers for NetworkService {
+    fn set_authorized_peers(&self, _peers: HashSet<PeerId>) {}
+
+    fn set_authorized_only(&self, _reserved_only: bool) {}
+
+    fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr) {
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::AddKnownAddress(peer_id, addr));
+    }
+
+    fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
+        self.peer_store_handle
+            .clone()
+            .report_peer(peer_id, cost_benefit);
+    }
+
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
+        self.peer_store_handle.peer_reputation(peer_id)
+    }
+
+    fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName) {
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::DisconnectPeer(peer_id, protocol));
+    }
+
+    fn accept_unreserved_peers(&self) {}
+
+    fn deny_unreserved_peers(&self) {}
+
+    fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
+        // Make sure the local peer ID is never added as a reserved peer.
+        if peer.peer_id == self.local_peer_id {
+            return Err("Local peer ID cannot be added as a reserved peer.".to_string());
+        }
+
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::AddKnownAddress(
+                peer.peer_id,
+                peer.multiaddr,
+            ));
+        Ok(())
+    }
+
+    fn remove_reserved_peer(&self, _peer_id: PeerId) {}
+
+    fn set_reserved_peers(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String> {
+        let Some(set_id) = self.notification_protocol_ids.get(&protocol) else {
+            return Err(format!(
+                "Cannot set reserved peers for unknown protocol: {}",
+                protocol
+            ));
+        };
+
+        let peers_addrs = self.split_multiaddr_and_peer_id(peers)?;
+
+        let mut peers: HashSet<PeerId> = HashSet::with_capacity(peers_addrs.len());
+
+        for (peer_id, addr) in peers_addrs.into_iter() {
+            // Make sure the local peer ID is never added to the PSM.
+            if peer_id == self.local_peer_id {
+                return Err("Local peer ID cannot be added as a reserved peer.".to_string());
+            }
+
+            peers.insert(peer_id);
+
+            if !addr.is_empty() {
+                let _ = self
+                    .to_worker
+                    .unbounded_send(ServiceToWorkerMsg::AddKnownAddress(peer_id, addr));
+            }
+        }
+
+        self.protocol_handles[usize::from(*set_id)].set_reserved_peers(peers);
+
+        Ok(())
+    }
+
+    fn add_peers_to_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String> {
+        let Some(set_id) = self.notification_protocol_ids.get(&protocol) else {
+            return Err(format!(
+                "Cannot add peers to reserved set of unknown protocol: {}",
+                protocol
+            ));
+        };
+
+        let peers = self.split_multiaddr_and_peer_id(peers)?;
+
+        for (peer_id, addr) in peers.into_iter() {
+            // Make sure the local peer ID is never added to the PSM.
+            if peer_id == self.local_peer_id {
+                return Err("Local peer ID cannot be added as a reserved peer.".to_string());
+            }
+
+            if !addr.is_empty() {
+                let _ = self
+                    .to_worker
+                    .unbounded_send(ServiceToWorkerMsg::AddKnownAddress(peer_id, addr));
+            }
+
+            self.protocol_handles[usize::from(*set_id)].add_reserved_peer(peer_id);
+        }
+
+        Ok(())
+    }
+
+    fn remove_peers_from_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: Vec<PeerId>,
+    ) -> Result<(), String> {
+        let Some(set_id) = self.notification_protocol_ids.get(&protocol) else {
+            return Err(format!(
+                "Cannot remove peers from reserved set of unknown protocol: {}",
+                protocol
+            ));
+        };
+
+        for peer_id in peers.into_iter() {
+            self.protocol_handles[usize::from(*set_id)].remove_reserved_peer(peer_id);
+        }
+
+        Ok(())
+    }
+
+    fn sync_num_connected(&self) -> usize {
+        self.num_connected.load(Ordering::Relaxed)
+    }
+
+    fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
+        match Roles::decode_all(&mut &handshake[..]) {
+            Ok(role) => Some(role.into()),
+            Err(_) => {
+                log::debug!(target: "sub-libp2p", "handshake doesn't contain peer role: {handshake:?}");
+                self.peer_store_handle.peer_role(&peer_id)
+            }
+        }
+    }
+}
+
+impl NetworkEventStream for NetworkService {
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>> {
+        let (tx, rx) = out_events::channel(name, 100_000);
+        let _ = self
+            .to_worker
+            .unbounded_send(ServiceToWorkerMsg::EventStream(tx));
+        Box::pin(rx)
+    }
+}
+
+impl NetworkNotification for NetworkService {
+    fn write_notification(&self, _target: PeerId, _protocol: ProtocolName, _message: Vec<u8>) {
+        unimplemented!();
+    }
+
+    fn notification_sender(
+        &self,
+        _target: PeerId,
+        _protocol: ProtocolName,
+    ) -> Result<Box<dyn NotificationSenderT>, NotificationSenderError> {
+        unimplemented!();
+    }
+
+    fn set_notification_handshake(&self, _protocol: ProtocolName, _handshake: Vec<u8>) {
+        unimplemented!();
+    }
+}
+
+#[async_trait::async_trait]
+impl NetworkRequest for NetworkService {
+    async fn request(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        connect: IfDisconnected,
+    ) -> Result<(Vec<u8>, ProtocolName), RequestFailure> {
+        let (tx, rx) = oneshot::channel();
+
+        self.start_request(target, protocol, request, fallback_request, tx, connect);
+
+        match rx.await {
+            Ok(v) => v,
+            // The channel can only be closed if the network worker no longer exists. If the
+            // network worker no longer exists, then all connections to `target` are necessarily
+            // closed, and we legitimately report this situation as a "ConnectionClosed".
+            Err(_) => Err(RequestFailure::Network(OutboundFailure::ConnectionClosed)),
+        }
+    }
+
+    fn start_request(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        let _ = self.to_worker.unbounded_send(ServiceToWorkerMsg::Request {
+            target,
+            protocol,
+            request,
+            fallback_request,
+            pending_response: tx,
+            connect,
+        });
+    }
+}
+
+/// A `NotificationSender` allows for sending notifications to a peer with a chosen protocol.
+#[must_use]
+pub struct NotificationSender {
+    sink: NotificationsSink,
+
+    /// Name of the protocol on the wire.
+    protocol_name: ProtocolName,
+
+    /// Field extracted from the [`Metrics`] struct and necessary to report the
+    /// notifications-related metrics.
+    notification_size_metric: Option<Histogram>,
+}
+
+#[async_trait::async_trait]
+impl NotificationSenderT for NotificationSender {
+    async fn ready(
+        &self,
+    ) -> Result<Box<dyn NotificationSenderReadyT + '_>, NotificationSenderError> {
+        Ok(Box::new(NotificationSenderReady {
+            ready: match self.sink.reserve_notification().await {
+                Ok(r) => Some(r),
+                Err(()) => return Err(NotificationSenderError::Closed),
+            },
+            peer_id: self.sink.peer_id(),
+            protocol_name: &self.protocol_name,
+            notification_size_metric: self.notification_size_metric.clone(),
+        }))
+    }
+}
+
+/// Reserved slot in the notifications buffer, ready to accept data.
+#[must_use]
+pub struct NotificationSenderReady<'a> {
+    ready: Option<Ready<'a>>,
+
+    /// Target of the notification.
+    peer_id: &'a PeerId,
+
+    /// Name of the protocol on the wire.
+    protocol_name: &'a ProtocolName,
+
+    /// Field extracted from the [`Metrics`] struct and necessary to report the
+    /// notifications-related metrics.
+    notification_size_metric: Option<Histogram>,
+}
+
+impl<'a> NotificationSenderReadyT for NotificationSenderReady<'a> {
+    fn send(&mut self, notification: Vec<u8>) -> Result<(), NotificationSenderError> {
+        if let Some(notification_size_metric) = &self.notification_size_metric {
+            notification_size_metric.observe(notification.len() as f64);
+        }
+
+        trace!(
+            target: "sub-libp2p",
+            "External API => Notification({:?}, {}, {} bytes)",
+            self.peer_id, self.protocol_name, notification.len(),
+        );
+        trace!(target: "sub-libp2p", "Handler({:?}) <= Async notification", self.peer_id);
+
+        self.ready
+            .take()
+            .ok_or(NotificationSenderError::Closed)?
+            .send(notification)
+            .map_err(|()| NotificationSenderError::Closed)
+    }
+}
+
+/// Messages sent from the `NetworkService` to the `NetworkWorker`.
+///
+/// Each entry corresponds to a method of `NetworkService`.
+enum ServiceToWorkerMsg {
+    GetValue(KademliaKey),
+    PutValue(KademliaKey, Vec<u8>),
+    AddKnownAddress(PeerId, Multiaddr),
+    EventStream(out_events::Sender),
+    Request {
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        pending_response: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    },
+    NetworkStatus {
+        pending_response: oneshot::Sender<Result<NetworkStatus, RequestFailure>>,
+    },
+    NetworkState {
+        pending_response: oneshot::Sender<Result<NetworkState, RequestFailure>>,
+    },
+    DisconnectPeer(PeerId, ProtocolName),
+}
+
+/// Main network worker. Must be polled in order for the network to advance.
+///
+/// You are encouraged to poll this in a separate background thread or task.
+#[must_use = "The NetworkWorker must be polled in order for the network to advance"]
+pub struct NetworkWorker {
+    /// Updated by the `NetworkWorker` and loaded by the `NetworkService`.
+    listen_addresses: Arc<Mutex<HashSet<Multiaddr>>>,
+    /// Updated by the `NetworkWorker` and loaded by the `NetworkService`.
+    num_connected: Arc<AtomicUsize>,
+    /// The network service that can be extracted and shared through the codebase.
+    service: Arc<NetworkService>,
+    /// The *actual* network.
+    network_service: Swarm<Behaviour>,
+    /// Messages from the [`NetworkService`] that must be processed.
+    from_service: TracingUnboundedReceiver<ServiceToWorkerMsg>,
+    /// Senders for events that happen on the network.
+    event_streams: out_events::OutChannels,
+    /// Prometheus network metrics.
+    metrics: Option<Metrics>,
+    /// The `PeerId`'s of all boot nodes mapped to the registered addresses.
+    boot_node_ids: Arc<HashMap<PeerId, Vec<Multiaddr>>>,
+    /// Boot nodes that we already have reported as invalid.
+    reported_invalid_boot_nodes: HashSet<PeerId>,
+    /// Peer reputation store handle.
+    peer_store_handle: PeerStoreHandle,
+    /// Notification protocol handles.
+    notif_protocol_handles: Vec<protocol::ProtocolHandle>,
+}
+
+impl NetworkWorker {
+    /// Run the network.
+    pub async fn run(mut self) {
+        while self.next_action().await {}
+    }
+
+    /// Perform one action on the network.
+    ///
+    /// Returns `false` when the worker should be shutdown.
+    /// Use in tests only.
+    pub async fn next_action(&mut self) -> bool {
+        futures::select! {
+            // Next message from the service.
+            msg = self.from_service.next() => {
+                if let Some(msg) = msg {
+                    self.handle_worker_message(msg);
+                } else {
+                    return false
+                }
+            },
+            // Next event from `Swarm` (the stream guaranteed to never terminate).
+            event = self.network_service.select_next_some() => {
+                self.handle_swarm_event(event);
+            },
+        };
+
+        // Update the `num_connected` count shared with the `NetworkService`.
+        let num_connected_peers = self
+            .network_service
+            .behaviour()
+            .user_protocol()
+            .num_sync_peers();
+        self.num_connected
+            .store(num_connected_peers, Ordering::Relaxed);
+
+        if let Some(metrics) = self.metrics.as_ref() {
+            if let Some(buckets) = self
+                .network_service
+                .behaviour_mut()
+                .num_entries_per_kbucket()
+            {
+                for (lower_ilog2_bucket_bound, num_entries) in buckets {
+                    metrics
+                        .kbuckets_num_nodes
+                        .with_label_values(&[&lower_ilog2_bucket_bound.to_string()])
+                        .set(num_entries as u64);
+                }
+            }
+            if let Some(num_entries) = self.network_service.behaviour_mut().num_kademlia_records() {
+                metrics.kademlia_records_count.set(num_entries as u64);
+            }
+            if let Some(num_entries) = self
+                .network_service
+                .behaviour_mut()
+                .kademlia_records_total_size()
+            {
+                metrics.kademlia_records_sizes_total.set(num_entries as u64);
+            }
+            metrics
+                .peerset_num_discovered
+                .set(self.peer_store_handle.num_known_peers() as u64);
+            metrics.pending_connections.set(
+                Swarm::network_info(&self.network_service)
+                    .connection_counters()
+                    .num_pending() as u64,
+            );
+        }
+
+        true
+    }
+
+    /// Process the next message coming from the `NetworkService`.
+    fn handle_worker_message(&mut self, msg: ServiceToWorkerMsg) {
+        match msg {
+            ServiceToWorkerMsg::GetValue(key) => {
+                self.network_service.behaviour_mut().get_value(key)
+            }
+            ServiceToWorkerMsg::PutValue(key, value) => {
+                self.network_service.behaviour_mut().put_value(key, value)
+            }
+            ServiceToWorkerMsg::AddKnownAddress(peer_id, addr) => self
+                .network_service
+                .behaviour_mut()
+                .add_known_address(peer_id, addr),
+            ServiceToWorkerMsg::EventStream(sender) => self.event_streams.push(sender),
+            ServiceToWorkerMsg::Request {
+                target,
+                protocol,
+                request,
+                fallback_request,
+                pending_response,
+                connect,
+            } => {
+                self.network_service.behaviour_mut().send_request(
+                    &target,
+                    protocol,
+                    request,
+                    fallback_request,
+                    pending_response,
+                    connect,
+                );
+            }
+            ServiceToWorkerMsg::NetworkStatus { pending_response } => {
+                let _ = pending_response.send(Ok(self.status()));
+            }
+            ServiceToWorkerMsg::NetworkState { pending_response } => {
+                let _ = pending_response.send(Ok(self.network_state()));
+            }
+            ServiceToWorkerMsg::DisconnectPeer(who, protocol_name) => self
+                .network_service
+                .behaviour_mut()
+                .user_protocol_mut()
+                .disconnect_peer(&who, protocol_name),
+        }
+    }
+
+    /// Process the next event coming from `Swarm`.
+    #[allow(clippy::collapsible_match)]
+    fn handle_swarm_event(&mut self, event: SwarmEvent<BehaviourOut, THandlerErr<Behaviour>>) {
+        match event {
+            SwarmEvent::Behaviour(BehaviourOut::InboundRequest {
+                protocol, result, ..
+            }) => {
+                if let Some(metrics) = self.metrics.as_ref() {
+                    match result {
+                        Ok(serve_time) => {
+                            metrics
+                                .requests_in_success_total
+                                .with_label_values(&[&protocol])
+                                .observe(serve_time.as_secs_f64());
+                        }
+                        Err(err) => {
+                            let reason = match err {
+                                ResponseFailure::Network(InboundFailure::Timeout) => {
+                                    Some("timeout")
+                                }
+                                ResponseFailure::Network(InboundFailure::UnsupportedProtocols) =>
+                                // `UnsupportedProtocols` is reported for every single
+                                // inbound request whenever a request with an unsupported
+                                // protocol is received. This is not reported in order to
+                                // avoid confusions.
+                                {
+                                    None
+                                }
+                                ResponseFailure::Network(InboundFailure::ResponseOmission) => {
+                                    Some("busy-omitted")
+                                }
+                                ResponseFailure::Network(InboundFailure::ConnectionClosed) => {
+                                    Some("connection-closed")
+                                }
+                            };
+
+                            if let Some(reason) = reason {
+                                metrics
+                                    .requests_in_failure_total
+                                    .with_label_values(&[&protocol, reason])
+                                    .inc();
+                            }
+                        }
+                    }
+                }
+            }
+            SwarmEvent::Behaviour(BehaviourOut::RequestFinished {
+                protocol,
+                duration,
+                result,
+                ..
+            }) => {
+                if let Some(metrics) = self.metrics.as_ref() {
+                    match result {
+                        Ok(_) => {
+                            metrics
+                                .requests_out_success_total
+                                .with_label_values(&[&protocol])
+                                .observe(duration.as_secs_f64());
+                        }
+                        Err(err) => {
+                            let reason = match err {
+                                RequestFailure::NotConnected => "not-connected",
+                                RequestFailure::UnknownProtocol => "unknown-protocol",
+                                RequestFailure::Refused => "refused",
+                                RequestFailure::Obsolete => "obsolete",
+                                RequestFailure::Network(OutboundFailure::DialFailure) => {
+                                    "dial-failure"
+                                }
+                                RequestFailure::Network(OutboundFailure::Timeout) => "timeout",
+                                RequestFailure::Network(OutboundFailure::ConnectionClosed) => {
+                                    "connection-closed"
+                                }
+                                RequestFailure::Network(OutboundFailure::UnsupportedProtocols) => {
+                                    "unsupported"
+                                }
+                            };
+
+                            metrics
+                                .requests_out_failure_total
+                                .with_label_values(&[&protocol, reason])
+                                .inc();
+                        }
+                    }
+                }
+            }
+            SwarmEvent::Behaviour(BehaviourOut::ReputationChanges { peer, changes }) => {
+                for change in changes {
+                    self.peer_store_handle.report_peer(peer, change);
+                }
+            }
+            SwarmEvent::Behaviour(BehaviourOut::PeerIdentify {
+                peer_id,
+                info:
+                    IdentifyInfo {
+                        protocol_version,
+                        agent_version,
+                        mut listen_addrs,
+                        protocols,
+                        ..
+                    },
+            }) => {
+                if listen_addrs.len() > 30 {
+                    debug!(
+                        target: "sub-libp2p",
+                        "Node {:?} has reported more than 30 addresses; it is identified by {:?} and {:?}",
+                        peer_id, protocol_version, agent_version
+                    );
+                    listen_addrs.truncate(30);
+                }
+                for addr in listen_addrs {
+                    self.network_service
+                        .behaviour_mut()
+                        .add_self_reported_address_to_dht(&peer_id, &protocols, addr);
+                }
+                self.peer_store_handle.add_known_peer(peer_id);
+            }
+            SwarmEvent::Behaviour(BehaviourOut::Discovered(peer_id)) => {
+                self.peer_store_handle.add_known_peer(peer_id);
+            }
+            SwarmEvent::Behaviour(BehaviourOut::RandomKademliaStarted) => {
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics.kademlia_random_queries_total.inc();
+                }
+            }
+            SwarmEvent::Behaviour(BehaviourOut::NotificationStreamOpened {
+                remote,
+                set_id,
+                direction,
+                negotiated_fallback,
+                notifications_sink,
+                received_handshake,
+            }) => {
+                let _ = self.notif_protocol_handles[usize::from(set_id)].report_substream_opened(
+                    remote,
+                    direction,
+                    received_handshake,
+                    negotiated_fallback,
+                    notifications_sink,
+                );
+            }
+            SwarmEvent::Behaviour(BehaviourOut::NotificationStreamReplaced {
+                remote,
+                set_id,
+                notifications_sink,
+            }) => {
+                let _ = self.notif_protocol_handles[usize::from(set_id)]
+                    .report_notification_sink_replaced(remote, notifications_sink);
+
+                // TODO: Notifications might have been lost as a result of the previous
+                // connection being dropped, and as a result it would be preferable to notify
+                // the users of this fact by simulating the substream being closed then
+                // reopened.
+                // The code below doesn't compile because `role` is unknown. Propagating the
+                // handshake of the secondary connections is quite an invasive change and
+                // would conflict with https://github.com/paritytech/substrate/issues/6403.
+                // Considering that dropping notifications is generally regarded as
+                // acceptable, this bug is at the moment intentionally left there and is
+                // intended to be fixed at the same time as
+                // https://github.com/paritytech/substrate/issues/6403.
+                // self.event_streams.send(Event::NotificationStreamClosed {
+                // remote,
+                // protocol,
+                // });
+                // self.event_streams.send(Event::NotificationStreamOpened {
+                // remote,
+                // protocol,
+                // role,
+                // });
+            }
+            SwarmEvent::Behaviour(BehaviourOut::NotificationStreamClosed { remote, set_id }) => {
+                let _ = self.notif_protocol_handles[usize::from(set_id)]
+                    .report_substream_closed(remote);
+            }
+            SwarmEvent::Behaviour(BehaviourOut::NotificationsReceived {
+                remote,
+                set_id,
+                notification,
+            }) => {
+                let _ = self.notif_protocol_handles[usize::from(set_id)]
+                    .report_notification_received(remote, notification);
+            }
+            SwarmEvent::Behaviour(BehaviourOut::Dht(event, duration)) => {
+                if let Some(metrics) = self.metrics.as_ref() {
+                    let query_type = match event {
+                        DhtEvent::ValueFound(_) => "value-found",
+                        DhtEvent::ValueNotFound(_) => "value-not-found",
+                        DhtEvent::ValuePut(_) => "value-put",
+                        DhtEvent::ValuePutFailed(_) => "value-put-failed",
+                    };
+                    metrics
+                        .kademlia_query_duration
+                        .with_label_values(&[query_type])
+                        .observe(duration.as_secs_f64());
+                }
+
+                self.event_streams.send(Event::Dht(event));
+            }
+            SwarmEvent::Behaviour(BehaviourOut::None) => {
+                // Ignored event from lower layers.
+            }
+            SwarmEvent::ConnectionEstablished {
+                peer_id,
+                endpoint,
+                num_established,
+                concurrent_dial_errors,
+                ..
+            } => {
+                if let Some(errors) = concurrent_dial_errors {
+                    debug!(target: "sub-libp2p", "Libp2p => Connected({:?}) with errors: {:?}", peer_id, errors);
+                } else {
+                    debug!(target: "sub-libp2p", "Libp2p => Connected({:?})", peer_id);
+                }
+
+                if let Some(metrics) = self.metrics.as_ref() {
+                    let direction = match endpoint {
+                        ConnectedPoint::Dialer { .. } => "out",
+                        ConnectedPoint::Listener { .. } => "in",
+                    };
+                    metrics
+                        .connections_opened_total
+                        .with_label_values(&[direction])
+                        .inc();
+
+                    if num_established.get() == 1 {
+                        metrics.distinct_peers_connections_opened_total.inc();
+                    }
+                }
+            }
+            SwarmEvent::ConnectionClosed {
+                peer_id,
+                cause,
+                endpoint,
+                num_established,
+            } => {
+                debug!(target: "sub-libp2p", "Libp2p => Disconnected({:?}, {:?})", peer_id, cause);
+                if let Some(metrics) = self.metrics.as_ref() {
+                    let direction = match endpoint {
+                        ConnectedPoint::Dialer { .. } => "out",
+                        ConnectedPoint::Listener { .. } => "in",
+                    };
+                    let reason = match cause {
+                        Some(ConnectionError::IO(_)) => "transport-error",
+                        Some(ConnectionError::Handler(Either::Left(Either::Left(
+                            Either::Right(Either::Left(PingFailure::Timeout)),
+                        )))) => "ping-timeout",
+                        Some(ConnectionError::Handler(Either::Left(Either::Left(
+                            Either::Left(NotifsHandlerError::SyncNotificationsClogged),
+                        )))) => "sync-notifications-clogged",
+                        Some(ConnectionError::Handler(_)) => "protocol-error",
+                        Some(ConnectionError::KeepAliveTimeout) => "keep-alive-timeout",
+                        None => "actively-closed",
+                    };
+                    metrics
+                        .connections_closed_total
+                        .with_label_values(&[direction, reason])
+                        .inc();
+
+                    // `num_established` represents the number of *remaining* connections.
+                    if num_established == 0 {
+                        metrics.distinct_peers_connections_closed_total.inc();
+                    }
+                }
+            }
+            SwarmEvent::NewListenAddr { address, .. } => {
+                trace!(target: "sub-libp2p", "Libp2p => NewListenAddr({})", address);
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics.listeners_local_addresses.inc();
+                }
+                self.listen_addresses.lock().insert(address.clone());
+            }
+            SwarmEvent::ExpiredListenAddr { address, .. } => {
+                info!(target: "sub-libp2p", "📪 No longer listening on {}", address);
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics.listeners_local_addresses.dec();
+                }
+                self.listen_addresses.lock().remove(&address);
+            }
+            SwarmEvent::OutgoingConnectionError { peer_id, error } => {
+                if let Some(peer_id) = peer_id {
+                    trace!(
+                        target: "sub-libp2p",
+                        "Libp2p => Failed to reach {:?}: {}",
+                        peer_id, error,
+                    );
+
+                    let not_reported = !self.reported_invalid_boot_nodes.contains(&peer_id);
+
+                    if let Some(addresses) = not_reported
+                        .then(|| self.boot_node_ids.get(&peer_id))
+                        .flatten()
+                    {
+                        if let DialError::WrongPeerId { obtained, endpoint } = &error {
+                            if let ConnectedPoint::Dialer {
+                                address,
+                                role_override: _,
+                            } = endpoint
+                            {
+                                let address_without_peer_id = parse_addr(address.clone())
+                                    .map_or_else(|_| address.clone(), |r| r.1);
+
+                                // Only report for address of boot node that was added at startup of
+                                // the node and not for any address that the node learned of the
+                                // boot node.
+                                if addresses.iter().any(|a| address_without_peer_id == *a) {
+                                    warn!(
+										"💔 The bootnode you want to connect to at `{address}` provided a \
+										 different peer ID `{obtained}` than the one you expect `{peer_id}`.",
+									);
+
+                                    self.reported_invalid_boot_nodes.insert(peer_id);
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if let Some(metrics) = self.metrics.as_ref() {
+                    #[allow(deprecated)]
+                    let reason = match error {
+                        DialError::Denied { cause } => {
+                            if cause.downcast::<Exceeded>().is_ok() {
+                                Some("limit-reached")
+                            } else {
+                                None
+                            }
+                        }
+                        DialError::ConnectionLimit(_) => Some("limit-reached"),
+                        DialError::InvalidPeerId(_)
+                        | DialError::WrongPeerId { .. }
+                        | DialError::LocalPeerId { .. } => Some("invalid-peer-id"),
+                        DialError::Transport(_) => Some("transport-error"),
+                        DialError::Banned
+                        | DialError::NoAddresses
+                        | DialError::DialPeerConditionFalse(_)
+                        | DialError::Aborted => None, // ignore them
+                    };
+                    if let Some(reason) = reason {
+                        metrics
+                            .pending_connections_errors_total
+                            .with_label_values(&[reason])
+                            .inc();
+                    }
+                }
+            }
+            SwarmEvent::Dialing(peer_id) => {
+                trace!(target: "sub-libp2p", "Libp2p => Dialing({:?})", peer_id)
+            }
+            SwarmEvent::IncomingConnection {
+                local_addr,
+                send_back_addr,
+            } => {
+                trace!(target: "sub-libp2p", "Libp2p => IncomingConnection({},{}))",
+					local_addr, send_back_addr);
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics.incoming_connections_total.inc();
+                }
+            }
+            SwarmEvent::IncomingConnectionError {
+                local_addr,
+                send_back_addr,
+                error,
+            } => {
+                debug!(
+                    target: "sub-libp2p",
+                    "Libp2p => IncomingConnectionError({},{}): {}",
+                    local_addr, send_back_addr, error,
+                );
+                if let Some(metrics) = self.metrics.as_ref() {
+                    #[allow(deprecated)]
+                    let reason = match error {
+                        ListenError::Denied { cause } => {
+                            if cause.downcast::<Exceeded>().is_ok() {
+                                Some("limit-reached")
+                            } else {
+                                None
+                            }
+                        }
+                        ListenError::ConnectionLimit(_) => Some("limit-reached"),
+                        ListenError::WrongPeerId { .. } | ListenError::LocalPeerId { .. } => {
+                            Some("invalid-peer-id")
+                        }
+                        ListenError::Transport(_) => Some("transport-error"),
+                        ListenError::Aborted => None, // ignore it
+                    };
+
+                    if let Some(reason) = reason {
+                        metrics
+                            .incoming_connections_errors_total
+                            .with_label_values(&[reason])
+                            .inc();
+                    }
+                }
+            }
+            #[allow(deprecated)]
+            SwarmEvent::BannedPeer { peer_id, endpoint } => {
+                debug!(
+                    target: "sub-libp2p",
+                    "Libp2p => BannedPeer({}). Connected via {:?}.",
+                    peer_id, endpoint,
+                );
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics
+                        .incoming_connections_errors_total
+                        .with_label_values(&["banned"])
+                        .inc();
+                }
+            }
+            SwarmEvent::ListenerClosed {
+                reason, addresses, ..
+            } => {
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics
+                        .listeners_local_addresses
+                        .sub(addresses.len() as u64);
+                }
+                let mut listen_addresses = self.listen_addresses.lock();
+                for addr in &addresses {
+                    listen_addresses.remove(addr);
+                }
+                drop(listen_addresses);
+
+                let addrs = addresses
+                    .into_iter()
+                    .map(|a| a.to_string())
+                    .collect::<Vec<_>>()
+                    .join(", ");
+                match reason {
+                    Ok(()) => error!(
+                        target: "sub-libp2p",
+                        "📪 Libp2p listener ({}) closed gracefully",
+                        addrs
+                    ),
+                    Err(e) => error!(
+                        target: "sub-libp2p",
+                        "📪 Libp2p listener ({}) closed: {}",
+                        addrs, e
+                    ),
+                }
+            }
+            SwarmEvent::ListenerError { error, .. } => {
+                debug!(target: "sub-libp2p", "Libp2p => ListenerError: {}", error);
+                if let Some(metrics) = self.metrics.as_ref() {
+                    metrics.listeners_errors_total.inc();
+                }
+            }
+        }
+    }
+}
+
+impl Unpin for NetworkWorker {}
+
+#[allow(clippy::result_large_err)]
+fn ensure_addresses_consistent_with_transport<'a>(
+    addresses: impl Iterator<Item = &'a Multiaddr>,
+    transport: &TransportConfig,
+) -> Result<(), Error> {
+    if matches!(transport, TransportConfig::MemoryOnly) {
+        let addresses: Vec<_> = addresses
+            .filter(|x| {
+                x.iter()
+                    .any(|y| !matches!(y, libp2p::core::multiaddr::Protocol::Memory(_)))
+            })
+            .cloned()
+            .collect();
+
+        if !addresses.is_empty() {
+            return Err(Error::AddressesForAnotherTransport {
+                transport: transport.clone(),
+                addresses,
+            });
+        }
+    } else {
+        let addresses: Vec<_> = addresses
+            .filter(|x| {
+                x.iter()
+                    .any(|y| matches!(y, libp2p::core::multiaddr::Protocol::Memory(_)))
+            })
+            .cloned()
+            .collect();
+
+        if !addresses.is_empty() {
+            return Err(Error::AddressesForAnotherTransport {
+                transport: transport.clone(),
+                addresses,
+            });
+        }
+    }
+
+    Ok(())
+}
diff --git a/libp2p/src/service/metrics.rs b/libp2p/src/service/metrics.rs
new file mode 100644
index 000000000..a16b3d6d8
--- /dev/null
+++ b/libp2p/src/service/metrics.rs
@@ -0,0 +1,271 @@
+// 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::transport::BandwidthSinks;
+use prometheus_endpoint::{
+    self as prometheus, Counter, CounterVec, Gauge, GaugeVec, HistogramOpts, MetricSource, Opts,
+    PrometheusError, Registry, SourcedCounter, SourcedGauge, U64,
+};
+use std::{
+    str,
+    sync::{
+        atomic::{AtomicUsize, Ordering},
+        Arc,
+    },
+};
+
+pub use prometheus_endpoint::{Histogram, HistogramVec};
+
+/// Registers all networking metrics with the given registry.
+pub fn register(registry: &Registry, sources: MetricSources) -> Result<Metrics, PrometheusError> {
+    BandwidthCounters::register(registry, sources.bandwidth)?;
+    NumConnectedGauge::register(registry, sources.connected_peers)?;
+    Metrics::register(registry)
+}
+
+/// Predefined metric sources that are fed directly into prometheus.
+pub struct MetricSources {
+    pub bandwidth: Arc<BandwidthSinks>,
+    pub connected_peers: Arc<AtomicUsize>,
+}
+
+/// Dedicated metrics.
+pub struct Metrics {
+    // This list is ordered alphabetically
+    pub connections_closed_total: CounterVec<U64>,
+    pub connections_opened_total: CounterVec<U64>,
+    pub distinct_peers_connections_closed_total: Counter<U64>,
+    pub distinct_peers_connections_opened_total: Counter<U64>,
+    pub incoming_connections_errors_total: CounterVec<U64>,
+    pub incoming_connections_total: Counter<U64>,
+    pub issued_light_requests: Counter<U64>,
+    pub kademlia_query_duration: HistogramVec,
+    pub kademlia_random_queries_total: Counter<U64>,
+    pub kademlia_records_count: Gauge<U64>,
+    pub kademlia_records_sizes_total: Gauge<U64>,
+    pub kbuckets_num_nodes: GaugeVec<U64>,
+    pub listeners_local_addresses: Gauge<U64>,
+    pub listeners_errors_total: Counter<U64>,
+    pub peerset_num_discovered: Gauge<U64>,
+    pub pending_connections: Gauge<U64>,
+    pub pending_connections_errors_total: CounterVec<U64>,
+    pub requests_in_failure_total: CounterVec<U64>,
+    pub requests_in_success_total: HistogramVec,
+    pub requests_out_failure_total: CounterVec<U64>,
+    pub requests_out_success_total: HistogramVec,
+}
+
+impl Metrics {
+    fn register(registry: &Registry) -> Result<Self, PrometheusError> {
+        Ok(Self {
+			// This list is ordered alphabetically
+			connections_closed_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_connections_closed_total",
+					"Total number of connections closed, by direction and reason"
+				),
+				&["direction", "reason"]
+			)?, registry)?,
+			connections_opened_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_connections_opened_total",
+					"Total number of connections opened by direction"
+				),
+				&["direction"]
+			)?, registry)?,
+			distinct_peers_connections_closed_total: prometheus::register(Counter::new(
+					"substrate_sub_libp2p_distinct_peers_connections_closed_total",
+					"Total number of connections closed with distinct peers"
+			)?, registry)?,
+			distinct_peers_connections_opened_total: prometheus::register(Counter::new(
+					"substrate_sub_libp2p_distinct_peers_connections_opened_total",
+					"Total number of connections opened with distinct peers"
+			)?, registry)?,
+			incoming_connections_errors_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_incoming_connections_handshake_errors_total",
+					"Total number of incoming connections that have failed during the \
+					initial handshake"
+				),
+				&["reason"]
+			)?, registry)?,
+			incoming_connections_total: prometheus::register(Counter::new(
+				"substrate_sub_libp2p_incoming_connections_total",
+				"Total number of incoming connections on the listening sockets"
+			)?, registry)?,
+			issued_light_requests: prometheus::register(Counter::new(
+				"substrate_issued_light_requests",
+				"Number of light client requests that our node has issued.",
+			)?, registry)?,
+			kademlia_query_duration: prometheus::register(HistogramVec::new(
+				HistogramOpts {
+					common_opts: Opts::new(
+						"substrate_sub_libp2p_kademlia_query_duration",
+						"Duration of Kademlia queries per query type"
+					),
+					buckets: prometheus::exponential_buckets(0.5, 2.0, 10)
+						.expect("parameters are always valid values; qed"),
+				},
+				&["type"]
+			)?, registry)?,
+			kademlia_random_queries_total: prometheus::register(Counter::new(
+				"substrate_sub_libp2p_kademlia_random_queries_total",
+				"Number of random Kademlia queries started",
+			)?, registry)?,
+			kademlia_records_count: prometheus::register(Gauge::new(
+				"substrate_sub_libp2p_kademlia_records_count",
+				"Number of records in the Kademlia records store",
+			)?, registry)?,
+			kademlia_records_sizes_total: prometheus::register(Gauge::new(
+				"substrate_sub_libp2p_kademlia_records_sizes_total",
+				"Total size of all the records in the Kademlia records store",
+			)?, registry)?,
+			kbuckets_num_nodes: prometheus::register(GaugeVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_kbuckets_num_nodes",
+					"Number of nodes per kbucket per Kademlia instance"
+				),
+				&["lower_ilog2_bucket_bound"]
+			)?, registry)?,
+			listeners_local_addresses: prometheus::register(Gauge::new(
+				"substrate_sub_libp2p_listeners_local_addresses",
+				"Number of local addresses we're listening on"
+			)?, registry)?,
+			listeners_errors_total: prometheus::register(Counter::new(
+				"substrate_sub_libp2p_listeners_errors_total",
+				"Total number of non-fatal errors reported by a listener"
+			)?, registry)?,
+			peerset_num_discovered: prometheus::register(Gauge::new(
+				"substrate_sub_libp2p_peerset_num_discovered",
+				"Number of nodes stored in the peerset manager",
+			)?, registry)?,
+			pending_connections: prometheus::register(Gauge::new(
+				"substrate_sub_libp2p_pending_connections",
+				"Number of connections in the process of being established",
+			)?, registry)?,
+			pending_connections_errors_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_pending_connections_errors_total",
+					"Total number of pending connection errors"
+				),
+				&["reason"]
+			)?, registry)?,
+			requests_in_failure_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_requests_in_failure_total",
+					"Total number of incoming requests that the node has failed to answer"
+				),
+				&["protocol", "reason"]
+			)?, registry)?,
+			requests_in_success_total: prometheus::register(HistogramVec::new(
+				HistogramOpts {
+					common_opts: Opts::new(
+						"substrate_sub_libp2p_requests_in_success_total",
+						"For successful incoming requests, time between receiving the request and \
+						 starting to send the response"
+					),
+					buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
+						.expect("parameters are always valid values; qed"),
+				},
+				&["protocol"]
+			)?, registry)?,
+			requests_out_failure_total: prometheus::register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_requests_out_failure_total",
+					"Total number of requests that have failed"
+				),
+				&["protocol", "reason"]
+			)?, registry)?,
+			requests_out_success_total: prometheus::register(HistogramVec::new(
+				HistogramOpts {
+					common_opts: Opts::new(
+						"substrate_sub_libp2p_requests_out_success_total",
+						"For successful outgoing requests, time between a request's start and finish"
+					),
+					buckets: prometheus::exponential_buckets(0.001, 2.0, 16)
+						.expect("parameters are always valid values; qed"),
+				},
+				&["protocol"]
+			)?, registry)?,
+		})
+    }
+}
+
+/// The bandwidth counter metric.
+#[derive(Clone)]
+pub struct BandwidthCounters(Arc<BandwidthSinks>);
+
+impl BandwidthCounters {
+    /// Registers the `BandwidthCounters` metric whose values are
+    /// obtained from the given sinks.
+    fn register(registry: &Registry, sinks: Arc<BandwidthSinks>) -> Result<(), PrometheusError> {
+        prometheus::register(
+            SourcedCounter::new(
+                &Opts::new(
+                    "substrate_sub_libp2p_network_bytes_total",
+                    "Total bandwidth usage",
+                )
+                .variable_label("direction"),
+                BandwidthCounters(sinks),
+            )?,
+            registry,
+        )?;
+
+        Ok(())
+    }
+}
+
+impl MetricSource for BandwidthCounters {
+    type N = u64;
+
+    fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
+        set(&["in"], self.0.total_inbound());
+        set(&["out"], self.0.total_outbound());
+    }
+}
+
+/// The connected peers metric.
+#[derive(Clone)]
+pub struct NumConnectedGauge(Arc<AtomicUsize>);
+
+impl NumConnectedGauge {
+    /// Registers the `MajorSyncingGauge` metric whose value is
+    /// obtained from the given `AtomicUsize`.
+    fn register(registry: &Registry, value: Arc<AtomicUsize>) -> Result<(), PrometheusError> {
+        prometheus::register(
+            SourcedGauge::new(
+                &Opts::new(
+                    "substrate_sub_libp2p_peers_count",
+                    "Number of connected peers",
+                ),
+                NumConnectedGauge(value),
+            )?,
+            registry,
+        )?;
+
+        Ok(())
+    }
+}
+
+impl MetricSource for NumConnectedGauge {
+    type N = u64;
+
+    fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
+        set(&[], self.0.load(Ordering::Relaxed) as u64);
+    }
+}
diff --git a/libp2p/src/service/out_events.rs b/libp2p/src/service/out_events.rs
new file mode 100644
index 000000000..52216eb28
--- /dev/null
+++ b/libp2p/src/service/out_events.rs
@@ -0,0 +1,373 @@
+// 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/>.
+
+//! Registering events streams.
+//!
+//! This code holds the logic that is used for the network service to inform other parts of
+//! Substrate about what is happening.
+//!
+//! # Usage
+//!
+//! - Create an instance of [`OutChannels`].
+//! - Create channels using the [`channel`] function. The receiving side implements the `Stream`
+//! trait.
+//! - You cannot directly send an event on a sender. Instead, you have to call
+//! [`OutChannels::push`] to put the sender within a [`OutChannels`].
+//! - Send events by calling [`OutChannels::send`]. Events are cloned for each sender in the
+//! collection.
+
+use crate::event::Event;
+
+use futures::{prelude::*, ready, stream::FusedStream};
+use log::{debug, error};
+use prometheus_endpoint::{register, CounterVec, GaugeVec, Opts, PrometheusError, Registry, U64};
+use std::{
+    backtrace::Backtrace,
+    cell::RefCell,
+    fmt,
+    pin::Pin,
+    task::{Context, Poll},
+};
+
+/// Log target for this file.
+pub const LOG_TARGET: &str = "sub-libp2p::out_events";
+
+/// Creates a new channel that can be associated to a [`OutChannels`].
+///
+/// The name is used in Prometheus reports, the queue size threshold is used
+/// to warn if there are too many unprocessed events in the channel.
+pub fn channel(name: &'static str, queue_size_warning: usize) -> (Sender, Receiver) {
+    let (tx, rx) = async_channel::unbounded();
+    let tx = Sender {
+        inner: tx,
+        name,
+        queue_size_warning,
+        warning_fired: SenderWarningState::NotFired,
+        creation_backtrace: Backtrace::force_capture(),
+        metrics: None,
+    };
+    let rx = Receiver {
+        inner: rx,
+        name,
+        metrics: None,
+    };
+    (tx, rx)
+}
+
+/// A state of a sender warning that is used to avoid spamming the logs.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum SenderWarningState {
+    /// The warning has not been fired yet.
+    NotFired,
+    /// The warning has been fired, and the channel is full
+    FiredFull,
+    /// The warning has been fired and the channel is not full anymore.
+    FiredFree,
+}
+
+/// Sending side of a channel.
+///
+/// Must be associated with an [`OutChannels`] before anything can be sent on it
+///
+/// > **Note**: Contrary to regular channels, this `Sender` is purposefully designed to not
+/// implement the `Clone` trait e.g. in Order to not complicate the logic keeping the metrics in
+/// sync on drop. If someone adds a `#[derive(Clone)]` below, it is **wrong**.
+pub struct Sender {
+    inner: async_channel::Sender<Event>,
+    /// Name to identify the channel (e.g., in Prometheus and logs).
+    name: &'static str,
+    /// Threshold queue size to generate an error message in the logs.
+    queue_size_warning: usize,
+    /// We generate the error message only once to not spam the logs after the first error.
+    /// Subsequently we indicate channel fullness on debug level.
+    warning_fired: SenderWarningState,
+    /// Backtrace of a place where the channel was created.
+    creation_backtrace: Backtrace,
+    /// Clone of [`Receiver::metrics`]. Will be initialized when [`Sender`] is added to
+    /// [`OutChannels`] with `OutChannels::push()`.
+    metrics: Option<Metrics>,
+}
+
+impl fmt::Debug for Sender {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_tuple("Sender").finish()
+    }
+}
+
+impl Drop for Sender {
+    fn drop(&mut self) {
+        if let Some(metrics) = self.metrics.as_ref() {
+            metrics.num_channels.with_label_values(&[self.name]).dec();
+        }
+    }
+}
+
+/// Receiving side of a channel.
+pub struct Receiver {
+    inner: async_channel::Receiver<Event>,
+    name: &'static str,
+    /// Initially contains `None`, and will be set to a value once the corresponding [`Sender`]
+    /// is assigned to an instance of [`OutChannels`].
+    metrics: Option<Metrics>,
+}
+
+impl Stream for Receiver {
+    type Item = Event;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Event>> {
+        if let Some(ev) = ready!(Pin::new(&mut self.inner).poll_next(cx)) {
+            if let Some(metrics) = &self.metrics {
+                metrics.event_out(&ev, self.name);
+            }
+            Poll::Ready(Some(ev))
+        } else {
+            Poll::Ready(None)
+        }
+    }
+}
+
+impl fmt::Debug for Receiver {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_tuple("Receiver").finish()
+    }
+}
+
+impl Drop for Receiver {
+    fn drop(&mut self) {
+        if !self.inner.is_terminated() {
+            // Empty the list to properly decrease the metrics.
+            while let Some(Some(_)) = self.next().now_or_never() {}
+        }
+    }
+}
+
+/// Collection of senders.
+pub struct OutChannels {
+    event_streams: Vec<Sender>,
+    /// The metrics we collect. A clone of this is sent to each [`Receiver`] associated with this
+    /// object.
+    metrics: Option<Metrics>,
+}
+
+impl OutChannels {
+    /// Creates a new empty collection of senders.
+    pub fn new(registry: Option<&Registry>) -> Result<Self, PrometheusError> {
+        let metrics = if let Some(registry) = registry {
+            Some(Metrics::register(registry)?)
+        } else {
+            None
+        };
+
+        Ok(Self {
+            event_streams: Vec::new(),
+            metrics,
+        })
+    }
+
+    /// Adds a new [`Sender`] to the collection.
+    pub fn push(&mut self, mut sender: Sender) {
+        debug_assert!(sender.metrics.is_none());
+        sender.metrics = self.metrics.clone();
+
+        if let Some(metrics) = &self.metrics {
+            metrics.num_channels.with_label_values(&[sender.name]).inc();
+        }
+
+        self.event_streams.push(sender);
+    }
+
+    /// Sends an event.
+    pub fn send(&mut self, event: Event) {
+        self.event_streams.retain_mut(|sender| {
+            let current_pending = sender.inner.len();
+            if current_pending >= sender.queue_size_warning {
+                if sender.warning_fired == SenderWarningState::NotFired {
+                    error!(
+                        "The number of unprocessed events in channel `{}` exceeded {}.\n\
+						 The channel was created at:\n{:}\n
+						 The last event was sent from:\n{:}",
+                        sender.name,
+                        sender.queue_size_warning,
+                        sender.creation_backtrace,
+                        Backtrace::force_capture(),
+                    );
+                } else if sender.warning_fired == SenderWarningState::FiredFree {
+                    // We don't want to spam the logs, so we only log on debug level
+                    debug!(
+                        target: LOG_TARGET,
+                        "Channel `{}` is overflowed again. Number of events: {}",
+                        sender.name, current_pending
+                    );
+                }
+                sender.warning_fired = SenderWarningState::FiredFull;
+            } else if sender.warning_fired == SenderWarningState::FiredFull
+                && current_pending < sender.queue_size_warning.wrapping_div(2)
+            {
+                sender.warning_fired = SenderWarningState::FiredFree;
+                debug!(
+                    target: LOG_TARGET,
+                    "Channel `{}` is no longer overflowed. Number of events: {}",
+                    sender.name, current_pending
+                );
+            }
+
+            sender.inner.try_send(event.clone()).is_ok()
+        });
+
+        if let Some(metrics) = &self.metrics {
+            for ev in &self.event_streams {
+                metrics.event_in(&event, ev.name);
+            }
+        }
+    }
+}
+
+impl fmt::Debug for OutChannels {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("OutChannels")
+            .field("num_channels", &self.event_streams.len())
+            .finish()
+    }
+}
+
+#[derive(Clone)]
+struct Metrics {
+    // This list is ordered alphabetically
+    events_total: CounterVec<U64>,
+    notifications_sizes: CounterVec<U64>,
+    num_channels: GaugeVec<U64>,
+}
+
+thread_local! {
+    static LABEL_BUFFER: RefCell<String> = RefCell::new(String::new());
+}
+
+fn format_label(prefix: &str, protocol: &str, callback: impl FnOnce(&str)) {
+    LABEL_BUFFER.with(|label_buffer| {
+        let mut label_buffer = label_buffer.borrow_mut();
+        label_buffer.clear();
+        label_buffer.reserve(prefix.len() + protocol.len() + 2);
+        label_buffer.push_str(prefix);
+        label_buffer.push('"');
+        label_buffer.push_str(protocol);
+        label_buffer.push('"');
+        callback(&label_buffer);
+    });
+}
+
+impl Metrics {
+    fn register(registry: &Registry) -> Result<Self, PrometheusError> {
+        Ok(Self {
+			events_total: register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_out_events_events_total",
+					"Number of broadcast network events that have been sent or received across all \
+					 channels"
+				),
+				&["event_name", "action", "name"]
+			)?, registry)?,
+			notifications_sizes: register(CounterVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_out_events_notifications_sizes",
+					"Size of notification events that have been sent or received across all \
+					 channels"
+				),
+				&["protocol", "action", "name"]
+			)?, registry)?,
+			num_channels: register(GaugeVec::new(
+				Opts::new(
+					"substrate_sub_libp2p_out_events_num_channels",
+					"Number of internal active channels that broadcast network events",
+				),
+				&["name"]
+			)?, registry)?,
+		})
+    }
+
+    fn event_in(&self, event: &Event, name: &str) {
+        match event {
+            Event::Dht(_) => {
+                self.events_total
+                    .with_label_values(&["dht", "sent", name])
+                    .inc();
+            }
+            Event::NotificationStreamOpened { protocol, .. } => {
+                format_label("notif-open-", protocol, |protocol_label| {
+                    self.events_total
+                        .with_label_values(&[protocol_label, "sent", name])
+                        .inc();
+                });
+            }
+            Event::NotificationStreamClosed { protocol, .. } => {
+                format_label("notif-closed-", protocol, |protocol_label| {
+                    self.events_total
+                        .with_label_values(&[protocol_label, "sent", name])
+                        .inc();
+                });
+            }
+            Event::NotificationsReceived { messages, .. } => {
+                for (protocol, message) in messages {
+                    format_label("notif-", protocol, |protocol_label| {
+                        self.events_total
+                            .with_label_values(&[protocol_label, "sent", name])
+                            .inc();
+                    });
+                    self.notifications_sizes
+                        .with_label_values(&[protocol, "sent", name])
+                        .inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
+                }
+            }
+        }
+    }
+
+    fn event_out(&self, event: &Event, name: &str) {
+        match event {
+            Event::Dht(_) => {
+                self.events_total
+                    .with_label_values(&["dht", "received", name])
+                    .inc();
+            }
+            Event::NotificationStreamOpened { protocol, .. } => {
+                format_label("notif-open-", protocol, |protocol_label| {
+                    self.events_total
+                        .with_label_values(&[protocol_label, "received", name])
+                        .inc();
+                });
+            }
+            Event::NotificationStreamClosed { protocol, .. } => {
+                format_label("notif-closed-", protocol, |protocol_label| {
+                    self.events_total
+                        .with_label_values(&[protocol_label, "received", name])
+                        .inc();
+                });
+            }
+            Event::NotificationsReceived { messages, .. } => {
+                for (protocol, message) in messages {
+                    format_label("notif-", protocol, |protocol_label| {
+                        self.events_total
+                            .with_label_values(&[protocol_label, "received", name])
+                            .inc();
+                    });
+                    self.notifications_sizes
+                        .with_label_values(&[protocol, "received", name])
+                        .inc_by(u64::try_from(message.len()).unwrap_or(u64::MAX));
+                }
+            }
+        }
+    }
+}
diff --git a/libp2p/src/service/signature.rs b/libp2p/src/service/signature.rs
new file mode 100644
index 000000000..1bdd5cd16
--- /dev/null
+++ b/libp2p/src/service/signature.rs
@@ -0,0 +1,57 @@
+// 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/>.
+//
+// If you read this, you are very thorough, congratulations.
+
+//! Signature-related code
+
+use libp2p::{
+    identity::{Keypair, PublicKey},
+    PeerId,
+};
+
+pub use libp2p::identity::SigningError;
+
+/// A result of signing a message with a network identity. Since `PeerId` is potentially a hash of a
+/// `PublicKey`, you need to reveal the `PublicKey` next to the signature, so the verifier can check
+/// if the signature was made by the entity that controls a given `PeerId`.
+pub struct Signature {
+    /// The public key derived from the network identity that signed the message.
+    pub public_key: PublicKey,
+    /// The actual signature made for the message signed.
+    pub bytes: Vec<u8>,
+}
+
+impl Signature {
+    /// Create a signature for a message with a given network identity.
+    pub fn sign_message(
+        message: impl AsRef<[u8]>,
+        keypair: &Keypair,
+    ) -> Result<Self, SigningError> {
+        let public_key = keypair.public();
+        let bytes = keypair.sign(message.as_ref())?;
+        Ok(Self { public_key, bytes })
+    }
+
+    /// Verify whether the signature was made for the given message by the entity that controls the
+    /// given `PeerId`.
+    pub fn verify(&self, message: impl AsRef<[u8]>, peer_id: &PeerId) -> bool {
+        *peer_id == self.public_key.to_peer_id()
+            && self.public_key.verify(message.as_ref(), &self.bytes)
+    }
+}
diff --git a/libp2p/src/service/traits.rs b/libp2p/src/service/traits.rs
new file mode 100644
index 000000000..ce4e474bc
--- /dev/null
+++ b/libp2p/src/service/traits.rs
@@ -0,0 +1,839 @@
+// 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/>.
+//
+// If you read this, you are very thorough, congratulations.
+
+//! Traits defined by `sc-network`.
+
+use crate::{
+    config::MultiaddrWithPeerId,
+    error,
+    event::Event,
+    request_responses::{IfDisconnected, RequestFailure},
+    service::signature::Signature,
+    types::ProtocolName,
+    ReputationChange,
+};
+
+use futures::{channel::oneshot, Stream};
+use libp2p::{Multiaddr, PeerId};
+
+use sc_network_common::role::ObservedRole;
+
+use std::{collections::HashSet, fmt::Debug, future::Future, pin::Pin, sync::Arc};
+
+pub use libp2p::{identity::SigningError, kad::record::Key as KademliaKey};
+
+/// Signer with network identity
+pub trait NetworkSigner {
+    /// Signs the message with the `KeyPair` that defines the local [`PeerId`].
+    fn sign_with_local_identity(&self, msg: impl AsRef<[u8]>) -> Result<Signature, SigningError>;
+}
+
+impl<T> NetworkSigner for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkSigner,
+{
+    fn sign_with_local_identity(&self, msg: impl AsRef<[u8]>) -> Result<Signature, SigningError> {
+        T::sign_with_local_identity(self, msg)
+    }
+}
+
+/// Provides access to the networking DHT.
+pub trait NetworkDHTProvider {
+    /// Start getting a value from the DHT.
+    fn get_value(&self, key: &KademliaKey);
+
+    /// Start putting a value in the DHT.
+    fn put_value(&self, key: KademliaKey, value: Vec<u8>);
+}
+
+impl<T> NetworkDHTProvider for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkDHTProvider,
+{
+    fn get_value(&self, key: &KademliaKey) {
+        T::get_value(self, key)
+    }
+
+    fn put_value(&self, key: KademliaKey, value: Vec<u8>) {
+        T::put_value(self, key, value)
+    }
+}
+
+/// Provides an ability to set a fork sync request for a particular block.
+pub trait NetworkSyncForkRequest<BlockHash, BlockNumber> {
+    /// Notifies the sync service to try and sync the given block from the given
+    /// peers.
+    ///
+    /// If the given vector of peers is empty then the underlying implementation
+    /// should make a best effort to fetch the block from any peers it is
+    /// connected to (NOTE: this assumption will change in the future #3629).
+    fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber);
+}
+
+impl<T, BlockHash, BlockNumber> NetworkSyncForkRequest<BlockHash, BlockNumber> for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkSyncForkRequest<BlockHash, BlockNumber>,
+{
+    fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: BlockHash, number: BlockNumber) {
+        T::set_sync_fork_request(self, peers, hash, number)
+    }
+}
+
+/// Overview status of the network.
+#[derive(Clone)]
+pub struct NetworkStatus {
+    /// Total number of connected peers.
+    pub num_connected_peers: usize,
+    /// The total number of bytes received.
+    pub total_bytes_inbound: u64,
+    /// The total number of bytes sent.
+    pub total_bytes_outbound: u64,
+}
+
+/// Provides high-level status information about network.
+#[async_trait::async_trait]
+pub trait NetworkStatusProvider {
+    /// High-level network status information.
+    ///
+    /// Returns an error if the `NetworkWorker` is no longer running.
+    async fn status(&self) -> Result<NetworkStatus, ()>;
+}
+
+// Manual implementation to avoid extra boxing here
+impl<T> NetworkStatusProvider for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkStatusProvider,
+{
+    fn status<'life0, 'async_trait>(
+        &'life0 self,
+    ) -> Pin<Box<dyn Future<Output = Result<NetworkStatus, ()>> + Send + 'async_trait>>
+    where
+        'life0: 'async_trait,
+        Self: 'async_trait,
+    {
+        T::status(self)
+    }
+}
+
+/// Provides low-level API for manipulating network peers.
+pub trait NetworkPeers {
+    /// Set authorized peers.
+    ///
+    /// Need a better solution to manage authorized peers, but now just use reserved peers for
+    /// prototyping.
+    fn set_authorized_peers(&self, peers: HashSet<PeerId>);
+
+    /// Set authorized_only flag.
+    ///
+    /// Need a better solution to decide authorized_only, but now just use reserved_only flag for
+    /// prototyping.
+    fn set_authorized_only(&self, reserved_only: bool);
+
+    /// Adds an address known to a node.
+    fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr);
+
+    /// Report a given peer as either beneficial (+) or costly (-) according to the
+    /// given scalar.
+    fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange);
+
+    /// Get peer reputation.
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32;
+
+    /// Disconnect from a node as soon as possible.
+    ///
+    /// This triggers the same effects as if the connection had closed itself spontaneously.
+    fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName);
+
+    /// Connect to unreserved peers and allow unreserved peers to connect for syncing purposes.
+    fn accept_unreserved_peers(&self);
+
+    /// Disconnect from unreserved peers and deny new unreserved peers to connect for syncing
+    /// purposes.
+    fn deny_unreserved_peers(&self);
+
+    /// Adds a `PeerId` and its `Multiaddr` as reserved for a sync protocol (default peer set).
+    ///
+    /// Returns an `Err` if the given string is not a valid multiaddress
+    /// or contains an invalid peer ID (which includes the local peer ID).
+    fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String>;
+
+    /// Removes a `PeerId` from the list of reserved peers for a sync protocol (default peer set).
+    fn remove_reserved_peer(&self, peer_id: PeerId);
+
+    /// Sets the reserved set of a protocol to the given set of peers.
+    ///
+    /// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
+    /// consist of only `/p2p/<peerid>`.
+    ///
+    /// The node will start establishing/accepting connections and substreams to/from peers in this
+    /// set, if it doesn't have any substream open with them yet.
+    ///
+    /// Note however, if a call to this function results in less peers on the reserved set, they
+    /// will not necessarily get disconnected (depending on available free slots in the peer set).
+    /// If you want to also disconnect those removed peers, you will have to call
+    /// `remove_from_peers_set` on those in addition to updating the reserved set. You can omit
+    /// this step if the peer set is in reserved only mode.
+    ///
+    /// Returns an `Err` if one of the given addresses is invalid or contains an
+    /// invalid peer ID (which includes the local peer ID), or if `protocol` does not
+    /// refer to a known protocol.
+    fn set_reserved_peers(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String>;
+
+    /// Add peers to a peer set.
+    ///
+    /// Each `Multiaddr` must end with a `/p2p/` component containing the `PeerId`. It can also
+    /// consist of only `/p2p/<peerid>`.
+    ///
+    /// Returns an `Err` if one of the given addresses is invalid or contains an
+    /// invalid peer ID (which includes the local peer ID), or if `protocol` does not
+    /// refer to a know protocol.
+    fn add_peers_to_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String>;
+
+    /// Remove peers from a peer set.
+    ///
+    /// Returns `Err` if `protocol` does not refer to a known protocol.
+    fn remove_peers_from_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: Vec<PeerId>,
+    ) -> Result<(), String>;
+
+    /// Returns the number of peers in the sync peer set we're connected to.
+    fn sync_num_connected(&self) -> usize;
+
+    /// Attempt to get peer role.
+    ///
+    /// Right now the peer role is decoded from the received handshake for all protocols
+    /// (`/block-announces/1` has other information as well). If the handshake cannot be
+    /// decoded into a role, the role queried from `PeerStore` and if the role is not stored
+    /// there either, `None` is returned and the peer should be discarded.
+    fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole>;
+}
+
+// Manual implementation to avoid extra boxing here
+impl<T> NetworkPeers for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkPeers,
+{
+    fn set_authorized_peers(&self, peers: HashSet<PeerId>) {
+        T::set_authorized_peers(self, peers)
+    }
+
+    fn set_authorized_only(&self, reserved_only: bool) {
+        T::set_authorized_only(self, reserved_only)
+    }
+
+    fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr) {
+        T::add_known_address(self, peer_id, addr)
+    }
+
+    fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange) {
+        T::report_peer(self, peer_id, cost_benefit)
+    }
+
+    fn peer_reputation(&self, peer_id: &PeerId) -> i32 {
+        T::peer_reputation(self, peer_id)
+    }
+
+    fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName) {
+        T::disconnect_peer(self, peer_id, protocol)
+    }
+
+    fn accept_unreserved_peers(&self) {
+        T::accept_unreserved_peers(self)
+    }
+
+    fn deny_unreserved_peers(&self) {
+        T::deny_unreserved_peers(self)
+    }
+
+    fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String> {
+        T::add_reserved_peer(self, peer)
+    }
+
+    fn remove_reserved_peer(&self, peer_id: PeerId) {
+        T::remove_reserved_peer(self, peer_id)
+    }
+
+    fn set_reserved_peers(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String> {
+        T::set_reserved_peers(self, protocol, peers)
+    }
+
+    fn add_peers_to_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: HashSet<Multiaddr>,
+    ) -> Result<(), String> {
+        T::add_peers_to_reserved_set(self, protocol, peers)
+    }
+
+    fn remove_peers_from_reserved_set(
+        &self,
+        protocol: ProtocolName,
+        peers: Vec<PeerId>,
+    ) -> Result<(), String> {
+        T::remove_peers_from_reserved_set(self, protocol, peers)
+    }
+
+    fn sync_num_connected(&self) -> usize {
+        T::sync_num_connected(self)
+    }
+
+    fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole> {
+        T::peer_role(self, peer_id, handshake)
+    }
+}
+
+/// Provides access to network-level event stream.
+pub trait NetworkEventStream {
+    /// Returns a stream containing the events that happen on the network.
+    ///
+    /// If this method is called multiple times, the events are duplicated.
+    ///
+    /// The stream never ends (unless the `NetworkWorker` gets shut down).
+    ///
+    /// The name passed is used to identify the channel in the Prometheus metrics. Note that the
+    /// parameter is a `&'static str`, and not a `String`, in order to avoid accidentally having
+    /// an unbounded set of Prometheus metrics, which would be quite bad in terms of memory
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>>;
+}
+
+impl<T> NetworkEventStream for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkEventStream,
+{
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = Event> + Send>> {
+        T::event_stream(self, name)
+    }
+}
+
+/// Trait for providing information about the local network state
+pub trait NetworkStateInfo {
+    /// Returns the local external addresses.
+    fn external_addresses(&self) -> Vec<Multiaddr>;
+
+    /// Returns the listening addresses (without trailing `/p2p/` with our `PeerId`).
+    fn listen_addresses(&self) -> Vec<Multiaddr>;
+
+    /// Returns the local Peer ID.
+    fn local_peer_id(&self) -> PeerId;
+}
+
+impl<T> NetworkStateInfo for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkStateInfo,
+{
+    fn external_addresses(&self) -> Vec<Multiaddr> {
+        T::external_addresses(self)
+    }
+
+    fn listen_addresses(&self) -> Vec<Multiaddr> {
+        T::listen_addresses(self)
+    }
+
+    fn local_peer_id(&self) -> PeerId {
+        T::local_peer_id(self)
+    }
+}
+
+/// Reserved slot in the notifications buffer, ready to accept data.
+pub trait NotificationSenderReady {
+    /// Consumes this slots reservation and actually queues the notification.
+    ///
+    /// NOTE: Traits can't consume itself, but calling this method second time will return an error.
+    fn send(&mut self, notification: Vec<u8>) -> Result<(), NotificationSenderError>;
+}
+
+/// A `NotificationSender` allows for sending notifications to a peer with a chosen protocol.
+#[async_trait::async_trait]
+pub trait NotificationSender: Send + Sync + 'static {
+    /// Returns a future that resolves when the `NotificationSender` is ready to send a
+    /// notification.
+    async fn ready(&self)
+        -> Result<Box<dyn NotificationSenderReady + '_>, NotificationSenderError>;
+}
+
+/// Error returned by [`NetworkNotification::notification_sender`].
+#[derive(Debug, thiserror::Error)]
+pub enum NotificationSenderError {
+    /// The notification receiver has been closed, usually because the underlying connection
+    /// closed.
+    ///
+    /// Some of the notifications most recently sent may not have been received. However,
+    /// the peer may still be connected and a new `NotificationSender` for the same
+    /// protocol obtained from [`NetworkNotification::notification_sender`].
+    #[error("The notification receiver has been closed")]
+    Closed,
+    /// Protocol name hasn't been registered.
+    #[error("Protocol name hasn't been registered")]
+    BadProtocol,
+}
+
+/// Provides ability to send network notifications.
+pub trait NetworkNotification {
+    /// Appends a notification to the buffer of pending outgoing notifications with the given peer.
+    /// Has no effect if the notifications channel with this protocol name is not open.
+    ///
+    /// If the buffer of pending outgoing notifications with that peer is full, the notification
+    /// is silently dropped and the connection to the remote will start being shut down. This
+    /// happens if you call this method at a higher rate than the rate at which the peer processes
+    /// these notifications, or if the available network bandwidth is too low.
+    ///
+    /// For this reason, this method is considered soft-deprecated. You are encouraged to use
+    /// [`NetworkNotification::notification_sender`] instead.
+    ///
+    /// > **Note**: The reason why this is a no-op in the situation where we have no channel is
+    /// >        that we don't guarantee message delivery anyway. Networking issues can cause
+    /// >        connections to drop at any time, and higher-level logic shouldn't differentiate
+    /// >        between the remote voluntarily closing a substream or a network error
+    /// >        preventing the message from being delivered.
+    ///
+    /// The protocol must have been registered with
+    /// `crate::config::NetworkConfiguration::notifications_protocols`.
+    fn write_notification(&self, target: PeerId, protocol: ProtocolName, message: Vec<u8>);
+
+    /// Obtains a [`NotificationSender`] for a connected peer, if it exists.
+    ///
+    /// A `NotificationSender` is scoped to a particular connection to the peer that holds
+    /// a receiver. With a `NotificationSender` at hand, sending a notification is done in two
+    /// steps:
+    ///
+    /// 1. [`NotificationSender::ready`] is used to wait for the sender to become ready
+    /// for another notification, yielding a [`NotificationSenderReady`] token.
+    /// 2. [`NotificationSenderReady::send`] enqueues the notification for sending. This operation
+    /// can only fail if the underlying notification substream or connection has suddenly closed.
+    ///
+    /// An error is returned by [`NotificationSenderReady::send`] if there exists no open
+    /// notifications substream with that combination of peer and protocol, or if the remote
+    /// has asked to close the notifications substream. If that happens, it is guaranteed that an
+    /// [`Event::NotificationStreamClosed`] has been generated on the stream returned by
+    /// [`NetworkEventStream::event_stream`].
+    ///
+    /// If the remote requests to close the notifications substream, all notifications successfully
+    /// enqueued using [`NotificationSenderReady::send`] will finish being sent out before the
+    /// substream actually gets closed, but attempting to enqueue more notifications will now
+    /// return an error. It is however possible for the entire connection to be abruptly closed,
+    /// in which case enqueued notifications will be lost.
+    ///
+    /// The protocol must have been registered with
+    /// `crate::config::NetworkConfiguration::notifications_protocols`.
+    ///
+    /// # Usage
+    ///
+    /// This method returns a struct that allows waiting until there is space available in the
+    /// buffer of messages towards the given peer. If the peer processes notifications at a slower
+    /// rate than we send them, this buffer will quickly fill up.
+    ///
+    /// As such, you should never do something like this:
+    ///
+    /// ```ignore
+    /// // Do NOT do this
+    /// for peer in peers {
+    ///    if let Ok(n) = network.notification_sender(peer, ...) {
+    ///        if let Ok(s) = n.ready().await {
+    ///            let _ = s.send(...);
+    ///        }
+    ///    }
+    /// }
+    /// ```
+    ///
+    /// Doing so would slow down all peers to the rate of the slowest one. A malicious or
+    /// malfunctioning peer could intentionally process notifications at a very slow rate.
+    ///
+    /// Instead, you are encouraged to maintain your own buffer of notifications on top of the one
+    /// maintained by `sc-network`, and use `notification_sender` to progressively send out
+    /// elements from your buffer. If this additional buffer is full (which will happen at some
+    /// point if the peer is too slow to process notifications), appropriate measures can be taken,
+    /// such as removing non-critical notifications from the buffer or disconnecting the peer
+    /// using [`NetworkPeers::disconnect_peer`].
+    ///
+    ///
+    /// Notifications              Per-peer buffer
+    ///   broadcast    +------->   of notifications   +-->  `notification_sender`  +-->  Internet
+    ///                    ^       (not covered by
+    ///                    |         sc-network)
+    ///                    +
+    ///      Notifications should be dropped
+    ///             if buffer is full
+    ///
+    ///
+    /// See also the `sc-network-gossip` crate for a higher-level way to send notifications.
+    fn notification_sender(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+    ) -> Result<Box<dyn NotificationSender>, NotificationSenderError>;
+
+    /// Set handshake for the notification protocol.
+    fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>);
+}
+
+impl<T> NetworkNotification for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkNotification,
+{
+    fn write_notification(&self, target: PeerId, protocol: ProtocolName, message: Vec<u8>) {
+        T::write_notification(self, target, protocol, message)
+    }
+
+    fn notification_sender(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+    ) -> Result<Box<dyn NotificationSender>, NotificationSenderError> {
+        T::notification_sender(self, target, protocol)
+    }
+
+    fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>) {
+        T::set_notification_handshake(self, protocol, handshake)
+    }
+}
+
+/// Provides ability to send network requests.
+#[async_trait::async_trait]
+pub trait NetworkRequest {
+    /// Sends a single targeted request to a specific peer. On success, returns the response of
+    /// the peer.
+    ///
+    /// Request-response protocols are a way to complement notifications protocols, but
+    /// notifications should remain the default ways of communicating information. For example, a
+    /// peer can announce something through a notification, after which the recipient can obtain
+    /// more information by performing a request.
+    /// As such, call this function with `IfDisconnected::ImmediateError` for `connect`. This way
+    /// you will get an error immediately for disconnected peers, instead of waiting for a
+    /// potentially very long connection attempt, which would suggest that something is wrong
+    /// anyway, as you are supposed to be connected because of the notification protocol.
+    ///
+    /// No limit or throttling of concurrent outbound requests per peer and protocol are enforced.
+    /// Such restrictions, if desired, need to be enforced at the call site(s).
+    ///
+    /// The protocol must have been registered through
+    /// `NetworkConfiguration::request_response_protocols`.
+    async fn request(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        connect: IfDisconnected,
+    ) -> Result<(Vec<u8>, ProtocolName), RequestFailure>;
+
+    /// Variation of `request` which starts a request whose response is delivered on a provided
+    /// channel.
+    ///
+    /// Instead of blocking and waiting for a reply, this function returns immediately, sending
+    /// responses via the passed in sender. This alternative API exists to make it easier to
+    /// integrate with message passing APIs.
+    ///
+    /// Keep in mind that the connected receiver might receive a `Canceled` event in case of a
+    /// closing connection. This is expected behaviour. With `request` you would get a
+    /// `RequestFailure::Network(OutboundFailure::ConnectionClosed)` in that case.
+    fn start_request(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    );
+}
+
+// Manual implementation to avoid extra boxing here
+impl<T> NetworkRequest for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkRequest,
+{
+    fn request<'life0, 'async_trait>(
+        &'life0 self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        connect: IfDisconnected,
+    ) -> Pin<
+        Box<
+            dyn Future<Output = Result<(Vec<u8>, ProtocolName), RequestFailure>>
+                + Send
+                + 'async_trait,
+        >,
+    >
+    where
+        'life0: 'async_trait,
+        Self: 'async_trait,
+    {
+        T::request(self, target, protocol, request, fallback_request, connect)
+    }
+
+    fn start_request(
+        &self,
+        target: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        fallback_request: Option<(Vec<u8>, ProtocolName)>,
+        tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        T::start_request(
+            self,
+            target,
+            protocol,
+            request,
+            fallback_request,
+            tx,
+            connect,
+        )
+    }
+}
+
+/// Provides ability to announce blocks to the network.
+pub trait NetworkBlock<BlockHash, BlockNumber> {
+    /// Make sure an important block is propagated to peers.
+    ///
+    /// In chain-based consensus, we often need to make sure non-best forks are
+    /// at least temporarily synced. This function forces such an announcement.
+    fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>);
+
+    /// Inform the network service about new best imported block.
+    fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber);
+}
+
+impl<T, BlockHash, BlockNumber> NetworkBlock<BlockHash, BlockNumber> for Arc<T>
+where
+    T: ?Sized,
+    T: NetworkBlock<BlockHash, BlockNumber>,
+{
+    fn announce_block(&self, hash: BlockHash, data: Option<Vec<u8>>) {
+        T::announce_block(self, hash, data)
+    }
+
+    fn new_best_block_imported(&self, hash: BlockHash, number: BlockNumber) {
+        T::new_best_block_imported(self, hash, number)
+    }
+}
+
+/// Substream acceptance result.
+#[derive(Debug, PartialEq, Eq)]
+pub enum ValidationResult {
+    /// Accept inbound substream.
+    Accept,
+
+    /// Reject inbound substream.
+    Reject,
+}
+
+/// Substream direction.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Direction {
+    /// Substream opened by the remote node.
+    Inbound,
+
+    /// Substream opened by the local node.
+    Outbound,
+}
+
+impl Direction {
+    /// Is the direction inbound.
+    pub fn is_inbound(&self) -> bool {
+        std::matches!(self, Direction::Inbound)
+    }
+}
+
+/// Events received by the protocol from `Notifications`.
+#[derive(Debug)]
+pub enum NotificationEvent {
+    /// Validate inbound substream.
+    ValidateInboundSubstream {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Received handshake.
+        handshake: Vec<u8>,
+
+        /// `oneshot::Sender` for sending validation result back to `Notifications`
+        result_tx: tokio::sync::oneshot::Sender<ValidationResult>,
+    },
+
+    /// Remote identified by `PeerId` opened a substream and sent `Handshake`.
+    /// Validate `Handshake` and report status (accept/reject) to `Notifications`.
+    NotificationStreamOpened {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Is the substream inbound or outbound.
+        direction: Direction,
+
+        /// Received handshake.
+        handshake: Vec<u8>,
+
+        /// Negotiated fallback.
+        negotiated_fallback: Option<ProtocolName>,
+    },
+
+    /// Substream was closed.
+    NotificationStreamClosed {
+        /// Peer Id.
+        peer: PeerId,
+    },
+
+    /// Notification was received from the substream.
+    NotificationReceived {
+        /// Peer ID.
+        peer: PeerId,
+
+        /// Received notification.
+        notification: Vec<u8>,
+    },
+}
+
+/// Notification service
+///
+/// Defines behaviors that both the protocol implementations and `Notifications` can expect from
+/// each other.
+///
+/// `Notifications` can send two different kinds of information to protocol:
+///  * substream-related information
+///  * notification-related information
+///
+/// When an unvalidated, inbound substream is received by `Notifications`, it sends the inbound
+/// stream information (peer ID, handshake) to protocol for validation. Protocol must then verify
+/// that the handshake is valid (and in the future that it has a slot it can allocate for the peer)
+/// and then report back the `ValidationResult` which is either `Accept` or `Reject`.
+///
+/// After the validation result has been received by `Notifications`, it prepares the
+/// substream for communication by initializing the necessary sinks and emits
+/// `NotificationStreamOpened` which informs the protocol that the remote peer is ready to receive
+/// notifications.
+///
+/// Two different flavors of sending options are provided:
+///  * synchronous sending ([`NotificationService::send_sync_notification()`])
+///  * asynchronous sending ([`NotificationService::send_async_notification()`])
+///
+/// The former is used by the protocols not ready to exercise backpressure and the latter by the
+/// protocols that can do it.
+///
+/// Both local and remote peer can close the substream at any time. Local peer can do so by calling
+/// [`NotificationService::close_substream()`] which instructs `Notifications` to close the
+/// substream. Remote closing the substream is indicated to the local peer by receiving
+/// [`NotificationEvent::NotificationStreamClosed`] event.
+///
+/// In case the protocol must update its handshake while it's operating (such as updating the best
+/// block information), it can do so by calling [`NotificationService::set_handshake()`]
+/// which instructs `Notifications` to update the handshake it stored during protocol
+/// initialization.
+///
+/// All peer events are multiplexed on the same incoming event stream from `Notifications` and thus
+/// each event carries a `PeerId` so the protocol knows whose information to update when receiving
+/// an event.
+#[async_trait::async_trait]
+pub trait NotificationService: Debug + Send {
+    /// Instruct `Notifications` to open a new substream for `peer`.
+    ///
+    /// `dial_if_disconnected` informs `Notifications` whether to dial
+    // the peer if there is currently no active connection to it.
+    //
+    // NOTE: not offered by the current implementation
+    async fn open_substream(&mut self, peer: PeerId) -> Result<(), ()>;
+
+    /// Instruct `Notifications` to close substream for `peer`.
+    //
+    // NOTE: not offered by the current implementation
+    async fn close_substream(&mut self, peer: PeerId) -> Result<(), ()>;
+
+    /// Send synchronous `notification` to `peer`.
+    fn send_sync_notification(&self, peer: &PeerId, notification: Vec<u8>);
+
+    /// Send asynchronous `notification` to `peer`, allowing sender to exercise backpressure.
+    ///
+    /// Returns an error if the peer doesn't exist.
+    async fn send_async_notification(
+        &self,
+        peer: &PeerId,
+        notification: Vec<u8>,
+    ) -> Result<(), error::Error>;
+
+    /// Set handshake for the notification protocol replacing the old handshake.
+    async fn set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
+
+    /// Non-blocking variant of `set_handshake()` that attempts to update the handshake
+    /// and returns an error if the channel is blocked.
+    ///
+    /// Technically the function can return an error if the channel to `Notifications` is closed
+    /// but that doesn't happen under normal operation.
+    #[allow(clippy::result_unit_err)]
+    fn try_set_handshake(&mut self, handshake: Vec<u8>) -> Result<(), ()>;
+
+    /// Get next event from the `Notifications` event stream.
+    async fn next_event(&mut self) -> Option<NotificationEvent>;
+
+    /// Make a copy of the object so it can be shared between protocol components
+    /// who wish to have access to the same underlying notification protocol.
+    #[allow(clippy::result_unit_err)]
+    fn clone(&mut self) -> Result<Box<dyn NotificationService>, ()>;
+
+    /// Get protocol name of the `NotificationService`.
+    fn protocol(&self) -> &ProtocolName;
+
+    /// Get message sink of the peer.
+    fn message_sink(&self, peer: &PeerId) -> Option<Box<dyn MessageSink>>;
+}
+
+/// Message sink for peers.
+///
+/// If protocol cannot use [`NotificationService`] to send notifications to peers and requires,
+/// e.g., notifications to be sent in another task, the protocol may acquire a [`MessageSink`]
+/// object for each peer by calling [`NotificationService::message_sink()`]. Calling this
+/// function returns an object which allows the protocol to send notifications to the remote peer.
+///
+/// Use of this API is discouraged as it's not as performant as sending notifications through
+/// [`NotificationService`] due to synchronization required to keep the underlying notification
+/// sink up to date with possible sink replacement events.
+#[async_trait::async_trait]
+pub trait MessageSink: Send + Sync {
+    /// Send synchronous `notification` to the peer associated with this [`MessageSink`].
+    fn send_sync_notification(&self, notification: Vec<u8>);
+
+    /// Send an asynchronous `notification` to to the peer associated with this [`MessageSink`],
+    /// allowing sender to exercise backpressure.
+    ///
+    /// Returns an error if the peer does not exist.
+    async fn send_async_notification(&self, notification: Vec<u8>) -> Result<(), error::Error>;
+}
diff --git a/libp2p/src/transport.rs b/libp2p/src/transport.rs
new file mode 100644
index 000000000..f0a4d63ab
--- /dev/null
+++ b/libp2p/src/transport.rs
@@ -0,0 +1,108 @@
+// 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/>.
+
+//! Transport that serves as a common ground for all connections.
+
+use either::Either;
+use libp2p::{
+    core::{
+        muxing::StreamMuxerBox,
+        transport::{Boxed, OptionalTransport},
+        upgrade,
+    },
+    dns, identity, noise, tcp, websocket, PeerId, Transport, TransportExt,
+};
+use std::{sync::Arc, time::Duration};
+
+pub use libp2p::bandwidth::BandwidthSinks;
+
+/// Builds the transport that serves as a common ground for all connections.
+///
+/// If `memory_only` is true, then only communication within the same process are allowed. Only
+/// addresses with the format `/memory/...` are allowed.
+///
+/// `yamux_window_size` is the maximum size of the Yamux receive windows. `None` to leave the
+/// default (256kiB).
+///
+/// `yamux_maximum_buffer_size` is the maximum allowed size of the Yamux buffer. This should be
+/// set either to the maximum of all the maximum allowed sizes of messages frames of all
+/// high-level protocols combined, or to some generously high value if you are sure that a maximum
+/// size is enforced on all high-level protocols.
+///
+/// Returns a `BandwidthSinks` object that allows querying the average bandwidth produced by all
+/// the connections spawned with this transport.
+pub fn build_transport(
+    keypair: identity::Keypair,
+    memory_only: bool,
+    yamux_window_size: Option<u32>,
+    yamux_maximum_buffer_size: usize,
+) -> (Boxed<(PeerId, StreamMuxerBox)>, Arc<BandwidthSinks>) {
+    // Build the base layer of the transport.
+    let transport = if !memory_only {
+        // Main transport: DNS(TCP)
+        let tcp_config = tcp::Config::new().nodelay(true);
+        let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
+        let dns_init = dns::TokioDnsConfig::system(tcp_trans);
+
+        Either::Left(if let Ok(dns) = dns_init {
+            // WS + WSS transport
+            //
+            // Main transport can't be used for `/wss` addresses because WSS transport needs
+            // unresolved addresses (BUT WSS transport itself needs an instance of DNS transport to
+            // resolve and dial addresses).
+            let tcp_trans = tcp::tokio::Transport::new(tcp_config);
+            let dns_for_wss = dns::TokioDnsConfig::system(tcp_trans)
+                .expect("same system_conf & resolver to work");
+            Either::Left(websocket::WsConfig::new(dns_for_wss).or_transport(dns))
+        } else {
+            // In case DNS can't be constructed, fallback to TCP + WS (WSS won't work)
+            let tcp_trans = tcp::tokio::Transport::new(tcp_config.clone());
+            let desktop_trans = websocket::WsConfig::new(tcp_trans)
+                .or_transport(tcp::tokio::Transport::new(tcp_config));
+            Either::Right(desktop_trans)
+        })
+    } else {
+        Either::Right(OptionalTransport::some(
+            libp2p::core::transport::MemoryTransport::default(),
+        ))
+    };
+
+    let authentication_config = noise::Config::new(&keypair).expect("Can create noise config. qed");
+    let multiplexing_config = {
+        let mut yamux_config = libp2p::yamux::Config::default();
+        // Enable proper flow-control: window updates are only sent when
+        // buffered data has been consumed.
+        yamux_config.set_window_update_mode(libp2p::yamux::WindowUpdateMode::on_read());
+        yamux_config.set_max_buffer_size(yamux_maximum_buffer_size);
+
+        if let Some(yamux_window_size) = yamux_window_size {
+            yamux_config.set_receive_window_size(yamux_window_size);
+        }
+
+        yamux_config
+    };
+
+    let transport = transport
+        .upgrade(upgrade::Version::V1Lazy)
+        .authenticate(authentication_config)
+        .multiplex(multiplexing_config)
+        .timeout(Duration::from_secs(20))
+        .boxed();
+
+    transport.with_bandwidth_logging()
+}
diff --git a/libp2p/src/types.rs b/libp2p/src/types.rs
new file mode 100644
index 000000000..69a0f53f7
--- /dev/null
+++ b/libp2p/src/types.rs
@@ -0,0 +1,158 @@
+// 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/>.
+
+//! `sc-network` type definitions
+
+use libp2p::core::upgrade;
+
+use std::{
+    borrow::Borrow,
+    fmt,
+    hash::{Hash, Hasher},
+    ops::Deref,
+    sync::Arc,
+};
+
+/// The protocol name transmitted on the wire.
+#[derive(Debug, Clone)]
+pub enum ProtocolName {
+    /// The protocol name as a static string.
+    Static(&'static str),
+    /// The protocol name as a dynamically allocated string.
+    OnHeap(Arc<str>),
+}
+
+impl From<&'static str> for ProtocolName {
+    fn from(name: &'static str) -> Self {
+        Self::Static(name)
+    }
+}
+
+impl From<Arc<str>> for ProtocolName {
+    fn from(name: Arc<str>) -> Self {
+        Self::OnHeap(name)
+    }
+}
+
+impl From<String> for ProtocolName {
+    fn from(name: String) -> Self {
+        Self::OnHeap(Arc::from(name))
+    }
+}
+
+impl Deref for ProtocolName {
+    type Target = str;
+
+    fn deref(&self) -> &str {
+        match self {
+            Self::Static(name) => name,
+            Self::OnHeap(name) => name,
+        }
+    }
+}
+
+impl Borrow<str> for ProtocolName {
+    fn borrow(&self) -> &str {
+        self
+    }
+}
+
+impl PartialEq for ProtocolName {
+    fn eq(&self, other: &Self) -> bool {
+        (self as &str) == (other as &str)
+    }
+}
+
+impl Eq for ProtocolName {}
+
+impl Hash for ProtocolName {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        (self as &str).hash(state)
+    }
+}
+
+impl fmt::Display for ProtocolName {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(self)
+    }
+}
+
+impl upgrade::ProtocolName for ProtocolName {
+    fn protocol_name(&self) -> &[u8] {
+        (self as &str).as_bytes()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::ProtocolName;
+    use std::{
+        borrow::Borrow,
+        collections::hash_map::DefaultHasher,
+        hash::{Hash, Hasher},
+    };
+
+    #[test]
+    fn protocol_name_keys_are_equivalent_to_str_keys() {
+        const PROTOCOL: &str = "/some/protocol/1";
+        let static_protocol_name = ProtocolName::from(PROTOCOL);
+        let on_heap_protocol_name = ProtocolName::from(String::from(PROTOCOL));
+
+        assert_eq!(
+            <ProtocolName as Borrow<str>>::borrow(&static_protocol_name),
+            PROTOCOL
+        );
+        assert_eq!(
+            <ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name),
+            PROTOCOL
+        );
+        assert_eq!(static_protocol_name, on_heap_protocol_name);
+
+        assert_eq!(hash(static_protocol_name), hash(PROTOCOL));
+        assert_eq!(hash(on_heap_protocol_name), hash(PROTOCOL));
+    }
+
+    #[test]
+    fn different_protocol_names_do_not_compare_equal() {
+        const PROTOCOL1: &str = "/some/protocol/1";
+        let static_protocol_name1 = ProtocolName::from(PROTOCOL1);
+        let on_heap_protocol_name1 = ProtocolName::from(String::from(PROTOCOL1));
+
+        const PROTOCOL2: &str = "/some/protocol/2";
+        let static_protocol_name2 = ProtocolName::from(PROTOCOL2);
+        let on_heap_protocol_name2 = ProtocolName::from(String::from(PROTOCOL2));
+
+        assert_ne!(
+            <ProtocolName as Borrow<str>>::borrow(&static_protocol_name1),
+            PROTOCOL2
+        );
+        assert_ne!(
+            <ProtocolName as Borrow<str>>::borrow(&on_heap_protocol_name1),
+            PROTOCOL2
+        );
+        assert_ne!(static_protocol_name1, static_protocol_name2);
+        assert_ne!(static_protocol_name1, on_heap_protocol_name2);
+        assert_ne!(on_heap_protocol_name1, on_heap_protocol_name2);
+    }
+
+    fn hash<T: Hash>(x: T) -> u64 {
+        let mut hasher = DefaultHasher::new();
+        x.hash(&mut hasher);
+        hasher.finish()
+    }
+}
diff --git a/libp2p/src/utils.rs b/libp2p/src/utils.rs
new file mode 100644
index 000000000..d87c78640
--- /dev/null
+++ b/libp2p/src/utils.rs
@@ -0,0 +1,91 @@
+// 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/>.
+
+//! `sc-network` utilities
+
+use futures::{stream::unfold, FutureExt, Stream, StreamExt};
+use futures_timer::Delay;
+use linked_hash_set::LinkedHashSet;
+
+use std::{hash::Hash, num::NonZeroUsize, time::Duration};
+
+/// Creates a stream that returns a new value every `duration`.
+pub fn interval(duration: Duration) -> impl Stream<Item = ()> + Unpin {
+    unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop)
+}
+
+/// Wrapper around `LinkedHashSet` with bounded growth.
+///
+/// In the limit, for each element inserted the oldest existing element will be removed.
+#[derive(Debug, Clone)]
+pub struct LruHashSet<T: Hash + Eq> {
+    set: LinkedHashSet<T>,
+    limit: NonZeroUsize,
+}
+
+impl<T: Hash + Eq> LruHashSet<T> {
+    /// Create a new `LruHashSet` with the given (exclusive) limit.
+    pub fn new(limit: NonZeroUsize) -> Self {
+        Self {
+            set: LinkedHashSet::new(),
+            limit,
+        }
+    }
+
+    /// Insert element into the set.
+    ///
+    /// Returns `true` if this is a new element to the set, `false` otherwise.
+    /// Maintains the limit of the set by removing the oldest entry if necessary.
+    /// Inserting the same element will update its LRU position.
+    pub fn insert(&mut self, e: T) -> bool {
+        if self.set.insert(e) {
+            if self.set.len() == usize::from(self.limit) {
+                self.set.pop_front(); // remove oldest entry
+            }
+            return true;
+        }
+        false
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn maintains_limit() {
+        let three = NonZeroUsize::new(3).unwrap();
+        let mut set = LruHashSet::<u8>::new(three);
+
+        // First element.
+        assert!(set.insert(1));
+        assert_eq!(vec![&1], set.set.iter().collect::<Vec<_>>());
+
+        // Second element.
+        assert!(set.insert(2));
+        assert_eq!(vec![&1, &2], set.set.iter().collect::<Vec<_>>());
+
+        // Inserting the same element updates its LRU position.
+        assert!(!set.insert(1));
+        assert_eq!(vec![&2, &1], set.set.iter().collect::<Vec<_>>());
+
+        // We reached the limit. The next element forces the oldest one out.
+        assert!(set.insert(3));
+        assert_eq!(vec![&1, &3], set.set.iter().collect::<Vec<_>>());
+    }
+}
diff --git a/libp2p/statement/Cargo.toml b/libp2p/statement/Cargo.toml
new file mode 100644
index 000000000..dc1030d84
--- /dev/null
+++ b/libp2p/statement/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+description = "Substrate statement protocol"
+name = "sc-network-statement"
+version = "0.16.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-statement"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+array-bytes = "6.1"
+async-channel = "1.8.0"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] }
+futures = "0.3.21"
+libp2p = "0.51.4"
+log = "0.4.17"
+prometheus-endpoint = { workspace = true }
+sc-network-common = { path = "../common" }
+sc-network-sync = { path = "../sync" }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sp-consensus = { workspace = true }
+sp-statement-store = { workspace = true }
diff --git a/libp2p/statement/src/config.rs b/libp2p/statement/src/config.rs
new file mode 100644
index 000000000..159998a0f
--- /dev/null
+++ b/libp2p/statement/src/config.rs
@@ -0,0 +1,33 @@
+// 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/>.
+
+//! Configuration of the statement protocol
+
+use std::time;
+
+/// Interval at which we propagate statements;
+pub(crate) const PROPAGATE_TIMEOUT: time::Duration = time::Duration::from_millis(1000);
+
+/// Maximum number of known statement hashes to keep for a peer.
+pub(crate) const MAX_KNOWN_STATEMENTS: usize = 10240;
+
+/// Maximum allowed size for a statement notification.
+pub(crate) const MAX_STATEMENT_SIZE: u64 = 256 * 1024;
+
+/// Maximum number of statement validation request we keep at any moment.
+pub(crate) const MAX_PENDING_STATEMENTS: usize = 8192;
diff --git a/libp2p/statement/src/lib.rs b/libp2p/statement/src/lib.rs
new file mode 100644
index 000000000..5187e681d
--- /dev/null
+++ b/libp2p/statement/src/lib.rs
@@ -0,0 +1,484 @@
+// 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/>.
+
+//! Statement handling to plug on top of the network service.
+//!
+//! Usage:
+//!
+//! - Use [`StatementHandlerPrototype::new`] to create a prototype.
+//! - Pass the `NonDefaultSetConfig` returned from [`StatementHandlerPrototype::new`] to the network
+//!   configuration as an extra peers set.
+//! - Use [`StatementHandlerPrototype::build`] then [`StatementHandler::run`] to obtain a
+//! `Future` that processes statements.
+
+use crate::config::*;
+
+use codec::{Decode, Encode};
+use futures::{channel::oneshot, prelude::*, stream::FuturesUnordered, FutureExt};
+use libp2p::{multiaddr, PeerId};
+use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
+use sc_network::{
+	config::{NonDefaultSetConfig, NonReservedPeerMode, SetConfig},
+	error,
+	service::traits::{NotificationEvent, NotificationService, ValidationResult},
+	types::ProtocolName,
+	utils::{interval, LruHashSet},
+	NetworkEventStream, NetworkNotification, NetworkPeers,
+};
+use sc_network_common::role::ObservedRole;
+use sc_network_sync::{SyncEvent, SyncEventStream};
+use sp_statement_store::{
+	Hash, NetworkPriority, Statement, StatementSource, StatementStore, SubmitResult,
+};
+use std::{
+	collections::{hash_map::Entry, HashMap, HashSet},
+	iter,
+	num::NonZeroUsize,
+	pin::Pin,
+	sync::Arc,
+};
+
+pub mod config;
+
+/// A set of statements.
+pub type Statements = Vec<Statement>;
+/// Future resolving to statement import result.
+pub type StatementImportFuture = oneshot::Receiver<SubmitResult>;
+
+mod rep {
+	use sc_network::ReputationChange as Rep;
+	/// Reputation change when a peer sends us any statement.
+	///
+	/// This forces node to verify it, thus the negative value here. Once statement is verified,
+	/// reputation change should be refunded with `ANY_STATEMENT_REFUND`
+	pub const ANY_STATEMENT: Rep = Rep::new(-(1 << 4), "Any statement");
+	/// Reputation change when a peer sends us any statement that is not invalid.
+	pub const ANY_STATEMENT_REFUND: Rep = Rep::new(1 << 4, "Any statement (refund)");
+	/// Reputation change when a peer sends us an statement that we didn't know about.
+	pub const GOOD_STATEMENT: Rep = Rep::new(1 << 7, "Good statement");
+	/// Reputation change when a peer sends us a bad statement.
+	pub const BAD_STATEMENT: Rep = Rep::new(-(1 << 12), "Bad statement");
+	/// Reputation change when a peer sends us a duplicate statement.
+	pub const DUPLICATE_STATEMENT: Rep = Rep::new(-(1 << 7), "Duplicate statement");
+	/// Reputation change when a peer sends us particularly useful statement
+	pub const EXCELLENT_STATEMENT: Rep = Rep::new(1 << 8, "High priority statement");
+}
+
+const LOG_TARGET: &str = "statement-gossip";
+
+struct Metrics {
+	propagated_statements: Counter<U64>,
+}
+
+impl Metrics {
+	fn register(r: &Registry) -> Result<Self, PrometheusError> {
+		Ok(Self {
+			propagated_statements: register(
+				Counter::new(
+					"substrate_sync_propagated_statements",
+					"Number of statements propagated to at least one peer",
+				)?,
+				r,
+			)?,
+		})
+	}
+}
+
+/// Prototype for a [`StatementHandler`].
+pub struct StatementHandlerPrototype {
+	protocol_name: ProtocolName,
+	notification_service: Box<dyn NotificationService>,
+}
+
+impl StatementHandlerPrototype {
+	/// Create a new instance.
+	pub fn new<Hash: AsRef<[u8]>>(
+		genesis_hash: Hash,
+		fork_id: Option<&str>,
+	) -> (Self, NonDefaultSetConfig) {
+		let genesis_hash = genesis_hash.as_ref();
+		let protocol_name = if let Some(fork_id) = fork_id {
+			format!("/{}/{}/statement/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
+		} else {
+			format!("/{}/statement/1", array_bytes::bytes2hex("", genesis_hash))
+		};
+		let (config, notification_service) = NonDefaultSetConfig::new(
+			protocol_name.clone().into(),
+			Vec::new(),
+			MAX_STATEMENT_SIZE,
+			None,
+			SetConfig {
+				in_peers: 0,
+				out_peers: 0,
+				reserved_nodes: Vec::new(),
+				non_reserved_mode: NonReservedPeerMode::Deny,
+			},
+		);
+
+		(Self { protocol_name: protocol_name.into(), notification_service }, config)
+	}
+
+	/// Turns the prototype into the actual handler.
+	///
+	/// Important: the statements handler is initially disabled and doesn't gossip statements.
+	/// Gossiping is enabled when major syncing is done.
+	pub fn build<
+		N: NetworkPeers + NetworkEventStream + NetworkNotification,
+		S: SyncEventStream + sp_consensus::SyncOracle,
+	>(
+		self,
+		network: N,
+		sync: S,
+		statement_store: Arc<dyn StatementStore>,
+		metrics_registry: Option<&Registry>,
+		executor: impl Fn(Pin<Box<dyn Future<Output = ()> + Send>>) + Send,
+	) -> error::Result<StatementHandler<N, S>> {
+		let sync_event_stream = sync.event_stream("statement-handler-sync");
+		let (queue_sender, mut queue_receiver) = async_channel::bounded(100_000);
+
+		let store = statement_store.clone();
+		executor(
+			async move {
+				loop {
+					let task: Option<(Statement, oneshot::Sender<SubmitResult>)> =
+						queue_receiver.next().await;
+					match task {
+						None => return,
+						Some((statement, completion)) => {
+							let result = store.submit(statement, StatementSource::Network);
+							if completion.send(result).is_err() {
+								log::debug!(
+									target: LOG_TARGET,
+									"Error sending validation completion"
+								);
+							}
+						},
+					}
+				}
+			}
+			.boxed(),
+		);
+
+		let handler = StatementHandler {
+			protocol_name: self.protocol_name,
+			notification_service: self.notification_service,
+			propagate_timeout: (Box::pin(interval(PROPAGATE_TIMEOUT))
+				as Pin<Box<dyn Stream<Item = ()> + Send>>)
+				.fuse(),
+			pending_statements: FuturesUnordered::new(),
+			pending_statements_peers: HashMap::new(),
+			network,
+			sync,
+			sync_event_stream: sync_event_stream.fuse(),
+			peers: HashMap::new(),
+			statement_store,
+			queue_sender,
+			metrics: if let Some(r) = metrics_registry {
+				Some(Metrics::register(r)?)
+			} else {
+				None
+			},
+		};
+
+		Ok(handler)
+	}
+}
+
+/// Handler for statements. Call [`StatementHandler::run`] to start the processing.
+pub struct StatementHandler<
+	N: NetworkPeers + NetworkEventStream + NetworkNotification,
+	S: SyncEventStream + sp_consensus::SyncOracle,
+> {
+	protocol_name: ProtocolName,
+	/// Interval at which we call `propagate_statements`.
+	propagate_timeout: stream::Fuse<Pin<Box<dyn Stream<Item = ()> + Send>>>,
+	/// Pending statements verification tasks.
+	pending_statements:
+		FuturesUnordered<Pin<Box<dyn Future<Output = (Hash, Option<SubmitResult>)> + Send>>>,
+	/// As multiple peers can send us the same statement, we group
+	/// these peers using the statement hash while the statement is
+	/// imported. This prevents that we import the same statement
+	/// multiple times concurrently.
+	pending_statements_peers: HashMap<Hash, HashSet<PeerId>>,
+	/// Network service to use to send messages and manage peers.
+	network: N,
+	/// Syncing service.
+	sync: S,
+	/// Receiver for syncing-related events.
+	sync_event_stream: stream::Fuse<Pin<Box<dyn Stream<Item = SyncEvent> + Send>>>,
+	/// Notification service.
+	notification_service: Box<dyn NotificationService>,
+	// All connected peers
+	peers: HashMap<PeerId, Peer>,
+	statement_store: Arc<dyn StatementStore>,
+	queue_sender: async_channel::Sender<(Statement, oneshot::Sender<SubmitResult>)>,
+	/// Prometheus metrics.
+	metrics: Option<Metrics>,
+}
+
+/// Peer information
+#[derive(Debug)]
+struct Peer {
+	/// Holds a set of statements known to this peer.
+	known_statements: LruHashSet<Hash>,
+	role: ObservedRole,
+}
+
+impl<N, S> StatementHandler<N, S>
+where
+	N: NetworkPeers + NetworkEventStream + NetworkNotification,
+	S: SyncEventStream + sp_consensus::SyncOracle,
+{
+	/// Turns the [`StatementHandler`] into a future that should run forever and not be
+	/// interrupted.
+	pub async fn run(mut self) {
+		loop {
+			futures::select! {
+				_ = self.propagate_timeout.next() => {
+					self.propagate_statements();
+				},
+				(hash, result) = self.pending_statements.select_next_some() => {
+					if let Some(peers) = self.pending_statements_peers.remove(&hash) {
+						if let Some(result) = result {
+							peers.into_iter().for_each(|p| self.on_handle_statement_import(p, &result));
+						}
+					} else {
+						log::warn!(target: LOG_TARGET, "Inconsistent state, no peers for pending statement!");
+					}
+				},
+				sync_event = self.sync_event_stream.next() => {
+					if let Some(sync_event) = sync_event {
+						self.handle_sync_event(sync_event);
+					} else {
+						// Syncing has seemingly closed. Closing as well.
+						return;
+					}
+				}
+				event = self.notification_service.next_event().fuse() => {
+					if let Some(event) = event {
+						self.handle_notification_event(event)
+					} else {
+						// `Notifications` has seemingly closed. Closing as well.
+						return
+					}
+				}
+			}
+		}
+	}
+
+	fn handle_sync_event(&mut self, event: SyncEvent) {
+		match event {
+			SyncEvent::PeerConnected(remote) => {
+				let addr = iter::once(multiaddr::Protocol::P2p(remote.into()))
+					.collect::<multiaddr::Multiaddr>();
+				let result = self.network.add_peers_to_reserved_set(
+					self.protocol_name.clone(),
+					iter::once(addr).collect(),
+				);
+				if let Err(err) = result {
+					log::error!(target: LOG_TARGET, "Add reserved peer failed: {}", err);
+				}
+			},
+			SyncEvent::PeerDisconnected(remote) => {
+				let result = self.network.remove_peers_from_reserved_set(
+					self.protocol_name.clone(),
+					iter::once(remote).collect(),
+				);
+				if let Err(err) = result {
+					log::error!(target: LOG_TARGET, "Failed to remove reserved peer: {err}");
+				}
+			},
+		}
+	}
+
+	fn handle_notification_event(&mut self, event: NotificationEvent) {
+		match event {
+			NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx, .. } => {
+				// only accept peers whose role can be determined
+				let result = self
+					.network
+					.peer_role(peer, handshake)
+					.map_or(ValidationResult::Reject, |_| ValidationResult::Accept);
+				let _ = result_tx.send(result);
+			},
+			NotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+				let Some(role) = self.network.peer_role(peer, handshake) else {
+					log::debug!(target: LOG_TARGET, "role for {peer} couldn't be determined");
+					return
+				};
+
+				let _was_in = self.peers.insert(
+					peer,
+					Peer {
+						known_statements: LruHashSet::new(
+							NonZeroUsize::new(MAX_KNOWN_STATEMENTS).expect("Constant is nonzero"),
+						),
+						role,
+					},
+				);
+				debug_assert!(_was_in.is_none());
+			},
+			NotificationEvent::NotificationStreamClosed { peer } => {
+				let _peer = self.peers.remove(&peer);
+				debug_assert!(_peer.is_some());
+			},
+			NotificationEvent::NotificationReceived { peer, notification } => {
+				// Accept statements only when node is not major syncing
+				if self.sync.is_major_syncing() {
+					log::trace!(
+						target: LOG_TARGET,
+						"{peer}: Ignoring statements while major syncing or offline"
+					);
+					return
+				}
+
+				if let Ok(statements) = <Statements as Decode>::decode(&mut notification.as_ref()) {
+					self.on_statements(peer, statements);
+				} else {
+					log::debug!(target: LOG_TARGET, "Failed to decode statement list from {peer}");
+				}
+			},
+		}
+	}
+
+	/// Called when peer sends us new statements
+	fn on_statements(&mut self, who: PeerId, statements: Statements) {
+		log::trace!(target: LOG_TARGET, "Received {} statements from {}", statements.len(), who);
+		if let Some(ref mut peer) = self.peers.get_mut(&who) {
+			for s in statements {
+				if self.pending_statements.len() > MAX_PENDING_STATEMENTS {
+					log::debug!(
+						target: LOG_TARGET,
+						"Ignoring any further statements that exceed `MAX_PENDING_STATEMENTS`({}) limit",
+						MAX_PENDING_STATEMENTS,
+					);
+					break
+				}
+
+				let hash = s.hash();
+				peer.known_statements.insert(hash);
+
+				self.network.report_peer(who, rep::ANY_STATEMENT);
+
+				match self.pending_statements_peers.entry(hash) {
+					Entry::Vacant(entry) => {
+						let (completion_sender, completion_receiver) = oneshot::channel();
+						match self.queue_sender.try_send((s, completion_sender)) {
+							Ok(()) => {
+								self.pending_statements.push(
+									async move {
+										let res = completion_receiver.await;
+										(hash, res.ok())
+									}
+									.boxed(),
+								);
+								entry.insert(HashSet::from_iter([who]));
+							},
+							Err(async_channel::TrySendError::Full(_)) => {
+								log::debug!(
+									target: LOG_TARGET,
+									"Dropped statement because validation channel is full",
+								);
+							},
+							Err(async_channel::TrySendError::Closed(_)) => {
+								log::trace!(
+									target: LOG_TARGET,
+									"Dropped statement because validation channel is closed",
+								);
+							},
+						}
+					},
+					Entry::Occupied(mut entry) => {
+						if !entry.get_mut().insert(who) {
+							// Already received this from the same peer.
+							self.network.report_peer(who, rep::DUPLICATE_STATEMENT);
+						}
+					},
+				}
+			}
+		}
+	}
+
+	fn on_handle_statement_import(&mut self, who: PeerId, import: &SubmitResult) {
+		match import {
+			SubmitResult::New(NetworkPriority::High) =>
+				self.network.report_peer(who, rep::EXCELLENT_STATEMENT),
+			SubmitResult::New(NetworkPriority::Low) =>
+				self.network.report_peer(who, rep::GOOD_STATEMENT),
+			SubmitResult::Known => self.network.report_peer(who, rep::ANY_STATEMENT_REFUND),
+			SubmitResult::KnownExpired => {},
+			SubmitResult::Ignored => {},
+			SubmitResult::Bad(_) => self.network.report_peer(who, rep::BAD_STATEMENT),
+			SubmitResult::InternalError(_) => {},
+		}
+	}
+
+	/// Propagate one statement.
+	pub fn propagate_statement(&mut self, hash: &Hash) {
+		// Accept statements only when node is not major syncing
+		if self.sync.is_major_syncing() {
+			return
+		}
+
+		log::debug!(target: LOG_TARGET, "Propagating statement [{:?}]", hash);
+		if let Ok(Some(statement)) = self.statement_store.statement(hash) {
+			self.do_propagate_statements(&[(*hash, statement)]);
+		}
+	}
+
+	fn do_propagate_statements(&mut self, statements: &[(Hash, Statement)]) {
+		let mut propagated_statements = 0;
+
+		for (who, peer) in self.peers.iter_mut() {
+			// never send statements to light nodes
+			if matches!(peer.role, ObservedRole::Light) {
+				continue
+			}
+
+			let to_send = statements
+				.iter()
+				.filter_map(|(hash, stmt)| peer.known_statements.insert(*hash).then(|| stmt))
+				.collect::<Vec<_>>();
+
+			propagated_statements += to_send.len();
+
+			if !to_send.is_empty() {
+				log::trace!(target: LOG_TARGET, "Sending {} statements to {}", to_send.len(), who);
+				self.network
+					.write_notification(*who, self.protocol_name.clone(), to_send.encode());
+			}
+		}
+
+		if let Some(ref metrics) = self.metrics {
+			metrics.propagated_statements.inc_by(propagated_statements as _)
+		}
+	}
+
+	/// Call when we must propagate ready statements to peers.
+	fn propagate_statements(&mut self) {
+		// Send out statements only when node is not major syncing
+		if self.sync.is_major_syncing() {
+			return
+		}
+
+		log::debug!(target: LOG_TARGET, "Propagating statements");
+		if let Ok(statements) = self.statement_store.statements() {
+			self.do_propagate_statements(&statements);
+		}
+	}
+}
diff --git a/libp2p/sync/Cargo.toml b/libp2p/sync/Cargo.toml
new file mode 100644
index 000000000..43c5a1c1f
--- /dev/null
+++ b/libp2p/sync/Cargo.toml
@@ -0,0 +1,53 @@
+[package]
+description = "Substrate sync network protocol"
+name = "sc-network-sync"
+version = "0.33.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-sync"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[build-dependencies]
+prost-build = "0.11"
+
+[dependencies]
+array-bytes = "6.1"
+async-channel = "1.8.0"
+async-trait = "0.1.74"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] }
+futures = "0.3.21"
+futures-timer = "3.0.2"
+libp2p = "0.51.4"
+log = "0.4.17"
+mockall = "0.11.3"
+prost = "0.12"
+schnellru = "0.2.1"
+smallvec = "1.11.0"
+thiserror = "1.0"
+tokio-stream = "0.1.14"
+tokio = { version = "1.32.0", features = ["macros", "time"] }
+fork-tree = { workspace = true }
+prometheus-endpoint = { workspace = true }
+sc-client-api = { workspace = true }
+sc-consensus = { workspace = true }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sc-network-common = { path = "../common" }
+sc-utils = { workspace = true }
+sp-arithmetic = { workspace = true }
+sp-blockchain = { workspace = true }
+sp-consensus = { workspace = true }
+sp-core = { workspace = true }
+sp-consensus-grandpa = { workspace = true }
+sp-runtime = { workspace = true }
+
+[dev-dependencies]
+mockall = "0.11.3"
+quickcheck = { version = "1.0.3", default-features = false }
+sc-block-builder = { workspace = true }
+sp-test-primitives = { workspace = true }
+sp-tracing = { workspace = true }
+substrate-test-runtime-client = { workspace = true }
diff --git a/libp2p/sync/build.rs b/libp2p/sync/build.rs
new file mode 100644
index 000000000..7b34a2c85
--- /dev/null
+++ b/libp2p/sync/build.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+const PROTOS: &[&str] = &["src/schema/api.v1.proto"];
+
+fn main() {
+    prost_build::compile_protos(PROTOS, &["src/schema"]).unwrap();
+}
diff --git a/libp2p/sync/src/block_announce_validator.rs b/libp2p/sync/src/block_announce_validator.rs
new file mode 100644
index 000000000..2ed7a9748
--- /dev/null
+++ b/libp2p/sync/src/block_announce_validator.rs
@@ -0,0 +1,412 @@
+// 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/>.
+
+//! [`BlockAnnounceValidator`] is responsible for async validation of block announcements.
+//! [`Stream`] implemented by [`BlockAnnounceValidator`] never terminates.
+
+use crate::{futures_stream::FuturesStream, LOG_TARGET};
+use futures::{stream::FusedStream, Future, FutureExt, Stream, StreamExt};
+use libp2p::PeerId;
+use log::{debug, error, trace, warn};
+use sc_network_common::sync::message::BlockAnnounce;
+use sp_consensus::block_validation::Validation;
+use sp_runtime::traits::{Block as BlockT, Header, Zero};
+use std::{
+    collections::{hash_map::Entry, HashMap},
+    default::Default,
+    pin::Pin,
+    task::{Context, Poll},
+};
+
+/// Maximum number of concurrent block announce validations.
+///
+/// If the queue reaches the maximum, we drop any new block
+/// announcements.
+const MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS: usize = 256;
+
+/// Maximum number of concurrent block announce validations per peer.
+///
+/// See [`MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS`] for more information.
+const MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER: usize = 4;
+
+/// Item that yields [`Stream`] implementation of [`BlockAnnounceValidator`].
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) enum BlockAnnounceValidationResult<H> {
+    /// The announcement failed at validation.
+    ///
+    /// The peer reputation should be decreased.
+    Failure {
+        /// The id of the peer that send us the announcement.
+        peer_id: PeerId,
+        /// Should the peer be disconnected?
+        disconnect: bool,
+    },
+    /// The announcement was validated successfully and should be passed to [`crate::ChainSync`].
+    Process {
+        /// The id of the peer that send us the announcement.
+        peer_id: PeerId,
+        /// Was this their new best block?
+        is_new_best: bool,
+        /// The announcement.
+        announce: BlockAnnounce<H>,
+    },
+    /// The block announcement should be skipped.
+    Skip {
+        /// The id of the peer that send us the announcement.
+        peer_id: PeerId,
+    },
+}
+
+impl<H> BlockAnnounceValidationResult<H> {
+    fn peer_id(&self) -> &PeerId {
+        match self {
+            BlockAnnounceValidationResult::Failure { peer_id, .. }
+            | BlockAnnounceValidationResult::Process { peer_id, .. }
+            | BlockAnnounceValidationResult::Skip { peer_id } => peer_id,
+        }
+    }
+}
+
+/// Result of [`BlockAnnounceValidator::allocate_slot_for_block_announce_validation`].
+enum AllocateSlotForBlockAnnounceValidation {
+    /// Success, there is a slot for the block announce validation.
+    Allocated,
+    /// We reached the total maximum number of validation slots.
+    TotalMaximumSlotsReached,
+    /// We reached the maximum number of validation slots for the given peer.
+    MaximumPeerSlotsReached,
+}
+
+pub(crate) struct BlockAnnounceValidator<B: BlockT> {
+    /// A type to check incoming block announcements.
+    validator: Box<dyn sp_consensus::block_validation::BlockAnnounceValidator<B> + Send>,
+    /// All block announcements that are currently being validated.
+    validations: FuturesStream<Validations<B::Header>>,
+    /// Number of concurrent block announce validations per peer.
+    validations_per_peer: HashMap<PeerId, usize>,
+}
+
+pub(crate) type Validations<B> =
+    Pin<Box<dyn Future<Output = BlockAnnounceValidationResult<B>> + Send>>;
+
+impl<B: BlockT> BlockAnnounceValidator<B> {
+    pub(crate) fn new(
+        validator: Box<dyn sp_consensus::block_validation::BlockAnnounceValidator<B> + Send>,
+    ) -> Self {
+        Self {
+            validator,
+            validations: Default::default(),
+            validations_per_peer: Default::default(),
+        }
+    }
+
+    /// Push a block announce validation.
+    pub(crate) fn push_block_announce_validation(
+        &mut self,
+        peer_id: PeerId,
+        hash: B::Hash,
+        announce: BlockAnnounce<B::Header>,
+        is_best: bool,
+    ) {
+        let header = &announce.header;
+        let number = *header.number();
+        debug!(
+            target: LOG_TARGET,
+            "Pre-validating received block announcement {:?} with number {:?} from {}",
+            hash,
+            number,
+            peer_id,
+        );
+
+        if number.is_zero() {
+            warn!(
+                target: LOG_TARGET,
+                "💔 Ignored genesis block (#0) announcement from {}: {}",
+                peer_id,
+                hash,
+            );
+            return;
+        }
+
+        // Try to allocate a slot for this block announce validation.
+        match self.allocate_slot_for_block_announce_validation(&peer_id) {
+            AllocateSlotForBlockAnnounceValidation::Allocated => {}
+            AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached => {
+                warn!(
+                    target: LOG_TARGET,
+                    "💔 Ignored block (#{} -- {}) announcement from {} because all validation slots are occupied.",
+                    number,
+                    hash,
+                    peer_id,
+                );
+                return;
+            }
+            AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached => {
+                warn!(
+                    target: LOG_TARGET,
+                    "💔 Ignored block (#{} -- {}) announcement from {} because all validation slots for this peer are occupied.",
+                    number,
+                    hash,
+                    peer_id,
+                );
+                return;
+            }
+        }
+
+        // Let external validator check the block announcement.
+        let assoc_data = announce.data.as_ref().map_or(&[][..], |v| v.as_slice());
+        let future = self.validator.validate(header, assoc_data);
+
+        self.validations.push(
+			async move {
+				match future.await {
+					Ok(Validation::Success { is_new_best }) => {
+						let is_new_best = is_new_best || is_best;
+
+						trace!(
+							target: LOG_TARGET,
+							"Block announcement validated successfully: from {}: {:?}. Local best: {}.",
+							peer_id,
+							announce.summary(),
+							is_new_best,
+						);
+
+						BlockAnnounceValidationResult::Process { is_new_best, announce, peer_id }
+					},
+					Ok(Validation::Failure { disconnect }) => {
+						debug!(
+							target: LOG_TARGET,
+							"Block announcement validation failed: from {}, block {:?}. Disconnect: {}.",
+							peer_id,
+							hash,
+							disconnect,
+						);
+
+						BlockAnnounceValidationResult::Failure { peer_id, disconnect }
+					},
+					Err(e) => {
+						debug!(
+							target: LOG_TARGET,
+							"💔 Ignoring block announcement validation from {} of block {:?} due to internal error: {}.",
+							peer_id,
+							hash,
+							e,
+						);
+
+						BlockAnnounceValidationResult::Skip { peer_id }
+					},
+				}
+			}
+			.boxed(),
+		);
+    }
+
+    /// Checks if there is a slot for a block announce validation.
+    ///
+    /// The total number and the number per peer of concurrent block announce validations
+    /// is capped.
+    ///
+    /// Returns [`AllocateSlotForBlockAnnounceValidation`] to inform about the result.
+    ///
+    /// # Note
+    ///
+    /// It is *required* to call [`Self::deallocate_slot_for_block_announce_validation`] when the
+    /// validation is finished to clear the slot.
+    fn allocate_slot_for_block_announce_validation(
+        &mut self,
+        peer_id: &PeerId,
+    ) -> AllocateSlotForBlockAnnounceValidation {
+        if self.validations.len() >= MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS {
+            return AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached;
+        }
+
+        match self.validations_per_peer.entry(*peer_id) {
+            Entry::Vacant(entry) => {
+                entry.insert(1);
+                AllocateSlotForBlockAnnounceValidation::Allocated
+            }
+            Entry::Occupied(mut entry) => {
+                if *entry.get() < MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
+                    *entry.get_mut() += 1;
+                    AllocateSlotForBlockAnnounceValidation::Allocated
+                } else {
+                    AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached
+                }
+            }
+        }
+    }
+
+    /// Should be called when a block announce validation is finished, to update the slots
+    /// of the peer that send the block announce.
+    fn deallocate_slot_for_block_announce_validation(&mut self, peer_id: &PeerId) {
+        match self.validations_per_peer.entry(*peer_id) {
+            Entry::Vacant(_) => {
+                error!(
+                    target: LOG_TARGET,
+                    "💔 Block announcement validation from peer {} finished for a slot that was not allocated!",
+                    peer_id,
+                );
+            }
+            Entry::Occupied(mut entry) => match entry.get().checked_sub(1) {
+                Some(value) => {
+                    if value == 0 {
+                        entry.remove();
+                    } else {
+                        *entry.get_mut() = value;
+                    }
+                }
+                None => {
+                    entry.remove();
+
+                    error!(
+                        target: LOG_TARGET,
+                        "Invalid (zero) block announce validation slot counter for peer {peer_id}.",
+                    );
+                    debug_assert!(
+                        false,
+                        "Invalid (zero) block announce validation slot counter for peer {peer_id}.",
+                    );
+                }
+            },
+        }
+    }
+}
+
+impl<B: BlockT> Stream for BlockAnnounceValidator<B> {
+    type Item = BlockAnnounceValidationResult<B::Header>;
+
+    /// Poll for finished block announce validations. The stream never terminates.
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        let validation = futures::ready!(self.validations.poll_next_unpin(cx))
+            .expect("`FuturesStream` never terminates; qed");
+        self.deallocate_slot_for_block_announce_validation(validation.peer_id());
+
+        Poll::Ready(Some(validation))
+    }
+}
+
+// As [`BlockAnnounceValidator`] never terminates, we can easily implement [`FusedStream`] for it.
+impl<B: BlockT> FusedStream for BlockAnnounceValidator<B> {
+    fn is_terminated(&self) -> bool {
+        false
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::block_announce_validator::AllocateSlotForBlockAnnounceValidation;
+    use libp2p::PeerId;
+    use sp_consensus::block_validation::DefaultBlockAnnounceValidator;
+    use substrate_test_runtime_client::runtime::Block;
+
+    #[test]
+    fn allocate_one_validation_slot() {
+        let mut validator =
+            BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
+        let peer_id = PeerId::random();
+
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id),
+            AllocateSlotForBlockAnnounceValidation::Allocated,
+        ));
+    }
+
+    #[test]
+    fn allocate_validation_slots_for_two_peers() {
+        let mut validator =
+            BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
+        let peer_id_1 = PeerId::random();
+        let peer_id_2 = PeerId::random();
+
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id_1),
+            AllocateSlotForBlockAnnounceValidation::Allocated,
+        ));
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id_2),
+            AllocateSlotForBlockAnnounceValidation::Allocated,
+        ));
+    }
+
+    #[test]
+    fn maximum_validation_slots_per_peer() {
+        let mut validator =
+            BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
+        let peer_id = PeerId::random();
+
+        for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
+            assert!(matches!(
+                validator.allocate_slot_for_block_announce_validation(&peer_id),
+                AllocateSlotForBlockAnnounceValidation::Allocated,
+            ));
+        }
+
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id),
+            AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached,
+        ));
+    }
+
+    #[test]
+    fn validation_slots_per_peer_deallocated() {
+        let mut validator =
+            BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
+        let peer_id = PeerId::random();
+
+        for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS_PER_PEER {
+            assert!(matches!(
+                validator.allocate_slot_for_block_announce_validation(&peer_id),
+                AllocateSlotForBlockAnnounceValidation::Allocated,
+            ));
+        }
+
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id),
+            AllocateSlotForBlockAnnounceValidation::MaximumPeerSlotsReached,
+        ));
+
+        validator.deallocate_slot_for_block_announce_validation(&peer_id);
+
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id),
+            AllocateSlotForBlockAnnounceValidation::Allocated,
+        ));
+    }
+
+    #[test]
+    fn maximum_validation_slots_for_all_peers() {
+        let mut validator =
+            BlockAnnounceValidator::<Block>::new(Box::new(DefaultBlockAnnounceValidator {}));
+
+        for _ in 0..MAX_CONCURRENT_BLOCK_ANNOUNCE_VALIDATIONS {
+            validator.validations.push(
+                futures::future::ready(BlockAnnounceValidationResult::Skip {
+                    peer_id: PeerId::random(),
+                })
+                .boxed(),
+            );
+        }
+
+        let peer_id = PeerId::random();
+        assert!(matches!(
+            validator.allocate_slot_for_block_announce_validation(&peer_id),
+            AllocateSlotForBlockAnnounceValidation::TotalMaximumSlotsReached,
+        ));
+    }
+}
diff --git a/libp2p/sync/src/block_relay_protocol.rs b/libp2p/sync/src/block_relay_protocol.rs
new file mode 100644
index 000000000..3637389de
--- /dev/null
+++ b/libp2p/sync/src/block_relay_protocol.rs
@@ -0,0 +1,75 @@
+// Copyright Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Block relay protocol related definitions.
+
+use futures::channel::oneshot;
+use libp2p::PeerId;
+use sc_network::{
+    request_responses::{ProtocolConfig, RequestFailure},
+    ProtocolName,
+};
+use sc_network_common::sync::message::{BlockData, BlockRequest};
+use sp_runtime::traits::Block as BlockT;
+use std::sync::Arc;
+
+/// The serving side of the block relay protocol. It runs a single instance
+/// of the server task that processes the incoming protocol messages.
+#[async_trait::async_trait]
+pub trait BlockServer<Block: BlockT>: Send {
+    /// Starts the protocol processing.
+    async fn run(&mut self);
+}
+
+/// The client side stub to download blocks from peers. This is a handle
+/// that can be used to initiate concurrent downloads.
+#[async_trait::async_trait]
+pub trait BlockDownloader<Block: BlockT>: Send + Sync {
+    /// Performs the protocol specific sequence to fetch the blocks from the peer.
+    /// Output: if the download succeeds, the response is a `Vec<u8>` which is
+    /// in a format specific to the protocol implementation. The block data
+    /// can be extracted from this response using [`BlockDownloader::block_response_into_blocks`].
+    async fn download_blocks(
+        &self,
+        who: PeerId,
+        request: BlockRequest<Block>,
+    ) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>;
+
+    /// Parses the protocol specific response to retrieve the block data.
+    fn block_response_into_blocks(
+        &self,
+        request: &BlockRequest<Block>,
+        response: Vec<u8>,
+    ) -> Result<Vec<BlockData<Block>>, BlockResponseError>;
+}
+
+/// Errors returned by [`BlockDownloader::block_response_into_blocks`].
+#[derive(Debug)]
+pub enum BlockResponseError {
+    /// Failed to decode the response bytes.
+    DecodeFailed(String),
+
+    /// Failed to extract the blocks from the decoded bytes.
+    ExtractionFailed(String),
+}
+
+/// Block relay specific params for network creation, specified in
+/// ['sc_service::BuildNetworkParams'].
+pub struct BlockRelayParams<Block: BlockT> {
+    pub server: Box<dyn BlockServer<Block>>,
+    pub downloader: Arc<dyn BlockDownloader<Block>>,
+    pub request_response_config: ProtocolConfig,
+}
diff --git a/libp2p/sync/src/block_request_handler.rs b/libp2p/sync/src/block_request_handler.rs
new file mode 100644
index 000000000..bf804d27f
--- /dev/null
+++ b/libp2p/sync/src/block_request_handler.rs
@@ -0,0 +1,651 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Helper for handling (i.e. answering) block requests from a remote peer via the
+//! `crate::request_responses::RequestResponsesBehaviour`.
+
+use crate::{
+    block_relay_protocol::{BlockDownloader, BlockRelayParams, BlockResponseError, BlockServer},
+    schema::v1::{
+        block_request::FromBlock as FromBlockSchema, BlockRequest as BlockRequestSchema,
+        BlockResponse as BlockResponseSchema, BlockResponse, Direction,
+    },
+    service::network::NetworkServiceHandle,
+    LOG_TARGET,
+};
+
+use codec::{Decode, DecodeAll, Encode};
+use futures::{channel::oneshot, stream::StreamExt};
+use libp2p::PeerId;
+use log::debug;
+use prost::Message;
+use sc_client_api::BlockBackend;
+use sc_network::{
+    config::ProtocolId,
+    request_responses::{
+        IfDisconnected, IncomingRequest, OutgoingResponse, ProtocolConfig, RequestFailure,
+    },
+    types::ProtocolName,
+};
+use sc_network_common::sync::message::{BlockAttributes, BlockData, BlockRequest, FromBlock};
+use schnellru::{ByLength, LruMap};
+use sp_blockchain::HeaderBackend;
+use sp_runtime::{
+    generic::BlockId,
+    traits::{Block as BlockT, Header, One, Zero},
+};
+use std::{
+    cmp::min,
+    hash::{Hash, Hasher},
+    sync::Arc,
+    time::Duration,
+};
+
+/// Maximum blocks per response.
+pub(crate) const MAX_BLOCKS_IN_RESPONSE: usize = 128;
+
+const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
+const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+
+    /// Reputation change when a peer sent us the same request multiple times.
+    pub const SAME_REQUEST: Rep = Rep::new_fatal("Same block request multiple times");
+
+    /// Reputation change when a peer sent us the same "small" request multiple times.
+    pub const SAME_SMALL_REQUEST: Rep =
+        Rep::new(-(1 << 10), "same small block request multiple times");
+}
+
+/// Generates a [`ProtocolConfig`] for the block request protocol, refusing incoming requests.
+pub fn generate_protocol_config<Hash: AsRef<[u8]>>(
+    protocol_id: &ProtocolId,
+    genesis_hash: Hash,
+    fork_id: Option<&str>,
+) -> ProtocolConfig {
+    ProtocolConfig {
+        name: generate_protocol_name(genesis_hash, fork_id).into(),
+        fallback_names: std::iter::once(generate_legacy_protocol_name(protocol_id).into())
+            .collect(),
+        max_request_size: 1024 * 1024,
+        max_response_size: 16 * 1024 * 1024,
+        request_timeout: Duration::from_secs(20),
+        inbound_queue: None,
+    }
+}
+
+/// Generate the block protocol name from the genesis hash and fork id.
+fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
+    let genesis_hash = genesis_hash.as_ref();
+    if let Some(fork_id) = fork_id {
+        format!(
+            "/{}/{}/sync/2",
+            array_bytes::bytes2hex("", genesis_hash),
+            fork_id
+        )
+    } else {
+        format!("/{}/sync/2", array_bytes::bytes2hex("", genesis_hash))
+    }
+}
+
+/// Generate the legacy block protocol name from chain specific protocol identifier.
+fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
+    format!("/{}/sync/2", protocol_id.as_ref())
+}
+
+/// The key of [`BlockRequestHandler::seen_requests`].
+#[derive(Eq, PartialEq, Clone)]
+struct SeenRequestsKey<B: BlockT> {
+    peer: PeerId,
+    from: BlockId<B>,
+    max_blocks: usize,
+    direction: Direction,
+    attributes: BlockAttributes,
+    support_multiple_justifications: bool,
+}
+
+#[allow(clippy::derived_hash_with_manual_eq)]
+impl<B: BlockT> Hash for SeenRequestsKey<B> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.peer.hash(state);
+        self.max_blocks.hash(state);
+        self.direction.hash(state);
+        self.attributes.hash(state);
+        self.support_multiple_justifications.hash(state);
+        match self.from {
+            BlockId::Hash(h) => h.hash(state),
+            BlockId::Number(n) => n.hash(state),
+        }
+    }
+}
+
+/// The value of [`BlockRequestHandler::seen_requests`].
+enum SeenRequestsValue {
+    /// First time we have seen the request.
+    First,
+    /// We have fulfilled the request `n` times.
+    Fulfilled(usize),
+}
+
+/// The full block server implementation of [`BlockServer`]. It handles
+/// the incoming block requests from a remote peer.
+pub struct BlockRequestHandler<B: BlockT, Client> {
+    client: Arc<Client>,
+    request_receiver: async_channel::Receiver<IncomingRequest>,
+    /// Maps from request to number of times we have seen this request.
+    ///
+    /// This is used to check if a peer is spamming us with the same request.
+    seen_requests: LruMap<SeenRequestsKey<B>, SeenRequestsValue>,
+}
+
+impl<B, Client> BlockRequestHandler<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
+{
+    /// Create a new [`BlockRequestHandler`].
+    #[allow(clippy::new_ret_no_self)]
+    pub fn new(
+        network: NetworkServiceHandle,
+        protocol_id: &ProtocolId,
+        fork_id: Option<&str>,
+        client: Arc<Client>,
+        num_peer_hint: usize,
+    ) -> BlockRelayParams<B> {
+        // Reserve enough request slots for one request per peer when we are at the maximum
+        // number of peers.
+        let capacity = std::cmp::max(num_peer_hint, 1);
+        let (tx, request_receiver) = async_channel::bounded(capacity);
+
+        let mut protocol_config = generate_protocol_config(
+            protocol_id,
+            client
+                .block_hash(0u32.into())
+                .ok()
+                .flatten()
+                .expect("Genesis block exists; qed"),
+            fork_id,
+        );
+        protocol_config.inbound_queue = Some(tx);
+
+        let capacity = ByLength::new(num_peer_hint.max(1) as u32 * 2);
+        let seen_requests = LruMap::new(capacity);
+
+        BlockRelayParams {
+            server: Box::new(Self {
+                client,
+                request_receiver,
+                seen_requests,
+            }),
+            downloader: Arc::new(FullBlockDownloader::new(
+                protocol_config.name.clone(),
+                network,
+            )),
+            request_response_config: protocol_config,
+        }
+    }
+
+    /// Run [`BlockRequestHandler`].
+    async fn process_requests(&mut self) {
+        while let Some(request) = self.request_receiver.next().await {
+            let IncomingRequest {
+                peer,
+                payload,
+                pending_response,
+            } = request;
+
+            match self.handle_request(payload, pending_response, &peer) {
+                Ok(()) => debug!(target: LOG_TARGET, "Handled block request from {}.", peer),
+                Err(e) => debug!(
+                    target: LOG_TARGET,
+                    "Failed to handle block request from {}: {}", peer, e,
+                ),
+            }
+        }
+    }
+
+    fn handle_request(
+        &mut self,
+        payload: Vec<u8>,
+        pending_response: oneshot::Sender<OutgoingResponse>,
+        peer: &PeerId,
+    ) -> Result<(), HandleRequestError> {
+        let request = crate::schema::v1::BlockRequest::decode(&payload[..])?;
+
+        let from_block_id = match request
+            .from_block
+            .ok_or(HandleRequestError::MissingFromField)?
+        {
+            FromBlockSchema::Hash(ref h) => {
+                let h = Decode::decode(&mut h.as_ref())?;
+                BlockId::<B>::Hash(h)
+            }
+            FromBlockSchema::Number(ref n) => {
+                let n = Decode::decode(&mut n.as_ref())?;
+                BlockId::<B>::Number(n)
+            }
+        };
+
+        let max_blocks = if request.max_blocks == 0 {
+            MAX_BLOCKS_IN_RESPONSE
+        } else {
+            min(request.max_blocks as usize, MAX_BLOCKS_IN_RESPONSE)
+        };
+
+        let direction =
+            i32::try_into(request.direction).map_err(|_| HandleRequestError::ParseDirection)?;
+
+        let attributes = BlockAttributes::from_be_u32(request.fields)?;
+
+        let support_multiple_justifications = request.support_multiple_justifications;
+
+        let key = SeenRequestsKey {
+            peer: *peer,
+            max_blocks,
+            direction,
+            from: from_block_id,
+            attributes,
+            support_multiple_justifications,
+        };
+
+        let mut reputation_change = None;
+
+        let small_request = attributes
+            .difference(BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION)
+            .is_empty();
+
+        match self.seen_requests.get(&key) {
+            Some(SeenRequestsValue::First) => {}
+            Some(SeenRequestsValue::Fulfilled(ref mut requests)) => {
+                *requests = requests.saturating_add(1);
+
+                if *requests > MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
+                    reputation_change = Some(if small_request {
+                        rep::SAME_SMALL_REQUEST
+                    } else {
+                        rep::SAME_REQUEST
+                    });
+                }
+            }
+            None => {
+                self.seen_requests
+                    .insert(key.clone(), SeenRequestsValue::First);
+            }
+        }
+
+        debug!(
+            target: LOG_TARGET,
+            "Handling block request from {peer}: Starting at `{from_block_id:?}` with \
+            maximum blocks of `{max_blocks}`, reputation_change: `{reputation_change:?}`, \
+            small_request `{small_request:?}`, direction `{direction:?}` and \
+            attributes `{attributes:?}`.",
+        );
+
+        let maybe_block_response = if reputation_change.is_none() || small_request {
+            let block_response = self.get_block_response(
+                attributes,
+                from_block_id,
+                direction,
+                max_blocks,
+                support_multiple_justifications,
+            )?;
+
+            // If any of the blocks contains any data, we can consider it as successful request.
+            if block_response
+                .blocks
+                .iter()
+                .any(|b| !b.header.is_empty() || !b.body.is_empty() || b.is_empty_justification)
+            {
+                if let Some(value) = self.seen_requests.get(&key) {
+                    // If this is the first time we have processed this request, we need to change
+                    // it to `Fulfilled`.
+                    if let SeenRequestsValue::First = value {
+                        *value = SeenRequestsValue::Fulfilled(1);
+                    }
+                }
+            }
+
+            Some(block_response)
+        } else {
+            None
+        };
+
+        debug!(
+            target: LOG_TARGET,
+            "Sending result of block request from {peer} starting at `{from_block_id:?}`: \
+            blocks: {:?}, data: {:?}",
+            maybe_block_response.as_ref().map(|res| res.blocks.len()),
+            maybe_block_response.as_ref().map(|res| res.encoded_len()),
+        );
+
+        let result = if let Some(block_response) = maybe_block_response {
+            let mut data = Vec::with_capacity(block_response.encoded_len());
+            block_response.encode(&mut data)?;
+            Ok(data)
+        } else {
+            Err(())
+        };
+
+        pending_response
+            .send(OutgoingResponse {
+                result,
+                reputation_changes: reputation_change.into_iter().collect(),
+                sent_feedback: None,
+            })
+            .map_err(|_| HandleRequestError::SendResponse)
+    }
+
+    fn get_block_response(
+        &self,
+        attributes: BlockAttributes,
+        mut block_id: BlockId<B>,
+        direction: Direction,
+        max_blocks: usize,
+        support_multiple_justifications: bool,
+    ) -> Result<BlockResponse, HandleRequestError> {
+        let get_header = attributes.contains(BlockAttributes::HEADER);
+        let get_body = attributes.contains(BlockAttributes::BODY);
+        let get_indexed_body = attributes.contains(BlockAttributes::INDEXED_BODY);
+        let get_justification = attributes.contains(BlockAttributes::JUSTIFICATION);
+
+        let mut blocks = Vec::new();
+
+        let mut total_size: usize = 0;
+
+        let client_header_from_block_id =
+            |block_id: BlockId<B>| -> Result<Option<B::Header>, HandleRequestError> {
+                if let Some(hash) = self.client.block_hash_from_id(&block_id)? {
+                    return self.client.header(hash).map_err(Into::into);
+                }
+                Ok(None)
+            };
+
+        while let Some(header) = client_header_from_block_id(block_id).unwrap_or_default() {
+            let number = *header.number();
+            let hash = header.hash();
+            let parent_hash = *header.parent_hash();
+            let justifications = if get_justification {
+                self.client.justifications(hash)?
+            } else {
+                None
+            };
+
+            let (justifications, justification, is_empty_justification) =
+                if support_multiple_justifications {
+                    let justifications = match justifications {
+                        Some(v) => v.encode(),
+                        None => Vec::new(),
+                    };
+                    (justifications, Vec::new(), false)
+                } else {
+                    // For now we keep compatibility by selecting precisely the GRANDPA one, and not
+                    // just the first one. When sending we could have just taken the first one,
+                    // since we don't expect there to be any other kind currently, but when
+                    // receiving we need to add the engine ID tag.
+                    // The ID tag is hardcoded here to avoid depending on the GRANDPA crate, and
+                    // will be removed once we remove the backwards compatibility.
+                    // See: https://github.com/paritytech/substrate/issues/8172
+                    let justification =
+                        justifications.and_then(|just| just.into_justification(*b"FRNK"));
+
+                    let is_empty_justification = justification
+                        .as_ref()
+                        .map(|j| j.is_empty())
+                        .unwrap_or(false);
+
+                    let justification = justification.unwrap_or_default();
+
+                    (Vec::new(), justification, is_empty_justification)
+                };
+
+            let body = if get_body {
+                match self.client.block_body(hash)? {
+                    Some(mut extrinsics) => extrinsics
+                        .iter_mut()
+                        .map(|extrinsic| extrinsic.encode())
+                        .collect(),
+                    None => {
+                        log::trace!(target: LOG_TARGET, "Missing data for block request.");
+                        break;
+                    }
+                }
+            } else {
+                Vec::new()
+            };
+
+            let indexed_body = if get_indexed_body {
+                match self.client.block_indexed_body(hash)? {
+                    Some(transactions) => transactions,
+                    None => {
+                        log::trace!(
+                            target: LOG_TARGET,
+                            "Missing indexed block data for block request."
+                        );
+                        // If the indexed body is missing we still continue returning headers.
+                        // Ideally `None` should distinguish a missing body from the empty body,
+                        // but the current protobuf based protocol does not allow it.
+                        Vec::new()
+                    }
+                }
+            } else {
+                Vec::new()
+            };
+
+            let block_data = crate::schema::v1::BlockData {
+                hash: hash.encode(),
+                header: if get_header {
+                    header.encode()
+                } else {
+                    Vec::new()
+                },
+                body,
+                receipt: Vec::new(),
+                message_queue: Vec::new(),
+                justification,
+                is_empty_justification,
+                justifications,
+                indexed_body,
+            };
+
+            let new_total_size = total_size
+                + block_data.body.iter().map(|ex| ex.len()).sum::<usize>()
+                + block_data
+                    .indexed_body
+                    .iter()
+                    .map(|ex| ex.len())
+                    .sum::<usize>();
+
+            // Send at least one block, but make sure to not exceed the limit.
+            if !blocks.is_empty() && new_total_size > MAX_BODY_BYTES {
+                break;
+            }
+
+            total_size = new_total_size;
+
+            blocks.push(block_data);
+
+            if blocks.len() >= max_blocks {
+                break;
+            }
+
+            match direction {
+                Direction::Ascending => block_id = BlockId::Number(number + One::one()),
+                Direction::Descending => {
+                    if number.is_zero() {
+                        break;
+                    }
+                    block_id = BlockId::Hash(parent_hash)
+                }
+            }
+        }
+
+        Ok(BlockResponse { blocks })
+    }
+}
+
+#[async_trait::async_trait]
+impl<B, Client> BlockServer<B> for BlockRequestHandler<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B> + BlockBackend<B> + Send + Sync + 'static,
+{
+    async fn run(&mut self) {
+        self.process_requests().await;
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum HandleRequestError {
+    #[error("Failed to decode request: {0}.")]
+    DecodeProto(#[from] prost::DecodeError),
+    #[error("Failed to encode response: {0}.")]
+    EncodeProto(#[from] prost::EncodeError),
+    #[error("Failed to decode block hash: {0}.")]
+    DecodeScale(#[from] codec::Error),
+    #[error("Missing `BlockRequest::from_block` field.")]
+    MissingFromField,
+    #[error("Failed to parse BlockRequest::direction.")]
+    ParseDirection,
+    #[error(transparent)]
+    Client(#[from] sp_blockchain::Error),
+    #[error("Failed to send response.")]
+    SendResponse,
+}
+
+/// The full block downloader implementation of [`BlockDownloader].
+pub struct FullBlockDownloader {
+    protocol_name: ProtocolName,
+    network: NetworkServiceHandle,
+}
+
+impl FullBlockDownloader {
+    fn new(protocol_name: ProtocolName, network: NetworkServiceHandle) -> Self {
+        Self {
+            protocol_name,
+            network,
+        }
+    }
+
+    /// Extracts the blocks from the response schema.
+    fn blocks_from_schema<B: BlockT>(
+        &self,
+        request: &BlockRequest<B>,
+        response: BlockResponseSchema,
+    ) -> Result<Vec<BlockData<B>>, String> {
+        response
+            .blocks
+            .into_iter()
+            .map(|block_data| {
+                Ok(BlockData::<B> {
+                    hash: Decode::decode(&mut block_data.hash.as_ref())?,
+                    header: if !block_data.header.is_empty() {
+                        Some(Decode::decode(&mut block_data.header.as_ref())?)
+                    } else {
+                        None
+                    },
+                    body: if request.fields.contains(BlockAttributes::BODY) {
+                        Some(
+                            block_data
+                                .body
+                                .iter()
+                                .map(|body| Decode::decode(&mut body.as_ref()))
+                                .collect::<Result<Vec<_>, _>>()?,
+                        )
+                    } else {
+                        None
+                    },
+                    indexed_body: if request.fields.contains(BlockAttributes::INDEXED_BODY) {
+                        Some(block_data.indexed_body)
+                    } else {
+                        None
+                    },
+                    receipt: if !block_data.receipt.is_empty() {
+                        Some(block_data.receipt)
+                    } else {
+                        None
+                    },
+                    message_queue: if !block_data.message_queue.is_empty() {
+                        Some(block_data.message_queue)
+                    } else {
+                        None
+                    },
+                    justification: if !block_data.justification.is_empty() {
+                        Some(block_data.justification)
+                    } else if block_data.is_empty_justification {
+                        Some(Vec::new())
+                    } else {
+                        None
+                    },
+                    justifications: if !block_data.justifications.is_empty() {
+                        Some(DecodeAll::decode_all(
+                            &mut block_data.justifications.as_ref(),
+                        )?)
+                    } else {
+                        None
+                    },
+                })
+            })
+            .collect::<Result<_, _>>()
+            .map_err(|error: codec::Error| error.to_string())
+    }
+}
+
+#[async_trait::async_trait]
+impl<B: BlockT> BlockDownloader<B> for FullBlockDownloader {
+    async fn download_blocks(
+        &self,
+        who: PeerId,
+        request: BlockRequest<B>,
+    ) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled> {
+        // Build the request protobuf.
+        let bytes = BlockRequestSchema {
+            fields: request.fields.to_be_u32(),
+            from_block: match request.from {
+                FromBlock::Hash(h) => Some(FromBlockSchema::Hash(h.encode())),
+                FromBlock::Number(n) => Some(FromBlockSchema::Number(n.encode())),
+            },
+            direction: request.direction as i32,
+            max_blocks: request.max.unwrap_or(0),
+            support_multiple_justifications: true,
+        }
+        .encode_to_vec();
+
+        let (tx, rx) = oneshot::channel();
+        self.network.start_request(
+            who,
+            self.protocol_name.clone(),
+            bytes,
+            tx,
+            IfDisconnected::ImmediateError,
+        );
+        rx.await
+    }
+
+    fn block_response_into_blocks(
+        &self,
+        request: &BlockRequest<B>,
+        response: Vec<u8>,
+    ) -> Result<Vec<BlockData<B>>, BlockResponseError> {
+        // Decode the response protobuf
+        let response_schema = BlockResponseSchema::decode(response.as_slice())
+            .map_err(|error| BlockResponseError::DecodeFailed(error.to_string()))?;
+
+        // Extract the block data from the protobuf
+        self.blocks_from_schema::<B>(request, response_schema)
+            .map_err(|error| BlockResponseError::ExtractionFailed(error.to_string()))
+    }
+}
diff --git a/libp2p/sync/src/blocks.rs b/libp2p/sync/src/blocks.rs
new file mode 100644
index 000000000..bf33ad6e1
--- /dev/null
+++ b/libp2p/sync/src/blocks.rs
@@ -0,0 +1,717 @@
+// 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::LOG_TARGET;
+use libp2p::PeerId;
+use log::trace;
+use sc_network_common::sync::message;
+use sp_runtime::traits::{Block as BlockT, NumberFor, One};
+use std::{
+    cmp,
+    collections::{BTreeMap, HashMap},
+    ops::Range,
+};
+
+/// Block data with origin.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BlockData<B: BlockT> {
+    /// The Block Message from the wire
+    pub block: message::BlockData<B>,
+    /// The peer, we received this from
+    pub origin: Option<PeerId>,
+}
+
+#[derive(Debug)]
+enum BlockRangeState<B: BlockT> {
+    Downloading { len: NumberFor<B>, downloading: u32 },
+    Complete(Vec<BlockData<B>>),
+    Queued { len: NumberFor<B> },
+}
+
+impl<B: BlockT> BlockRangeState<B> {
+    pub fn len(&self) -> NumberFor<B> {
+        match *self {
+            Self::Downloading { len, .. } => len,
+            Self::Complete(ref blocks) => (blocks.len() as u32).into(),
+            Self::Queued { len } => len,
+        }
+    }
+}
+
+/// A collection of blocks being downloaded.
+#[derive(Default)]
+pub struct BlockCollection<B: BlockT> {
+    /// Downloaded blocks.
+    blocks: BTreeMap<NumberFor<B>, BlockRangeState<B>>,
+    peer_requests: HashMap<PeerId, NumberFor<B>>,
+    /// Block ranges downloaded and queued for import.
+    /// Maps start_hash => (start_num, end_num).
+    queued_blocks: HashMap<B::Hash, (NumberFor<B>, NumberFor<B>)>,
+}
+
+impl<B: BlockT> BlockCollection<B> {
+    /// Create a new instance.
+    pub fn new() -> Self {
+        Self {
+            blocks: BTreeMap::new(),
+            peer_requests: HashMap::new(),
+            queued_blocks: HashMap::new(),
+        }
+    }
+
+    /// Clear everything.
+    pub fn clear(&mut self) {
+        self.blocks.clear();
+        self.peer_requests.clear();
+    }
+
+    /// Insert a set of blocks into collection.
+    pub fn insert(&mut self, start: NumberFor<B>, blocks: Vec<message::BlockData<B>>, who: PeerId) {
+        if blocks.is_empty() {
+            return;
+        }
+
+        match self.blocks.get(&start) {
+            Some(&BlockRangeState::Downloading { .. }) => {
+                trace!(target: LOG_TARGET, "Inserting block data still marked as being downloaded: {}", start);
+            }
+            Some(BlockRangeState::Complete(existing)) if existing.len() >= blocks.len() => {
+                trace!(target: LOG_TARGET, "Ignored block data already downloaded: {}", start);
+                return;
+            }
+            _ => (),
+        }
+
+        self.blocks.insert(
+            start,
+            BlockRangeState::Complete(
+                blocks
+                    .into_iter()
+                    .map(|b| BlockData {
+                        origin: Some(who),
+                        block: b,
+                    })
+                    .collect(),
+            ),
+        );
+    }
+
+    /// Returns a set of block hashes that require a header download. The returned set is marked as
+    /// being downloaded.
+    pub fn needed_blocks(
+        &mut self,
+        who: PeerId,
+        count: u32,
+        peer_best: NumberFor<B>,
+        common: NumberFor<B>,
+        max_parallel: u32,
+        max_ahead: u32,
+    ) -> Option<Range<NumberFor<B>>> {
+        if peer_best <= common {
+            // Bail out early
+            return None;
+        }
+        // First block number that we need to download
+        let first_different = common + <NumberFor<B>>::one();
+        let count = count.into();
+        let (mut range, downloading) = {
+            // Iterate through the ranges in `self.blocks` looking for a range to download
+            let mut downloading_iter = self.blocks.iter().peekable();
+            let mut prev: Option<(&NumberFor<B>, &BlockRangeState<B>)> = None;
+            loop {
+                let next = downloading_iter.next();
+                break match (prev, next) {
+                    // If we are already downloading this range, request it from `max_parallel`
+                    // peers (`max_parallel = 5` by default).
+                    // Do not request already downloading range from peers with common number above
+                    // the range start.
+                    (
+                        Some((
+                            start,
+                            &BlockRangeState::Downloading {
+                                ref len,
+                                downloading,
+                            },
+                        )),
+                        _,
+                    ) if downloading < max_parallel && *start >= first_different => {
+                        (*start..*start + *len, downloading)
+                    }
+                    // If there is a gap between ranges requested, download this gap unless the peer
+                    // has common number above the gap start
+                    (Some((start, r)), Some((next_start, _)))
+                        if *start + r.len() < *next_start
+                            && *start + r.len() >= first_different =>
+                    {
+                        (
+                            *start + r.len()..cmp::min(*next_start, *start + r.len() + count),
+                            0,
+                        )
+                    }
+                    // Download `count` blocks after the last range requested unless the peer
+                    // has common number above this new range
+                    (Some((start, r)), None) if *start + r.len() >= first_different => {
+                        (*start + r.len()..*start + r.len() + count, 0)
+                    }
+                    // If there are no ranges currently requested, download `count` blocks after
+                    // `common` number
+                    (None, None) => (first_different..first_different + count, 0),
+                    // If the first range starts above `common + 1`, download the gap at the start
+                    (None, Some((start, _))) if *start > first_different => (
+                        first_different..cmp::min(first_different + count, *start),
+                        0,
+                    ),
+                    // Move on to the next range pair
+                    _ => {
+                        prev = next;
+                        continue;
+                    }
+                };
+            }
+        };
+        // crop to peers best
+        if range.start > peer_best {
+            trace!(target: LOG_TARGET, "Out of range for peer {} ({} vs {})", who, range.start, peer_best);
+            return None;
+        }
+        range.end = cmp::min(peer_best + One::one(), range.end);
+
+        if self
+            .blocks
+            .iter()
+            .next()
+            .map_or(false, |(n, _)| range.start > *n + max_ahead.into())
+        {
+            trace!(target: LOG_TARGET, "Too far ahead for peer {} ({})", who, range.start);
+            return None;
+        }
+
+        self.peer_requests.insert(who, range.start);
+        self.blocks.insert(
+            range.start,
+            BlockRangeState::Downloading {
+                len: range.end - range.start,
+                downloading: downloading + 1,
+            },
+        );
+        if range.end <= range.start {
+            panic!(
+                "Empty range {:?}, count={}, peer_best={}, common={}, blocks={:?}",
+                range, count, peer_best, common, self.blocks
+            );
+        }
+        Some(range)
+    }
+
+    /// Get a valid chain of blocks ordered in descending order and ready for importing into
+    /// the blockchain.
+    /// `from` is the maximum block number for the start of the range that we are interested in.
+    /// The function will return empty Vec if the first block ready is higher than `from`.
+    /// For each returned block hash `clear_queued` must be called at some later stage.
+    pub fn ready_blocks(&mut self, from: NumberFor<B>) -> Vec<BlockData<B>> {
+        let mut ready = Vec::new();
+
+        let mut prev = from;
+        for (&start, range_data) in &mut self.blocks {
+            if start > prev {
+                break;
+            }
+            let len = match range_data {
+                BlockRangeState::Complete(blocks) => {
+                    let len = (blocks.len() as u32).into();
+                    prev = start + len;
+                    if let Some(BlockData { block, .. }) = blocks.first() {
+                        self.queued_blocks
+                            .insert(block.hash, (start, start + (blocks.len() as u32).into()));
+                    }
+                    // Remove all elements from `blocks` and add them to `ready`
+                    ready.append(blocks);
+                    len
+                }
+                BlockRangeState::Queued { .. } => continue,
+                _ => break,
+            };
+            *range_data = BlockRangeState::Queued { len };
+        }
+        trace!(target: LOG_TARGET, "{} blocks ready for import", ready.len());
+        ready
+    }
+
+    pub fn clear_queued(&mut self, hash: &B::Hash) {
+        if let Some((from, to)) = self.queued_blocks.remove(hash) {
+            let mut block_num = from;
+            while block_num < to {
+                self.blocks.remove(&block_num);
+                block_num += One::one();
+            }
+            trace!(target: LOG_TARGET, "Cleared blocks from {:?} to {:?}", from, to);
+        }
+    }
+
+    pub fn clear_peer_download(&mut self, who: &PeerId) {
+        if let Some(start) = self.peer_requests.remove(who) {
+            let remove = match self.blocks.get_mut(&start) {
+                Some(&mut BlockRangeState::Downloading {
+                    ref mut downloading,
+                    ..
+                }) if *downloading > 1 => {
+                    *downloading -= 1;
+                    false
+                }
+                Some(&mut BlockRangeState::Downloading { .. }) => true,
+                _ => false,
+            };
+            if remove {
+                self.blocks.remove(&start);
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{BlockCollection, BlockData, BlockRangeState};
+    use libp2p::PeerId;
+    use sc_network_common::sync::message;
+    use sp_core::H256;
+    use sp_runtime::testing::{Block as RawBlock, ExtrinsicWrapper};
+
+    type Block = RawBlock<ExtrinsicWrapper<u64>>;
+
+    fn is_empty(bc: &BlockCollection<Block>) -> bool {
+        bc.blocks.is_empty() && bc.peer_requests.is_empty()
+    }
+
+    fn generate_blocks(n: usize) -> Vec<message::BlockData<Block>> {
+        (0..n)
+            .map(|_| message::generic::BlockData {
+                hash: H256::random(),
+                header: None,
+                body: None,
+                indexed_body: None,
+                message_queue: None,
+                receipt: None,
+                justification: None,
+                justifications: None,
+            })
+            .collect()
+    }
+
+    #[test]
+    fn create_clear() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+        bc.insert(1, generate_blocks(100), PeerId::random());
+        assert!(!is_empty(&bc));
+        bc.clear();
+        assert!(is_empty(&bc));
+    }
+
+    #[test]
+    fn insert_blocks() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+        let peer0 = PeerId::random();
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+
+        let blocks = generate_blocks(150);
+        assert_eq!(bc.needed_blocks(peer0, 40, 150, 0, 1, 200), Some(1..41));
+        assert_eq!(bc.needed_blocks(peer1, 40, 150, 0, 1, 200), Some(41..81));
+        assert_eq!(bc.needed_blocks(peer2, 40, 150, 0, 1, 200), Some(81..121));
+
+        bc.clear_peer_download(&peer1);
+        bc.insert(41, blocks[41..81].to_vec(), peer1);
+        assert_eq!(bc.ready_blocks(1), vec![]);
+        assert_eq!(bc.needed_blocks(peer1, 40, 150, 0, 1, 200), Some(121..151));
+        bc.clear_peer_download(&peer0);
+        bc.insert(1, blocks[1..11].to_vec(), peer0);
+
+        assert_eq!(bc.needed_blocks(peer0, 40, 150, 0, 1, 200), Some(11..41));
+        assert_eq!(
+            bc.ready_blocks(1),
+            blocks[1..11]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer0)
+                })
+                .collect::<Vec<_>>()
+        );
+
+        bc.clear_peer_download(&peer0);
+        bc.insert(11, blocks[11..41].to_vec(), peer0);
+
+        let ready = bc.ready_blocks(12);
+        assert_eq!(
+            ready[..30],
+            blocks[11..41]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer0)
+                })
+                .collect::<Vec<_>>()[..]
+        );
+        assert_eq!(
+            ready[30..],
+            blocks[41..81]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer1)
+                })
+                .collect::<Vec<_>>()[..]
+        );
+
+        bc.clear_peer_download(&peer2);
+        assert_eq!(bc.needed_blocks(peer2, 40, 150, 80, 1, 200), Some(81..121));
+        bc.clear_peer_download(&peer2);
+        bc.insert(81, blocks[81..121].to_vec(), peer2);
+        bc.clear_peer_download(&peer1);
+        bc.insert(121, blocks[121..150].to_vec(), peer1);
+
+        assert_eq!(bc.ready_blocks(80), vec![]);
+        let ready = bc.ready_blocks(81);
+        assert_eq!(
+            ready[..40],
+            blocks[81..121]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer2)
+                })
+                .collect::<Vec<_>>()[..]
+        );
+        assert_eq!(
+            ready[40..],
+            blocks[121..150]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer1)
+                })
+                .collect::<Vec<_>>()[..]
+        );
+    }
+
+    #[test]
+    fn large_gap() {
+        let mut bc: BlockCollection<Block> = BlockCollection::new();
+        bc.blocks.insert(
+            100,
+            BlockRangeState::Downloading {
+                len: 128,
+                downloading: 1,
+            },
+        );
+        let blocks = generate_blocks(10)
+            .into_iter()
+            .map(|b| BlockData {
+                block: b,
+                origin: None,
+            })
+            .collect();
+        bc.blocks.insert(114305, BlockRangeState::Complete(blocks));
+
+        let peer0 = PeerId::random();
+        assert_eq!(bc.needed_blocks(peer0, 128, 10000, 0, 1, 200), Some(1..100));
+        assert_eq!(bc.needed_blocks(peer0, 128, 10000, 0, 1, 200), None); // too far ahead
+        assert_eq!(
+            bc.needed_blocks(peer0, 128, 10000, 0, 1, 200000),
+            Some(100 + 128..100 + 128 + 128)
+        );
+    }
+
+    #[test]
+    fn no_duplicate_requests_on_fork() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+        let peer = PeerId::random();
+
+        let blocks = generate_blocks(10);
+
+        // count = 5, peer_best = 50, common = 39, max_parallel = 0, max_ahead = 200
+        assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(40..45));
+
+        // got a response on the request for `40..45`
+        bc.clear_peer_download(&peer);
+        bc.insert(40, blocks[..5].to_vec(), peer);
+
+        // our "node" started on a fork, with its current best = 47, which is > common
+        let ready = bc.ready_blocks(48);
+        assert_eq!(
+            ready,
+            blocks[..5]
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer)
+                })
+                .collect::<Vec<_>>()
+        );
+
+        assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(45..50));
+    }
+
+    #[test]
+    fn clear_queued_subsequent_ranges() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+        let peer = PeerId::random();
+
+        let blocks = generate_blocks(10);
+
+        // Request 2 ranges
+        assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(40..45));
+        assert_eq!(bc.needed_blocks(peer, 5, 50, 39, 0, 200), Some(45..50));
+
+        // got a response on the request for `40..50`
+        bc.clear_peer_download(&peer);
+        bc.insert(40, blocks.to_vec(), peer);
+
+        // request any blocks starting from 1000 or lower.
+        let ready = bc.ready_blocks(1000);
+        assert_eq!(
+            ready,
+            blocks
+                .iter()
+                .map(|b| BlockData {
+                    block: b.clone(),
+                    origin: Some(peer)
+                })
+                .collect::<Vec<_>>()
+        );
+
+        bc.clear_queued(&blocks[0].hash);
+        assert!(bc.blocks.is_empty());
+        assert!(bc.queued_blocks.is_empty());
+    }
+
+    #[test]
+    fn downloaded_range_is_requested_from_max_parallel_peers() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        // identical ranges requested from 2 peers
+        let max_parallel = 2;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let peer3 = PeerId::random();
+
+        // common for all peers
+        let best = 100;
+        let common = 10;
+
+        assert_eq!(
+            bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
+            Some(11..16)
+        );
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(11..16)
+        );
+        assert_eq!(
+            bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
+            Some(16..21)
+        );
+    }
+    #[test]
+    fn downloaded_range_not_requested_from_peers_with_higher_common_number() {
+        // A peer connects with a common number falling behind our best number
+        // (either a fork or lagging behind).
+        // We request a range from this peer starting at its common number + 1.
+        // Even though we have less than `max_parallel` downloads, we do not request
+        // this range from peers with a common number above the start of this range.
+
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        let max_parallel = 2;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer1_best = 20;
+        let peer1_common = 10;
+
+        // `peer2` has first different above the start of the range downloaded from `peer1`
+        let peer2 = PeerId::random();
+        let peer2_best = 20;
+        let peer2_common = 11; // first_different = 12
+
+        assert_eq!(
+            bc.needed_blocks(
+                peer1,
+                count,
+                peer1_best,
+                peer1_common,
+                max_parallel,
+                max_ahead
+            ),
+            Some(11..16),
+        );
+        assert_eq!(
+            bc.needed_blocks(
+                peer2,
+                count,
+                peer2_best,
+                peer2_common,
+                max_parallel,
+                max_ahead
+            ),
+            Some(16..21),
+        );
+    }
+
+    #[test]
+    fn gap_above_common_number_requested() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        let best = 30;
+        // We need at least 3 ranges requested to have a gap, so to minimize the number of peers
+        // set `max_parallel = 1`
+        let max_parallel = 1;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let peer3 = PeerId::random();
+
+        let common = 10;
+        assert_eq!(
+            bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
+            Some(11..16),
+        );
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(16..21),
+        );
+        assert_eq!(
+            bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
+            Some(21..26),
+        );
+
+        // For some reason there is now a gap at 16..21. We just disconnect `peer2`, but it might
+        // also happen that 16..21 received first and got imported if our best is actually >= 15.
+        bc.clear_peer_download(&peer2);
+
+        // Some peer connects with common number below the gap. The gap is requested from it.
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(16..21),
+        );
+    }
+
+    #[test]
+    fn gap_below_common_number_not_requested() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        let best = 30;
+        // We need at least 3 ranges requested to have a gap, so to minimize the number of peers
+        // set `max_parallel = 1`
+        let max_parallel = 1;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+        let peer3 = PeerId::random();
+
+        let common = 10;
+        assert_eq!(
+            bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
+            Some(11..16),
+        );
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(16..21),
+        );
+        assert_eq!(
+            bc.needed_blocks(peer3, count, best, common, max_parallel, max_ahead),
+            Some(21..26),
+        );
+
+        // For some reason there is now a gap at 16..21. We just disconnect `peer2`, but it might
+        // also happen that 16..21 received first and got imported if our best is actually >= 15.
+        bc.clear_peer_download(&peer2);
+
+        // Some peer connects with common number above the gap. The gap is not requested from it.
+        let common = 23;
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(26..31), // not 16..21
+        );
+    }
+
+    #[test]
+    fn range_at_the_end_above_common_number_requested() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        let best = 30;
+        let max_parallel = 1;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+
+        let common = 10;
+        assert_eq!(
+            bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
+            Some(11..16),
+        );
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(16..21),
+        );
+    }
+
+    #[test]
+    fn range_at_the_end_below_common_number_not_requested() {
+        let mut bc = BlockCollection::new();
+        assert!(is_empty(&bc));
+
+        let count = 5;
+        let best = 30;
+        let max_parallel = 1;
+        let max_ahead = 200;
+
+        let peer1 = PeerId::random();
+        let peer2 = PeerId::random();
+
+        let common = 10;
+        assert_eq!(
+            bc.needed_blocks(peer1, count, best, common, max_parallel, max_ahead),
+            Some(11..16),
+        );
+
+        let common = 20;
+        assert_eq!(
+            bc.needed_blocks(peer2, count, best, common, max_parallel, max_ahead),
+            Some(21..26), // not 16..21
+        );
+    }
+}
diff --git a/libp2p/sync/src/engine.rs b/libp2p/sync/src/engine.rs
new file mode 100644
index 000000000..17c43e33b
--- /dev/null
+++ b/libp2p/sync/src/engine.rs
@@ -0,0 +1,1506 @@
+// 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/>.
+
+//! `SyncingEngine` is the actor responsible for syncing Substrate chain
+//! to tip and keep the blockchain up to date with network updates.
+
+use crate::{
+    block_announce_validator::{
+        BlockAnnounceValidationResult, BlockAnnounceValidator as BlockAnnounceValidatorStream,
+    },
+    block_relay_protocol::{BlockDownloader, BlockResponseError},
+    block_request_handler::MAX_BLOCKS_IN_RESPONSE,
+    pending_responses::{PendingResponses, ResponseEvent},
+    schema::v1::{StateRequest, StateResponse},
+    service::{
+        self,
+        syncing_service::{SyncingService, ToServiceCommand},
+    },
+    strategy::{
+        warp::{EncodedProof, WarpProofRequest, WarpSyncParams},
+        SyncingAction, SyncingConfig, SyncingStrategy,
+    },
+    types::{
+        BadPeer, ExtendedPeerInfo, OpaqueStateRequest, OpaqueStateResponse, PeerRequest, SyncEvent,
+    },
+    LOG_TARGET,
+};
+
+use codec::{Decode, DecodeAll, Encode};
+use futures::{
+    channel::oneshot,
+    future::{BoxFuture, Fuse},
+    FutureExt, StreamExt,
+};
+use libp2p::{request_response::OutboundFailure, PeerId};
+use log::{debug, error, trace};
+use prometheus_endpoint::{
+    register, Counter, Gauge, MetricSource, Opts, PrometheusError, Registry, SourcedGauge, U64,
+};
+use prost::Message;
+use schnellru::{ByLength, LruMap};
+use tokio::time::{Interval, MissedTickBehavior};
+
+use sc_client_api::{BlockBackend, HeaderBackend, ProofProvider};
+use sc_consensus::{import_queue::ImportQueueService, IncomingBlock};
+use sc_network::{
+    config::{
+        FullNetworkConfiguration, NonDefaultSetConfig, NonReservedPeerMode, NotificationHandshake,
+        ProtocolId, SetConfig,
+    },
+    peer_store::{PeerStoreHandle, PeerStoreProvider},
+    request_responses::{IfDisconnected, RequestFailure},
+    service::traits::{Direction, NotificationEvent, ValidationResult},
+    types::ProtocolName,
+    utils::LruHashSet,
+    NotificationService, ReputationChange,
+};
+use sc_network_common::{
+    role::Roles,
+    sync::message::{BlockAnnounce, BlockAnnouncesHandshake, BlockRequest, BlockState},
+};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+use sp_blockchain::{Error as ClientError, HeaderMetadata};
+use sp_consensus::{block_validation::BlockAnnounceValidator, BlockOrigin};
+use sp_runtime::{
+    traits::{Block as BlockT, Header, NumberFor, Zero},
+    Justifications,
+};
+
+use std::{
+    collections::{HashMap, HashSet},
+    iter,
+    num::NonZeroUsize,
+    sync::{
+        atomic::{AtomicBool, AtomicUsize, Ordering},
+        Arc,
+    },
+    time::{Duration, Instant},
+};
+
+/// Interval at which we perform time based maintenance
+const TICK_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(1100);
+
+/// Maximum number of known block hashes to keep for a peer.
+const MAX_KNOWN_BLOCKS: usize = 1024; // ~32kb per peer + LruHashSet overhead
+
+/// If the block announces stream to peer has been inactive for 30 seconds meaning local node
+/// has not sent or received block announcements to/from the peer, report the node for inactivity,
+/// disconnect it and attempt to establish connection to some other peer.
+const INACTIVITY_EVICT_THRESHOLD: Duration = Duration::from_secs(30);
+
+/// When `SyncingEngine` is started, wait two minutes before actually staring to count peers as
+/// evicted.
+///
+/// Parachain collator may incorrectly get evicted because it's waiting to receive a number of
+/// relaychain blocks before it can start creating parachain blocks. During this wait,
+/// `SyncingEngine` still counts it as active and as the peer is not sending blocks, it may get
+/// evicted if a block is not received within the first 30 secons since the peer connected.
+///
+/// To prevent this from happening, define a threshold for how long `SyncingEngine` should wait
+/// before it starts evicting peers.
+const INITIAL_EVICTION_WAIT_PERIOD: Duration = Duration::from_secs(2 * 60);
+
+/// Maximum allowed size for a block announce.
+const MAX_BLOCK_ANNOUNCE_SIZE: u64 = 1024 * 1024;
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+    /// Peer has different genesis.
+    pub const GENESIS_MISMATCH: Rep = Rep::new_fatal("Genesis mismatch");
+    /// Peer send us a block announcement that failed at validation.
+    pub const BAD_BLOCK_ANNOUNCEMENT: Rep = Rep::new(-(1 << 12), "Bad block announcement");
+    /// Block announce substream with the peer has been inactive too long
+    pub const INACTIVE_SUBSTREAM: Rep = Rep::new(-(1 << 10), "Inactive block announce substream");
+    /// We received a message that failed to decode.
+    pub const BAD_MESSAGE: Rep = Rep::new(-(1 << 12), "Bad message");
+    /// Peer is on unsupported protocol version.
+    pub const BAD_PROTOCOL: Rep = Rep::new_fatal("Unsupported protocol");
+    /// Reputation change when a peer refuses a request.
+    pub const REFUSED: Rep = Rep::new(-(1 << 10), "Request refused");
+    /// Reputation change when a peer doesn't respond in time to our messages.
+    pub const TIMEOUT: Rep = Rep::new(-(1 << 10), "Request timeout");
+}
+
+struct Metrics {
+    peers: Gauge<U64>,
+    import_queue_blocks_submitted: Counter<U64>,
+    import_queue_justifications_submitted: Counter<U64>,
+}
+
+impl Metrics {
+    fn register(r: &Registry, major_syncing: Arc<AtomicBool>) -> Result<Self, PrometheusError> {
+        MajorSyncingGauge::register(r, major_syncing)?;
+        Ok(Self {
+            peers: {
+                let g = Gauge::new("substrate_sync_peers", "Number of peers we sync with")?;
+                register(g, r)?
+            },
+            import_queue_blocks_submitted: {
+                let c = Counter::new(
+                    "substrate_sync_import_queue_blocks_submitted",
+                    "Number of blocks submitted to the import queue.",
+                )?;
+                register(c, r)?
+            },
+            import_queue_justifications_submitted: {
+                let c = Counter::new(
+                    "substrate_sync_import_queue_justifications_submitted",
+                    "Number of justifications submitted to the import queue.",
+                )?;
+                register(c, r)?
+            },
+        })
+    }
+}
+
+/// The "major syncing" metric.
+#[derive(Clone)]
+pub struct MajorSyncingGauge(Arc<AtomicBool>);
+
+impl MajorSyncingGauge {
+    /// Registers the [`MajorSyncGauge`] metric whose value is
+    /// obtained from the given `AtomicBool`.
+    fn register(registry: &Registry, value: Arc<AtomicBool>) -> Result<(), PrometheusError> {
+        prometheus_endpoint::register(
+            SourcedGauge::new(
+                &Opts::new(
+                    "substrate_sub_libp2p_is_major_syncing",
+                    "Whether the node is performing a major sync or not.",
+                ),
+                MajorSyncingGauge(value),
+            )?,
+            registry,
+        )?;
+
+        Ok(())
+    }
+}
+
+impl MetricSource for MajorSyncingGauge {
+    type N = u64;
+
+    fn collect(&self, mut set: impl FnMut(&[&str], Self::N)) {
+        set(&[], self.0.load(Ordering::Relaxed) as u64);
+    }
+}
+
+/// Peer information
+#[derive(Debug)]
+pub struct Peer<B: BlockT> {
+    pub info: ExtendedPeerInfo<B>,
+    /// Holds a set of blocks known to this peer.
+    pub known_blocks: LruHashSet<B::Hash>,
+    /// Is the peer inbound.
+    inbound: bool,
+}
+
+pub struct SyncingEngine<B: BlockT, Client> {
+    /// Syncing strategy.
+    strategy: SyncingStrategy<B, Client>,
+
+    /// Syncing configuration for startegies.
+    syncing_config: SyncingConfig,
+
+    /// Blockchain client.
+    client: Arc<Client>,
+
+    /// Number of peers we're connected to.
+    num_connected: Arc<AtomicUsize>,
+
+    /// Are we actively catching up with the chain?
+    is_major_syncing: Arc<AtomicBool>,
+
+    /// Network service.
+    network_service: service::network::NetworkServiceHandle,
+
+    /// Channel for receiving service commands
+    service_rx: TracingUnboundedReceiver<ToServiceCommand<B>>,
+
+    /// Assigned roles.
+    roles: Roles,
+
+    /// Genesis hash.
+    genesis_hash: B::Hash,
+
+    /// Set of channels for other protocols that have subscribed to syncing events.
+    event_streams: Vec<TracingUnboundedSender<SyncEvent>>,
+
+    /// Interval at which we call `tick`.
+    tick_timeout: Interval,
+
+    /// All connected peers. Contains both full and light node peers.
+    peers: HashMap<PeerId, Peer<B>>,
+
+    /// List of nodes for which we perform additional logging because they are important for the
+    /// user.
+    important_peers: HashSet<PeerId>,
+
+    /// Actual list of connected no-slot nodes.
+    default_peers_set_no_slot_connected_peers: HashSet<PeerId>,
+
+    /// List of nodes that should never occupy peer slots.
+    default_peers_set_no_slot_peers: HashSet<PeerId>,
+
+    /// Value that was passed as part of the configuration. Used to cap the number of full
+    /// nodes.
+    default_peers_set_num_full: usize,
+
+    /// Number of slots to allocate to light nodes.
+    default_peers_set_num_light: usize,
+
+    /// Maximum number of inbound peers.
+    max_in_peers: usize,
+
+    /// Number of inbound peers accepted so far.
+    num_in_peers: usize,
+
+    /// Async processor of block announce validations.
+    block_announce_validator: BlockAnnounceValidatorStream<B>,
+
+    /// A cache for the data that was associated to a block announcement.
+    block_announce_data_cache: LruMap<B::Hash, Vec<u8>>,
+
+    /// The `PeerId`'s of all boot nodes.
+    boot_node_ids: HashSet<PeerId>,
+
+    /// A channel to get target block header if we skip over proofs downloading during warp sync.
+    warp_sync_target_block_header_rx_fused:
+        Fuse<BoxFuture<'static, Result<B::Header, oneshot::Canceled>>>,
+
+    /// Protocol name used for block announcements
+    block_announce_protocol_name: ProtocolName,
+
+    /// Prometheus metrics.
+    metrics: Option<Metrics>,
+
+    /// Handle that is used to communicate with `sc_network::Notifications`.
+    notification_service: Box<dyn NotificationService>,
+
+    /// When the syncing was started.
+    ///
+    /// Stored as an `Option<Instant>` so once the initial wait has passed, `SyncingEngine`
+    /// can reset the peer timers and continue with the normal eviction process.
+    syncing_started: Option<Instant>,
+
+    /// Handle to `PeerStore`.
+    peer_store_handle: PeerStoreHandle,
+
+    /// Instant when the last notification was sent or received.
+    last_notification_io: Instant,
+
+    /// Pending responses
+    pending_responses: PendingResponses<B>,
+
+    /// Block downloader
+    block_downloader: Arc<dyn BlockDownloader<B>>,
+
+    /// Protocol name used to send out state requests
+    state_request_protocol_name: ProtocolName,
+
+    /// Protocol name used to send out warp sync requests
+    warp_sync_protocol_name: Option<ProtocolName>,
+
+    /// Handle to import queue.
+    import_queue: Box<dyn ImportQueueService<B>>,
+}
+
+impl<B: BlockT, Client> SyncingEngine<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B>
+        + BlockBackend<B>
+        + HeaderMetadata<B, Error = sp_blockchain::Error>
+        + ProofProvider<B>
+        + Send
+        + Sync
+        + 'static,
+{
+    #[allow(clippy::too_many_arguments)]
+    pub fn new(
+        roles: Roles,
+        client: Arc<Client>,
+        metrics_registry: Option<&Registry>,
+        net_config: &FullNetworkConfiguration,
+        protocol_id: ProtocolId,
+        fork_id: &Option<String>,
+        block_announce_validator: Box<dyn BlockAnnounceValidator<B> + Send>,
+        warp_sync_params: Option<WarpSyncParams<B>>,
+        network_service: service::network::NetworkServiceHandle,
+        import_queue: Box<dyn ImportQueueService<B>>,
+        block_downloader: Arc<dyn BlockDownloader<B>>,
+        state_request_protocol_name: ProtocolName,
+        warp_sync_protocol_name: Option<ProtocolName>,
+        peer_store_handle: PeerStoreHandle,
+    ) -> Result<(Self, SyncingService<B>, NonDefaultSetConfig), ClientError> {
+        let mode = net_config.network_config.sync_mode;
+        let max_parallel_downloads = net_config.network_config.max_parallel_downloads;
+        let max_blocks_per_request =
+            if net_config.network_config.max_blocks_per_request > MAX_BLOCKS_IN_RESPONSE as u32 {
+                log::info!(
+                    target: LOG_TARGET,
+                    "clamping maximum blocks per request to {}",
+                    MAX_BLOCKS_IN_RESPONSE,
+                );
+                MAX_BLOCKS_IN_RESPONSE as u32
+            } else {
+                net_config.network_config.max_blocks_per_request
+            };
+        let syncing_config = SyncingConfig {
+            mode,
+            max_parallel_downloads,
+            max_blocks_per_request,
+            metrics_registry: metrics_registry.cloned(),
+        };
+        let cache_capacity = (net_config.network_config.default_peers_set.in_peers
+            + net_config.network_config.default_peers_set.out_peers)
+            .max(1);
+        let important_peers = {
+            let mut imp_p = HashSet::new();
+            for reserved in &net_config.network_config.default_peers_set.reserved_nodes {
+                imp_p.insert(reserved.peer_id);
+            }
+            for config in net_config.notification_protocols() {
+                let peer_ids = config
+                    .set_config()
+                    .reserved_nodes
+                    .iter()
+                    .map(|info| info.peer_id)
+                    .collect::<Vec<PeerId>>();
+                imp_p.extend(peer_ids);
+            }
+
+            imp_p.shrink_to_fit();
+            imp_p
+        };
+        let boot_node_ids = {
+            let mut list = HashSet::new();
+            for node in &net_config.network_config.boot_nodes {
+                list.insert(node.peer_id);
+            }
+            list.shrink_to_fit();
+            list
+        };
+        let default_peers_set_no_slot_peers = {
+            let mut no_slot_p: HashSet<PeerId> = net_config
+                .network_config
+                .default_peers_set
+                .reserved_nodes
+                .iter()
+                .map(|reserved| reserved.peer_id)
+                .collect();
+            no_slot_p.shrink_to_fit();
+            no_slot_p
+        };
+        let default_peers_set_num_full =
+            net_config.network_config.default_peers_set_num_full as usize;
+        let default_peers_set_num_light = {
+            let total = net_config.network_config.default_peers_set.out_peers
+                + net_config.network_config.default_peers_set.in_peers;
+            total.saturating_sub(net_config.network_config.default_peers_set_num_full) as usize
+        };
+
+        let (block_announce_config, notification_service) = Self::get_block_announce_proto_config(
+            protocol_id,
+            fork_id,
+            roles,
+            client.info().best_number,
+            client.info().best_hash,
+            client
+                .block_hash(Zero::zero())
+                .ok()
+                .flatten()
+                .expect("Genesis block exists; qed"),
+        );
+
+        // Split warp sync params into warp sync config and a channel to retreive target block
+        // header.
+        let (warp_sync_config, warp_sync_target_block_header_rx) =
+            warp_sync_params.map_or((None, None), |params| {
+                let (config, target_block_rx) = params.split();
+                (Some(config), target_block_rx)
+            });
+
+        // Make sure polling of the target block channel is a no-op if there is no block to
+        // retrieve.
+        let warp_sync_target_block_header_rx_fused = warp_sync_target_block_header_rx
+            .map_or(futures::future::pending().boxed().fuse(), |rx| {
+                rx.boxed().fuse()
+            });
+
+        // Initialize syncing strategy.
+        let strategy =
+            SyncingStrategy::new(syncing_config.clone(), client.clone(), warp_sync_config)?;
+
+        let block_announce_protocol_name = block_announce_config.protocol_name().clone();
+        let (tx, service_rx) = tracing_unbounded("mpsc_chain_sync", 100_000);
+        let num_connected = Arc::new(AtomicUsize::new(0));
+        let is_major_syncing = Arc::new(AtomicBool::new(false));
+        let genesis_hash = client
+            .block_hash(0u32.into())
+            .ok()
+            .flatten()
+            .expect("Genesis block exists; qed");
+
+        // `default_peers_set.in_peers` contains an unspecified amount of light peers so the number
+        // of full inbound peers must be calculated from the total full peer count
+        let max_full_peers = net_config.network_config.default_peers_set_num_full;
+        let max_out_peers = net_config.network_config.default_peers_set.out_peers;
+        let max_in_peers = (max_full_peers - max_out_peers) as usize;
+
+        let tick_timeout = {
+            let mut interval = tokio::time::interval(TICK_TIMEOUT);
+            interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
+            interval
+        };
+
+        Ok((
+            Self {
+                roles,
+                client,
+                strategy,
+                syncing_config,
+                network_service,
+                peers: HashMap::new(),
+                block_announce_data_cache: LruMap::new(ByLength::new(cache_capacity)),
+                block_announce_protocol_name,
+                block_announce_validator: BlockAnnounceValidatorStream::new(
+                    block_announce_validator,
+                ),
+                num_connected: num_connected.clone(),
+                is_major_syncing: is_major_syncing.clone(),
+                service_rx,
+                genesis_hash,
+                important_peers,
+                default_peers_set_no_slot_connected_peers: HashSet::new(),
+                warp_sync_target_block_header_rx_fused,
+                boot_node_ids,
+                default_peers_set_no_slot_peers,
+                default_peers_set_num_full,
+                default_peers_set_num_light,
+                num_in_peers: 0usize,
+                max_in_peers,
+                event_streams: Vec::new(),
+                notification_service,
+                tick_timeout,
+                syncing_started: None,
+                peer_store_handle,
+                last_notification_io: Instant::now(),
+                metrics: if let Some(r) = metrics_registry {
+                    match Metrics::register(r, is_major_syncing.clone()) {
+                        Ok(metrics) => Some(metrics),
+                        Err(err) => {
+                            log::error!(target: LOG_TARGET, "Failed to register metrics {err:?}");
+                            None
+                        }
+                    }
+                } else {
+                    None
+                },
+                pending_responses: PendingResponses::new(),
+                block_downloader,
+                state_request_protocol_name,
+                warp_sync_protocol_name,
+                import_queue,
+            },
+            SyncingService::new(tx, num_connected, is_major_syncing),
+            block_announce_config,
+        ))
+    }
+
+    /// Report Prometheus metrics.
+    pub fn report_metrics(&self) {
+        if let Some(metrics) = &self.metrics {
+            let n = u64::try_from(self.peers.len()).unwrap_or(std::u64::MAX);
+            metrics.peers.set(n);
+        }
+        self.strategy.report_metrics();
+    }
+
+    fn update_peer_info(
+        &mut self,
+        peer_id: &PeerId,
+        best_hash: B::Hash,
+        best_number: NumberFor<B>,
+    ) {
+        if let Some(ref mut peer) = self.peers.get_mut(peer_id) {
+            peer.info.best_hash = best_hash;
+            peer.info.best_number = best_number;
+        }
+    }
+
+    /// Process the result of the block announce validation.
+    fn process_block_announce_validation_result(
+        &mut self,
+        validation_result: BlockAnnounceValidationResult<B::Header>,
+    ) {
+        match validation_result {
+            BlockAnnounceValidationResult::Skip { peer_id: _ } => {}
+            BlockAnnounceValidationResult::Process {
+                is_new_best,
+                peer_id,
+                announce,
+            } => {
+                if let Some((best_hash, best_number)) =
+                    self.strategy
+                        .on_validated_block_announce(is_new_best, peer_id, &announce)
+                {
+                    self.update_peer_info(&peer_id, best_hash, best_number);
+                }
+
+                if let Some(data) = announce.data {
+                    if !data.is_empty() {
+                        self.block_announce_data_cache
+                            .insert(announce.header.hash(), data);
+                    }
+                }
+            }
+            BlockAnnounceValidationResult::Failure {
+                peer_id,
+                disconnect,
+            } => {
+                if disconnect {
+                    self.network_service
+                        .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                }
+
+                self.network_service
+                    .report_peer(peer_id, rep::BAD_BLOCK_ANNOUNCEMENT);
+            }
+        }
+    }
+
+    /// Push a block announce validation.
+    pub fn push_block_announce_validation(
+        &mut self,
+        peer_id: PeerId,
+        announce: BlockAnnounce<B::Header>,
+    ) {
+        let hash = announce.header.hash();
+
+        let peer = match self.peers.get_mut(&peer_id) {
+            Some(p) => p,
+            None => {
+                log::error!(
+                    target: LOG_TARGET,
+                    "Received block announce from disconnected peer {peer_id}",
+                );
+                debug_assert!(false);
+                return;
+            }
+        };
+        peer.known_blocks.insert(hash);
+
+        if peer.info.roles.is_full() {
+            let is_best = match announce.state.unwrap_or(BlockState::Best) {
+                BlockState::Best => true,
+                BlockState::Normal => false,
+            };
+
+            self.block_announce_validator
+                .push_block_announce_validation(peer_id, hash, announce, is_best);
+        }
+    }
+
+    /// Make sure an important block is propagated to peers.
+    ///
+    /// In chain-based consensus, we often need to make sure non-best forks are
+    /// at least temporarily synced.
+    pub fn announce_block(&mut self, hash: B::Hash, data: Option<Vec<u8>>) {
+        let header = match self.client.header(hash) {
+            Ok(Some(header)) => header,
+            Ok(None) => {
+                log::warn!(target: LOG_TARGET, "Trying to announce unknown block: {hash}");
+                return;
+            }
+            Err(e) => {
+                log::warn!(target: LOG_TARGET, "Error reading block header {hash}: {e}");
+                return;
+            }
+        };
+
+        // don't announce genesis block since it will be ignored
+        if header.number().is_zero() {
+            return;
+        }
+
+        let is_best = self.client.info().best_hash == hash;
+        log::debug!(target: LOG_TARGET, "Reannouncing block {hash:?} is_best: {is_best}");
+
+        let data = data
+            .or_else(|| self.block_announce_data_cache.get(&hash).cloned())
+            .unwrap_or_default();
+
+        for (peer_id, ref mut peer) in self.peers.iter_mut() {
+            let inserted = peer.known_blocks.insert(hash);
+            if inserted {
+                log::trace!(target: LOG_TARGET, "Announcing block {hash:?} to {peer_id}");
+                let message = BlockAnnounce {
+                    header: header.clone(),
+                    state: if is_best {
+                        Some(BlockState::Best)
+                    } else {
+                        Some(BlockState::Normal)
+                    },
+                    data: Some(data.clone()),
+                };
+
+                self.last_notification_io = Instant::now();
+                self.notification_service
+                    .send_sync_notification(peer_id, message.encode());
+            }
+        }
+    }
+
+    pub async fn run(mut self) {
+        self.syncing_started = Some(Instant::now());
+
+        loop {
+            tokio::select! {
+                _ = self.tick_timeout.tick() => self.perform_periodic_actions(),
+                command = self.service_rx.select_next_some() =>
+                    self.process_service_command(command),
+                notification_event = self.notification_service.next_event() => match notification_event {
+                    Some(event) => self.process_notification_event(event),
+                    None => return,
+                },
+                warp_target_block_header = &mut self.warp_sync_target_block_header_rx_fused =>
+                    self.pass_warp_sync_target_block_header(warp_target_block_header),
+                response_event = self.pending_responses.select_next_some() =>
+                    self.process_response_event(response_event),
+                validation_result = self.block_announce_validator.select_next_some() =>
+                    self.process_block_announce_validation_result(validation_result),
+            }
+
+            // Update atomic variables
+            self.num_connected
+                .store(self.peers.len(), Ordering::Relaxed);
+            self.is_major_syncing
+                .store(self.strategy.is_major_syncing(), Ordering::Relaxed);
+
+            // Process actions requested by a syncing strategy.
+            if let Err(e) = self.process_strategy_actions() {
+                error!("Terminating `SyncingEngine` due to fatal error: {e:?}");
+                return;
+            }
+        }
+    }
+
+    fn process_strategy_actions(&mut self) -> Result<(), ClientError> {
+        for action in self.strategy.actions() {
+            match action {
+                SyncingAction::SendBlockRequest { peer_id, request } => {
+                    // Sending block request implies dropping obsolete pending response as we are
+                    // not interested in it anymore (see [`SyncingAction::SendBlockRequest`]).
+                    // Furthermore, only one request at a time is allowed to any peer.
+                    let removed = self.pending_responses.remove(&peer_id);
+                    self.send_block_request(peer_id, request.clone());
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed `ChainSyncAction::SendBlockRequest` to {} with {:?}, stale response removed: {}.",
+                        peer_id,
+                        request,
+                        removed,
+                    )
+                }
+                SyncingAction::CancelBlockRequest { peer_id } => {
+                    let removed = self.pending_responses.remove(&peer_id);
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed {action:?}, response removed: {removed}.",
+                    );
+                }
+                SyncingAction::SendStateRequest { peer_id, request } => {
+                    self.send_state_request(peer_id, request);
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed `ChainSyncAction::SendBlockRequest` to {peer_id}.",
+                    );
+                }
+                SyncingAction::SendWarpProofRequest { peer_id, request } => {
+                    self.send_warp_proof_request(peer_id, request.clone());
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed `ChainSyncAction::SendWarpProofRequest` to {}, request: {:?}.",
+                        peer_id,
+                        request,
+                    );
+                }
+                SyncingAction::DropPeer(BadPeer(peer_id, rep)) => {
+                    self.pending_responses.remove(&peer_id);
+                    self.network_service
+                        .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    self.network_service.report_peer(peer_id, rep);
+
+                    trace!(target: LOG_TARGET, "{peer_id:?} dropped: {rep:?}.");
+                }
+                SyncingAction::ImportBlocks { origin, blocks } => {
+                    let count = blocks.len();
+                    self.import_blocks(origin, blocks);
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed `ChainSyncAction::ImportBlocks` with {count} blocks.",
+                    );
+                }
+                SyncingAction::ImportJustifications {
+                    peer_id,
+                    hash,
+                    number,
+                    justifications,
+                } => {
+                    self.import_justifications(peer_id, hash, number, justifications);
+
+                    trace!(
+                        target: LOG_TARGET,
+                        "Processed `ChainSyncAction::ImportJustifications` from peer {} for block {} ({}).",
+                        peer_id,
+                        hash,
+                        number,
+                    )
+                }
+                SyncingAction::Finished => {
+                    let connected_peers = self.peers.iter().filter_map(|(peer_id, peer)| {
+                        peer.info.roles.is_full().then_some((
+                            *peer_id,
+                            peer.info.best_hash,
+                            peer.info.best_number,
+                        ))
+                    });
+                    self.strategy.switch_to_next(
+                        self.syncing_config.clone(),
+                        self.client.clone(),
+                        connected_peers,
+                    )?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn perform_periodic_actions(&mut self) {
+        self.report_metrics();
+
+        // if `SyncingEngine` has just started, don't evict seemingly inactive peers right away
+        // as they may not have produced blocks not because they've disconnected but because
+        // they're still waiting to receive enough relaychain blocks to start producing blocks.
+        if let Some(started) = self.syncing_started {
+            if started.elapsed() < INITIAL_EVICTION_WAIT_PERIOD {
+                return;
+            }
+
+            self.syncing_started = None;
+            self.last_notification_io = Instant::now();
+        }
+
+        // if syncing hasn't sent or received any blocks within `INACTIVITY_EVICT_THRESHOLD`,
+        // it means the local node has stalled and is connected to peers who either don't
+        // consider it connected or are also all stalled. In order to unstall the node,
+        // disconnect all peers and allow `ProtocolController` to establish new connections.
+        if self.last_notification_io.elapsed() > INACTIVITY_EVICT_THRESHOLD {
+            log::debug!(
+                target: LOG_TARGET,
+                "syncing has halted due to inactivity, evicting all peers",
+            );
+
+            for peer in self.peers.keys() {
+                self.network_service
+                    .report_peer(*peer, rep::INACTIVE_SUBSTREAM);
+                self.network_service
+                    .disconnect_peer(*peer, self.block_announce_protocol_name.clone());
+            }
+
+            // after all the peers have been evicted, start timer again to prevent evicting
+            // new peers that join after the old peer have been evicted
+            self.last_notification_io = Instant::now();
+        }
+    }
+
+    fn process_service_command(&mut self, command: ToServiceCommand<B>) {
+        match command {
+            ToServiceCommand::SetSyncForkRequest(peers, hash, number) => {
+                self.strategy.set_sync_fork_request(peers, &hash, number);
+            }
+            ToServiceCommand::EventStream(tx) => self.event_streams.push(tx),
+            ToServiceCommand::RequestJustification(hash, number) => {
+                self.strategy.request_justification(&hash, number)
+            }
+            ToServiceCommand::ClearJustificationRequests => {
+                self.strategy.clear_justification_requests()
+            }
+            ToServiceCommand::BlocksProcessed(imported, count, results) => {
+                self.strategy.on_blocks_processed(imported, count, results);
+            }
+            ToServiceCommand::JustificationImported(peer_id, hash, number, success) => {
+                self.strategy.on_justification_import(hash, number, success);
+                if !success {
+                    log::info!(
+                        target: LOG_TARGET,
+                        "💔 Invalid justification provided by {peer_id} for #{hash}",
+                    );
+                    self.network_service
+                        .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    self.network_service.report_peer(
+                        peer_id,
+                        ReputationChange::new_fatal("Invalid justification"),
+                    );
+                }
+            }
+            ToServiceCommand::AnnounceBlock(hash, data) => self.announce_block(hash, data),
+            ToServiceCommand::NewBestBlockImported(hash, number) => {
+                log::debug!(target: LOG_TARGET, "New best block imported {:?}/#{}", hash, number);
+
+                self.strategy.update_chain_info(&hash, number);
+                let _ = self.notification_service.try_set_handshake(
+                    BlockAnnouncesHandshake::<B>::build(
+                        self.roles,
+                        number,
+                        hash,
+                        self.genesis_hash,
+                    )
+                    .encode(),
+                );
+            }
+            ToServiceCommand::Status(tx) => {
+                let mut status = self.strategy.status();
+                status.num_connected_peers = self.peers.len() as u32;
+                let _ = tx.send(status);
+            }
+            ToServiceCommand::NumActivePeers(tx) => {
+                let _ = tx.send(self.num_active_peers());
+            }
+            ToServiceCommand::SyncState(tx) => {
+                let _ = tx.send(self.strategy.status());
+            }
+            ToServiceCommand::BestSeenBlock(tx) => {
+                let _ = tx.send(self.strategy.status().best_seen_block);
+            }
+            ToServiceCommand::NumSyncPeers(tx) => {
+                let _ = tx.send(self.strategy.status().num_peers);
+            }
+            ToServiceCommand::NumQueuedBlocks(tx) => {
+                let _ = tx.send(self.strategy.status().queued_blocks);
+            }
+            ToServiceCommand::NumDownloadedBlocks(tx) => {
+                let _ = tx.send(self.strategy.num_downloaded_blocks());
+            }
+            ToServiceCommand::NumSyncRequests(tx) => {
+                let _ = tx.send(self.strategy.num_sync_requests());
+            }
+            ToServiceCommand::PeersInfo(tx) => {
+                let peers_info = self
+                    .peers
+                    .iter()
+                    .map(|(peer_id, peer)| (*peer_id, peer.info.clone()))
+                    .collect();
+                let _ = tx.send(peers_info);
+            }
+            ToServiceCommand::OnBlockFinalized(hash, header) => {
+                self.strategy.on_block_finalized(&hash, *header.number())
+            }
+        }
+    }
+
+    fn process_notification_event(&mut self, event: NotificationEvent) {
+        match event {
+            NotificationEvent::ValidateInboundSubstream {
+                peer,
+                handshake,
+                result_tx,
+            } => {
+                let validation_result = self
+                    .validate_connection(&peer, handshake, Direction::Inbound)
+                    .map_or(ValidationResult::Reject, |_| ValidationResult::Accept);
+
+                let _ = result_tx.send(validation_result);
+            }
+            NotificationEvent::NotificationStreamOpened {
+                peer,
+                handshake,
+                direction,
+                ..
+            } => {
+                log::debug!(
+                    target: LOG_TARGET,
+                    "Substream opened for {peer}, handshake {handshake:?}"
+                );
+
+                match self.validate_connection(&peer, handshake, direction) {
+                    Ok(handshake) => {
+                        if self
+                            .on_sync_peer_connected(peer, &handshake, direction)
+                            .is_err()
+                        {
+                            log::debug!(target: LOG_TARGET, "Failed to register peer {peer}");
+                            self.network_service
+                                .disconnect_peer(peer, self.block_announce_protocol_name.clone());
+                        }
+                    }
+                    Err(wrong_genesis) => {
+                        log::debug!(target: LOG_TARGET, "`SyncingEngine` rejected {peer}");
+
+                        if wrong_genesis {
+                            self.peer_store_handle
+                                .report_peer(peer, rep::GENESIS_MISMATCH);
+                        }
+
+                        self.network_service
+                            .disconnect_peer(peer, self.block_announce_protocol_name.clone());
+                    }
+                }
+            }
+            NotificationEvent::NotificationStreamClosed { peer } => {
+                self.on_sync_peer_disconnected(peer);
+            }
+            NotificationEvent::NotificationReceived { peer, notification } => {
+                if !self.peers.contains_key(&peer) {
+                    log::error!(
+                        target: LOG_TARGET,
+                        "received notification from {peer} who had been earlier refused by `SyncingEngine`",
+                    );
+                    return;
+                }
+
+                let Ok(announce) = BlockAnnounce::decode(&mut notification.as_ref()) else {
+                    log::warn!(target: LOG_TARGET, "failed to decode block announce");
+                    return;
+                };
+
+                self.last_notification_io = Instant::now();
+                self.push_block_announce_validation(peer, announce);
+            }
+        }
+    }
+
+    fn pass_warp_sync_target_block_header(&mut self, header: Result<B::Header, oneshot::Canceled>) {
+        match header {
+            Ok(header) => {
+                if let SyncingStrategy::WarpSyncStrategy(warp_sync) = &mut self.strategy {
+                    warp_sync.set_target_block(header);
+                } else {
+                    error!(
+                        target: LOG_TARGET,
+                        "Cannot set warp sync target block: no warp sync strategy is active."
+                    );
+                    debug_assert!(false);
+                }
+            }
+            Err(err) => {
+                error!(
+                    target: LOG_TARGET,
+                    "Failed to get target block for warp sync. Error: {err:?}",
+                );
+            }
+        }
+    }
+
+    /// Called by peer when it is disconnecting.
+    ///
+    /// Returns a result if the handshake of this peer was indeed accepted.
+    fn on_sync_peer_disconnected(&mut self, peer_id: PeerId) {
+        let Some(info) = self.peers.remove(&peer_id) else {
+            log::debug!(target: LOG_TARGET, "{peer_id} does not exist in `SyncingEngine`");
+            return;
+        };
+
+        if self.important_peers.contains(&peer_id) {
+            log::warn!(target: LOG_TARGET, "Reserved peer {peer_id} disconnected");
+        } else {
+            log::debug!(target: LOG_TARGET, "{peer_id} disconnected");
+        }
+
+        if !self
+            .default_peers_set_no_slot_connected_peers
+            .remove(&peer_id)
+            && info.inbound
+            && info.info.roles.is_full()
+        {
+            match self.num_in_peers.checked_sub(1) {
+                Some(value) => {
+                    self.num_in_peers = value;
+                }
+                None => {
+                    log::error!(
+                        target: LOG_TARGET,
+                        "trying to disconnect an inbound node which is not counted as inbound"
+                    );
+                    debug_assert!(false);
+                }
+            }
+        }
+
+        self.strategy.remove_peer(&peer_id);
+        self.pending_responses.remove(&peer_id);
+        self.event_streams.retain(|stream| {
+            stream
+                .unbounded_send(SyncEvent::PeerDisconnected(peer_id))
+                .is_ok()
+        });
+    }
+
+    /// Validate received handshake.
+    fn validate_handshake(
+        &mut self,
+        peer_id: &PeerId,
+        handshake: Vec<u8>,
+    ) -> Result<BlockAnnouncesHandshake<B>, bool> {
+        log::trace!(target: LOG_TARGET, "Validate handshake for {peer_id}");
+
+        let handshake = <BlockAnnouncesHandshake<B> as DecodeAll>::decode_all(&mut &handshake[..])
+			.map_err(|error| {
+				log::debug!(target: LOG_TARGET, "Failed to decode handshake for {peer_id}: {error:?}");
+				false
+			})?;
+
+        if handshake.genesis_hash != self.genesis_hash {
+            if self.important_peers.contains(peer_id) {
+                log::error!(
+                    target: LOG_TARGET,
+                    "Reserved peer id `{peer_id}` is on a different chain (our genesis: {} theirs: {})",
+                    self.genesis_hash,
+                    handshake.genesis_hash,
+                );
+            } else if self.boot_node_ids.contains(peer_id) {
+                log::error!(
+                    target: LOG_TARGET,
+                    "Bootnode with peer id `{peer_id}` is on a different chain (our genesis: {} theirs: {})",
+                    self.genesis_hash,
+                    handshake.genesis_hash,
+                );
+            } else {
+                log::debug!(
+                    target: LOG_TARGET,
+                    "Peer is on different chain (our genesis: {} theirs: {})",
+                    self.genesis_hash,
+                    handshake.genesis_hash
+                );
+            }
+
+            return Err(true);
+        }
+
+        Ok(handshake)
+    }
+
+    /// Validate connection.
+    // NOTE Returning `Err(bool)` is a really ugly hack to work around the issue
+    // that `ProtocolController` thinks the peer is connected when in fact it can
+    // still be under validation. If the peer has different genesis than the
+    // local node the validation fails but the peer cannot be reported in
+    // `validate_connection()` as that is also called by
+    // `ValiateInboundSubstream` which means that the peer is still being
+    // validated and banning the peer when handling that event would
+    // result in peer getting dropped twice.
+    //
+    // The proper way to fix this is to integrate `ProtocolController` more
+    // tightly with `NotificationService` or add an additional API call for
+    // banning pre-accepted peers (which is not desirable)
+    fn validate_connection(
+        &mut self,
+        peer_id: &PeerId,
+        handshake: Vec<u8>,
+        direction: Direction,
+    ) -> Result<BlockAnnouncesHandshake<B>, bool> {
+        log::trace!(target: LOG_TARGET, "New peer {peer_id} {handshake:?}");
+
+        let handshake = self.validate_handshake(peer_id, handshake)?;
+
+        if self.peers.contains_key(peer_id) {
+            log::error!(
+                target: LOG_TARGET,
+                "Called `validate_connection()` with already connected peer {peer_id}",
+            );
+            debug_assert!(false);
+            return Err(false);
+        }
+
+        let no_slot_peer = self.default_peers_set_no_slot_peers.contains(peer_id);
+        let this_peer_reserved_slot: usize = if no_slot_peer { 1 } else { 0 };
+
+        if handshake.roles.is_full()
+            && self.strategy.num_peers()
+                >= self.default_peers_set_num_full
+                    + self.default_peers_set_no_slot_connected_peers.len()
+                    + this_peer_reserved_slot
+        {
+            log::debug!(target: LOG_TARGET, "Too many full nodes, rejecting {peer_id}");
+            return Err(false);
+        }
+
+        // make sure to accept no more than `--in-peers` many full nodes
+        if !no_slot_peer
+            && handshake.roles.is_full()
+            && direction.is_inbound()
+            && self.num_in_peers == self.max_in_peers
+        {
+            log::debug!(target: LOG_TARGET, "All inbound slots have been consumed, rejecting {peer_id}");
+            return Err(false);
+        }
+
+        // make sure that all slots are not occupied by light peers
+        //
+        // `ChainSync` only accepts full peers whereas `SyncingEngine` accepts both full and light
+        // peers. Verify that there is a slot in `SyncingEngine` for the inbound light peer
+        if handshake.roles.is_light()
+            && (self.peers.len() - self.strategy.num_peers()) >= self.default_peers_set_num_light
+        {
+            log::debug!(target: LOG_TARGET, "Too many light nodes, rejecting {peer_id}");
+            return Err(false);
+        }
+
+        Ok(handshake)
+    }
+
+    /// Called on the first connection between two peers on the default set, after their exchange
+    /// of handshake.
+    ///
+    /// Returns `Ok` if the handshake is accepted and the peer added to the list of peers we sync
+    /// from.
+    fn on_sync_peer_connected(
+        &mut self,
+        peer_id: PeerId,
+        status: &BlockAnnouncesHandshake<B>,
+        direction: Direction,
+    ) -> Result<(), ()> {
+        log::trace!(target: LOG_TARGET, "New peer {peer_id} {status:?}");
+
+        let peer = Peer {
+            info: ExtendedPeerInfo {
+                roles: status.roles,
+                best_hash: status.best_hash,
+                best_number: status.best_number,
+            },
+            known_blocks: LruHashSet::new(
+                NonZeroUsize::new(MAX_KNOWN_BLOCKS).expect("Constant is nonzero"),
+            ),
+            inbound: direction.is_inbound(),
+        };
+
+        // Only forward full peers to syncing strategy.
+        if status.roles.is_full() {
+            self.strategy
+                .add_peer(peer_id, peer.info.best_hash, peer.info.best_number);
+        }
+
+        log::debug!(target: LOG_TARGET, "Connected {peer_id}");
+
+        self.peers.insert(peer_id, peer);
+        self.peer_store_handle
+            .set_peer_role(&peer_id, status.roles.into());
+
+        if self.default_peers_set_no_slot_peers.contains(&peer_id) {
+            self.default_peers_set_no_slot_connected_peers
+                .insert(peer_id);
+        } else if direction.is_inbound() && status.roles.is_full() {
+            self.num_in_peers += 1;
+        }
+
+        self.event_streams.retain(|stream| {
+            stream
+                .unbounded_send(SyncEvent::PeerConnected(peer_id))
+                .is_ok()
+        });
+
+        Ok(())
+    }
+
+    fn send_block_request(&mut self, peer_id: PeerId, request: BlockRequest<B>) {
+        if !self.peers.contains_key(&peer_id) {
+            trace!(target: LOG_TARGET, "Cannot send block request to unknown peer {peer_id}");
+            debug_assert!(false);
+            return;
+        }
+
+        let downloader = self.block_downloader.clone();
+
+        self.pending_responses.insert(
+            peer_id,
+            PeerRequest::Block(request.clone()),
+            async move { downloader.download_blocks(peer_id, request).await }.boxed(),
+        );
+    }
+
+    fn send_state_request(&mut self, peer_id: PeerId, request: OpaqueStateRequest) {
+        if !self.peers.contains_key(&peer_id) {
+            trace!(target: LOG_TARGET, "Cannot send state request to unknown peer {peer_id}");
+            debug_assert!(false);
+            return;
+        }
+
+        let (tx, rx) = oneshot::channel();
+
+        self.pending_responses
+            .insert(peer_id, PeerRequest::State, rx.boxed());
+
+        match Self::encode_state_request(&request) {
+            Ok(data) => {
+                self.network_service.start_request(
+                    peer_id,
+                    self.state_request_protocol_name.clone(),
+                    data,
+                    tx,
+                    IfDisconnected::ImmediateError,
+                );
+            }
+            Err(err) => {
+                log::warn!(
+                    target: LOG_TARGET,
+                    "Failed to encode state request {request:?}: {err:?}",
+                );
+            }
+        }
+    }
+
+    fn send_warp_proof_request(&mut self, peer_id: PeerId, request: WarpProofRequest<B>) {
+        if !self.peers.contains_key(&peer_id) {
+            trace!(target: LOG_TARGET, "Cannot send warp proof request to unknown peer {peer_id}");
+            debug_assert!(false);
+            return;
+        }
+
+        let (tx, rx) = oneshot::channel();
+
+        self.pending_responses
+            .insert(peer_id, PeerRequest::WarpProof, rx.boxed());
+
+        match &self.warp_sync_protocol_name {
+            Some(name) => self.network_service.start_request(
+                peer_id,
+                name.clone(),
+                request.encode(),
+                tx,
+                IfDisconnected::ImmediateError,
+            ),
+            None => {
+                log::warn!(
+                    target: LOG_TARGET,
+                    "Trying to send warp sync request when no protocol is configured {request:?}",
+                );
+            }
+        }
+    }
+
+    fn encode_state_request(request: &OpaqueStateRequest) -> Result<Vec<u8>, String> {
+        let request: &StateRequest = request.0.downcast_ref().ok_or_else(|| {
+            "Failed to downcast opaque state response during encoding, this is an \
+				implementation bug."
+                .to_string()
+        })?;
+
+        Ok(request.encode_to_vec())
+    }
+
+    fn decode_state_response(response: &[u8]) -> Result<OpaqueStateResponse, String> {
+        let response = StateResponse::decode(response)
+            .map_err(|error| format!("Failed to decode state response: {error}"))?;
+
+        Ok(OpaqueStateResponse(Box::new(response)))
+    }
+
+    fn process_response_event(&mut self, response_event: ResponseEvent<B>) {
+        let ResponseEvent {
+            peer_id,
+            request,
+            response,
+        } = response_event;
+
+        match response {
+            Ok(Ok((resp, _))) => match request {
+                PeerRequest::Block(req) => {
+                    match self.block_downloader.block_response_into_blocks(&req, resp) {
+                        Ok(blocks) => {
+                            self.strategy.on_block_response(peer_id, req, blocks);
+                        }
+                        Err(BlockResponseError::DecodeFailed(e)) => {
+                            debug!(
+                                target: LOG_TARGET,
+                                "Failed to decode block response from peer {:?}: {:?}.",
+                                peer_id,
+                                e
+                            );
+                            self.network_service.report_peer(peer_id, rep::BAD_MESSAGE);
+                            self.network_service.disconnect_peer(
+                                peer_id,
+                                self.block_announce_protocol_name.clone(),
+                            );
+                        }
+                        Err(BlockResponseError::ExtractionFailed(e)) => {
+                            debug!(
+                                target: LOG_TARGET,
+                                "Failed to extract blocks from peer response {:?}: {:?}.",
+                                peer_id,
+                                e
+                            );
+                            self.network_service.report_peer(peer_id, rep::BAD_MESSAGE);
+                        }
+                    }
+                }
+                PeerRequest::State => {
+                    let response = match Self::decode_state_response(&resp[..]) {
+                        Ok(proto) => proto,
+                        Err(e) => {
+                            debug!(
+                                target: LOG_TARGET,
+                                "Failed to decode state response from peer {peer_id:?}: {e:?}.",
+                            );
+                            self.network_service.report_peer(peer_id, rep::BAD_MESSAGE);
+                            self.network_service.disconnect_peer(
+                                peer_id,
+                                self.block_announce_protocol_name.clone(),
+                            );
+                            return;
+                        }
+                    };
+
+                    self.strategy.on_state_response(peer_id, response);
+                }
+                PeerRequest::WarpProof => {
+                    self.strategy
+                        .on_warp_proof_response(&peer_id, EncodedProof(resp));
+                }
+            },
+            Ok(Err(e)) => {
+                debug!(target: LOG_TARGET, "Request to peer {peer_id:?} failed: {e:?}.");
+
+                match e {
+                    RequestFailure::Network(OutboundFailure::Timeout) => {
+                        self.network_service.report_peer(peer_id, rep::TIMEOUT);
+                        self.network_service
+                            .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    }
+                    RequestFailure::Network(OutboundFailure::UnsupportedProtocols) => {
+                        self.network_service.report_peer(peer_id, rep::BAD_PROTOCOL);
+                        self.network_service
+                            .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    }
+                    RequestFailure::Network(OutboundFailure::DialFailure) => {
+                        self.network_service
+                            .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    }
+                    RequestFailure::Refused => {
+                        self.network_service.report_peer(peer_id, rep::REFUSED);
+                        self.network_service
+                            .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    }
+                    RequestFailure::Network(OutboundFailure::ConnectionClosed)
+                    | RequestFailure::NotConnected => {
+                        self.network_service
+                            .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+                    }
+                    RequestFailure::UnknownProtocol => {
+                        debug_assert!(false, "Block request protocol should always be known.");
+                    }
+                    RequestFailure::Obsolete => {
+                        debug_assert!(
+                            false,
+                            "Can not receive `RequestFailure::Obsolete` after dropping the \
+								response receiver.",
+                        );
+                    }
+                }
+            }
+            Err(oneshot::Canceled) => {
+                trace!(
+                    target: LOG_TARGET,
+                    "Request to peer {peer_id:?} failed due to oneshot being canceled.",
+                );
+                self.network_service
+                    .disconnect_peer(peer_id, self.block_announce_protocol_name.clone());
+            }
+        }
+    }
+
+    /// Returns the number of peers we're connected to and that are being queried.
+    fn num_active_peers(&self) -> usize {
+        self.pending_responses.len()
+    }
+
+    /// Get config for the block announcement protocol
+    fn get_block_announce_proto_config(
+        protocol_id: ProtocolId,
+        fork_id: &Option<String>,
+        roles: Roles,
+        best_number: NumberFor<B>,
+        best_hash: B::Hash,
+        genesis_hash: B::Hash,
+    ) -> (NonDefaultSetConfig, Box<dyn NotificationService>) {
+        let block_announces_protocol = {
+            let genesis_hash = genesis_hash.as_ref();
+            if let Some(ref fork_id) = fork_id {
+                format!(
+                    "/{}/{}/block-announces/1",
+                    array_bytes::bytes2hex("", genesis_hash),
+                    fork_id
+                )
+            } else {
+                format!(
+                    "/{}/block-announces/1",
+                    array_bytes::bytes2hex("", genesis_hash)
+                )
+            }
+        };
+
+        let (cfg, handle) = NonDefaultSetConfig::new(
+            block_announces_protocol.into(),
+            iter::once(format!("/{}/block-announces/1", protocol_id.as_ref()).into()).collect(),
+            MAX_BLOCK_ANNOUNCE_SIZE,
+            Some(NotificationHandshake::new(
+                BlockAnnouncesHandshake::<B>::build(roles, best_number, best_hash, genesis_hash),
+            )),
+            // NOTE: `set_config` will be ignored by `protocol.rs` as the block announcement
+            // protocol is still hardcoded into the peerset.
+            SetConfig {
+                in_peers: 0,
+                out_peers: 0,
+                reserved_nodes: Vec::new(),
+                non_reserved_mode: NonReservedPeerMode::Deny,
+            },
+        );
+        (cfg, Box::new(handle))
+    }
+
+    /// Import blocks.
+    fn import_blocks(&mut self, origin: BlockOrigin, blocks: Vec<IncomingBlock<B>>) {
+        if let Some(metrics) = &self.metrics {
+            metrics.import_queue_blocks_submitted.inc();
+        }
+
+        self.import_queue.import_blocks(origin, blocks);
+    }
+
+    /// Import justifications.
+    fn import_justifications(
+        &mut self,
+        peer_id: PeerId,
+        hash: B::Hash,
+        number: NumberFor<B>,
+        justifications: Justifications,
+    ) {
+        if let Some(metrics) = &self.metrics {
+            metrics.import_queue_justifications_submitted.inc();
+        }
+
+        self.import_queue
+            .import_justifications(peer_id, hash, number, justifications);
+    }
+}
diff --git a/libp2p/sync/src/extra_requests.rs b/libp2p/sync/src/extra_requests.rs
new file mode 100644
index 000000000..ea472161c
--- /dev/null
+++ b/libp2p/sync/src/extra_requests.rs
@@ -0,0 +1,653 @@
+// 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::{
+    request_metrics::Metrics,
+    strategy::chain_sync::{PeerSync, PeerSyncState},
+    LOG_TARGET,
+};
+use fork_tree::ForkTree;
+use libp2p::PeerId;
+use log::{debug, trace, warn};
+use sp_blockchain::Error as ClientError;
+use sp_runtime::traits::{Block as BlockT, NumberFor, Zero};
+use std::{
+    collections::{HashMap, HashSet, VecDeque},
+    time::{Duration, Instant},
+};
+
+// Time to wait before trying to get the same extra data from the same peer.
+const EXTRA_RETRY_WAIT: Duration = Duration::from_secs(10);
+
+/// Pending extra data request for the given block (hash and number).
+type ExtraRequest<B> = (<B as BlockT>::Hash, NumberFor<B>);
+
+/// Manages pending block extra data (e.g. justification) requests.
+///
+/// Multiple extras may be requested for competing forks, or for the same branch
+/// at different (increasing) heights. This structure will guarantee that extras
+/// are fetched in-order, and that obsolete changes are pruned (when finalizing a
+/// competing fork).
+#[derive(Debug)]
+pub(crate) struct ExtraRequests<B: BlockT> {
+    tree: ForkTree<B::Hash, NumberFor<B>, ()>,
+    /// best finalized block number that we have seen since restart
+    best_seen_finalized_number: NumberFor<B>,
+    /// requests which have been queued for later processing
+    pending_requests: VecDeque<ExtraRequest<B>>,
+    /// requests which are currently underway to some peer
+    active_requests: HashMap<PeerId, ExtraRequest<B>>,
+    /// previous requests without response
+    failed_requests: HashMap<ExtraRequest<B>, Vec<(PeerId, Instant)>>,
+    /// successful requests
+    importing_requests: HashSet<ExtraRequest<B>>,
+    /// the name of this type of extra request (useful for logging.)
+    request_type_name: &'static str,
+}
+
+impl<B: BlockT> ExtraRequests<B> {
+    pub(crate) fn new(request_type_name: &'static str) -> Self {
+        Self {
+            tree: ForkTree::new(),
+            best_seen_finalized_number: Zero::zero(),
+            pending_requests: VecDeque::new(),
+            active_requests: HashMap::new(),
+            failed_requests: HashMap::new(),
+            importing_requests: HashSet::new(),
+            request_type_name,
+        }
+    }
+
+    /// Reset all state as if returned from `new`.
+    pub(crate) fn reset(&mut self) {
+        self.tree = ForkTree::new();
+        self.pending_requests.clear();
+        self.active_requests.clear();
+        self.failed_requests.clear();
+    }
+
+    /// Returns an iterator-like struct that yields peers which extra
+    /// requests can be sent to.
+    pub(crate) fn matcher(&mut self) -> Matcher<B> {
+        Matcher::new(self)
+    }
+
+    /// Queue an extra data request to be considered by the `Matcher`.
+    pub(crate) fn schedule<F>(&mut self, request: ExtraRequest<B>, is_descendent_of: F)
+    where
+        F: Fn(&B::Hash, &B::Hash) -> Result<bool, ClientError>,
+    {
+        match self
+            .tree
+            .import(request.0, request.1, (), &is_descendent_of)
+        {
+            Ok(true) => {
+                // this is a new root so we add it to the current `pending_requests`
+                self.pending_requests.push_back((request.0, request.1));
+            }
+            Err(fork_tree::Error::Revert) => {
+                // we have finalized further than the given request, presumably
+                // by some other part of the system (not sync). we can safely
+                // ignore the `Revert` error.
+            }
+            Err(err) => {
+                debug!(target: LOG_TARGET, "Failed to insert request {:?} into tree: {}", request, err);
+            }
+            _ => (),
+        }
+    }
+
+    /// Retry any pending request if a peer disconnected.
+    pub(crate) fn peer_disconnected(&mut self, who: &PeerId) {
+        if let Some(request) = self.active_requests.remove(who) {
+            self.pending_requests.push_front(request);
+        }
+    }
+
+    /// Processes the response for the request previously sent to the given peer.
+    pub(crate) fn on_response<R>(
+        &mut self,
+        who: PeerId,
+        resp: Option<R>,
+    ) -> Option<(PeerId, B::Hash, NumberFor<B>, R)> {
+        // we assume that the request maps to the given response, this is
+        // currently enforced by the outer network protocol before passing on
+        // messages to chain sync.
+        if let Some(request) = self.active_requests.remove(&who) {
+            if let Some(r) = resp {
+                trace!(target: LOG_TARGET,
+                    "Queuing import of {} from {:?} for {:?}",
+                    self.request_type_name, who, request,
+                );
+
+                self.importing_requests.insert(request);
+                return Some((who, request.0, request.1, r));
+            } else {
+                trace!(target: LOG_TARGET,
+                    "Empty {} response from {:?} for {:?}",
+                    self.request_type_name, who, request,
+                );
+            }
+            self.failed_requests
+                .entry(request)
+                .or_default()
+                .push((who, Instant::now()));
+            self.pending_requests.push_front(request);
+        } else {
+            trace!(target: LOG_TARGET,
+                "No active {} request to {:?}",
+                self.request_type_name, who,
+            );
+        }
+        None
+    }
+
+    /// Removes any pending extra requests for blocks lower than the given best finalized.
+    pub(crate) fn on_block_finalized<F>(
+        &mut self,
+        best_finalized_hash: &B::Hash,
+        best_finalized_number: NumberFor<B>,
+        is_descendent_of: F,
+    ) -> Result<(), fork_tree::Error<ClientError>>
+    where
+        F: Fn(&B::Hash, &B::Hash) -> Result<bool, ClientError>,
+    {
+        let request = (*best_finalized_hash, best_finalized_number);
+
+        if self.try_finalize_root::<()>(request, Ok(request), false) {
+            return Ok(());
+        }
+
+        if best_finalized_number > self.best_seen_finalized_number {
+            // we receive finality notification only for the finalized branch head.
+            match self.tree.finalize_with_ancestors(
+                best_finalized_hash,
+                best_finalized_number,
+                &is_descendent_of,
+            ) {
+                Err(fork_tree::Error::Revert) => {
+                    // we might have finalized further already in which case we
+                    // will get a `Revert` error which we can safely ignore.
+                }
+                Err(err) => return Err(err),
+                Ok(_) => {}
+            }
+
+            self.best_seen_finalized_number = best_finalized_number;
+        }
+
+        let roots = self.tree.roots().collect::<HashSet<_>>();
+
+        self.pending_requests
+            .retain(|(h, n)| roots.contains(&(h, n, &())));
+        self.active_requests
+            .retain(|_, (h, n)| roots.contains(&(h, n, &())));
+        self.failed_requests
+            .retain(|(h, n), _| roots.contains(&(h, n, &())));
+
+        Ok(())
+    }
+
+    /// Try to finalize pending root.
+    ///
+    /// Returns true if import of this request has been scheduled.
+    pub(crate) fn try_finalize_root<E>(
+        &mut self,
+        request: ExtraRequest<B>,
+        result: Result<ExtraRequest<B>, E>,
+        reschedule_on_failure: bool,
+    ) -> bool {
+        if !self.importing_requests.remove(&request) {
+            return false;
+        }
+
+        let (finalized_hash, finalized_number) = match result {
+            Ok(req) => (req.0, req.1),
+            Err(_) => {
+                if reschedule_on_failure {
+                    self.pending_requests.push_front(request);
+                }
+                return true;
+            }
+        };
+
+        if self.tree.finalize_root(&finalized_hash).is_none() {
+            warn!(target: LOG_TARGET,
+                "‼️ Imported {:?} {:?} which isn't a root in the tree: {:?}",
+                finalized_hash, finalized_number, self.tree.roots().collect::<Vec<_>>()
+            );
+            return true;
+        }
+
+        self.failed_requests.clear();
+        self.active_requests.clear();
+        self.pending_requests.clear();
+        self.pending_requests
+            .extend(self.tree.roots().map(|(&h, &n, _)| (h, n)));
+        self.best_seen_finalized_number = finalized_number;
+
+        true
+    }
+
+    /// Returns an iterator over all active (in-flight) requests and associated peer id.
+    #[cfg(test)]
+    pub(crate) fn active_requests(&self) -> impl Iterator<Item = (&PeerId, &ExtraRequest<B>)> {
+        self.active_requests.iter()
+    }
+
+    /// Returns an iterator over all scheduled pending requests.
+    #[cfg(test)]
+    pub(crate) fn pending_requests(&self) -> impl Iterator<Item = &ExtraRequest<B>> {
+        self.pending_requests.iter()
+    }
+
+    /// Get some key metrics.
+    pub(crate) fn metrics(&self) -> Metrics {
+        Metrics {
+            pending_requests: self
+                .pending_requests
+                .len()
+                .try_into()
+                .unwrap_or(std::u32::MAX),
+            active_requests: self
+                .active_requests
+                .len()
+                .try_into()
+                .unwrap_or(std::u32::MAX),
+            failed_requests: self
+                .failed_requests
+                .len()
+                .try_into()
+                .unwrap_or(std::u32::MAX),
+            importing_requests: self
+                .importing_requests
+                .len()
+                .try_into()
+                .unwrap_or(std::u32::MAX),
+        }
+    }
+}
+
+/// Matches peers with pending extra requests.
+#[derive(Debug)]
+pub(crate) struct Matcher<'a, B: BlockT> {
+    /// Length of pending requests collection.
+    /// Used to ensure we do not loop more than once over all pending requests.
+    remaining: usize,
+    extras: &'a mut ExtraRequests<B>,
+}
+
+impl<'a, B: BlockT> Matcher<'a, B> {
+    fn new(extras: &'a mut ExtraRequests<B>) -> Self {
+        Self {
+            remaining: extras.pending_requests.len(),
+            extras,
+        }
+    }
+
+    /// Finds a peer to which a pending request can be sent.
+    ///
+    /// Peers are filtered according to the current known best block (i.e. we won't
+    /// send an extra request for block #10 to a peer at block #2), and we also
+    /// throttle requests to the same peer if a previous request yielded no results.
+    ///
+    /// This method returns as soon as it finds a peer that should be able to answer
+    /// our request. If no request is pending or no peer can handle it, `None` is
+    /// returned instead.
+    ///
+    /// # Note
+    ///
+    /// The returned `PeerId` (if any) is guaranteed to come from the given `peers`
+    /// argument.
+    pub(crate) fn next(
+        &mut self,
+        peers: &HashMap<PeerId, PeerSync<B>>,
+    ) -> Option<(PeerId, ExtraRequest<B>)> {
+        if self.remaining == 0 {
+            return None;
+        }
+
+        // clean up previously failed requests so we can retry again
+        for requests in self.extras.failed_requests.values_mut() {
+            requests.retain(|(_, instant)| instant.elapsed() < EXTRA_RETRY_WAIT);
+        }
+
+        while let Some(request) = self.extras.pending_requests.pop_front() {
+            for (peer, sync) in peers
+                .iter()
+                .filter(|(_, sync)| sync.state == PeerSyncState::Available)
+            {
+                // only ask peers that have synced at least up to the block number that we're asking
+                // the extra for
+                if sync.best_number < request.1 {
+                    continue;
+                }
+                // don't request to any peers that already have pending requests
+                if self.extras.active_requests.contains_key(peer) {
+                    continue;
+                }
+                // only ask if the same request has not failed for this peer before
+                if self
+                    .extras
+                    .failed_requests
+                    .get(&request)
+                    .map(|rr| rr.iter().any(|i| &i.0 == peer))
+                    .unwrap_or(false)
+                {
+                    continue;
+                }
+                self.extras.active_requests.insert(*peer, request);
+
+                trace!(target: LOG_TARGET,
+                    "Sending {} request to {:?} for {:?}",
+                    self.extras.request_type_name, peer, request,
+                );
+
+                return Some((*peer, request));
+            }
+
+            self.extras.pending_requests.push_back(request);
+            self.remaining -= 1;
+
+            if self.remaining == 0 {
+                break;
+            }
+        }
+
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::strategy::chain_sync::PeerSync;
+    use quickcheck::{Arbitrary, Gen, QuickCheck};
+    use sp_blockchain::Error as ClientError;
+    use sp_test_primitives::{Block, BlockNumber, Hash};
+    use std::collections::{HashMap, HashSet};
+
+    #[test]
+    fn requests_are_processed_in_order() {
+        fn property(mut peers: ArbitraryPeers) {
+            let mut requests = ExtraRequests::<Block>::new("test");
+
+            let num_peers_available = peers
+                .0
+                .values()
+                .filter(|s| s.state == PeerSyncState::Available)
+                .count();
+
+            for i in 0..num_peers_available {
+                requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
+            }
+
+            let pending = requests.pending_requests.clone();
+            let mut m = requests.matcher();
+
+            for p in &pending {
+                let (peer, r) = m.next(&peers.0).unwrap();
+                assert_eq!(p, &r);
+                peers.0.get_mut(&peer).unwrap().state =
+                    PeerSyncState::DownloadingJustification(r.0);
+            }
+        }
+
+        QuickCheck::new().quickcheck(property as fn(ArbitraryPeers))
+    }
+
+    #[test]
+    fn new_roots_schedule_new_request() {
+        fn property(data: Vec<BlockNumber>) {
+            let mut requests = ExtraRequests::<Block>::new("test");
+            for (i, number) in data.into_iter().enumerate() {
+                let hash = [i as u8; 32].into();
+                let pending = requests.pending_requests.len();
+                let is_root = requests
+                    .tree
+                    .roots()
+                    .any(|(&h, &n, _)| hash == h && number == n);
+                requests.schedule((hash, number), |a, b| Ok(a[0] >= b[0]));
+                if !is_root {
+                    assert_eq!(1 + pending, requests.pending_requests.len())
+                }
+            }
+        }
+        QuickCheck::new().quickcheck(property as fn(Vec<BlockNumber>))
+    }
+
+    #[test]
+    fn disconnecting_implies_rescheduling() {
+        fn property(mut peers: ArbitraryPeers) -> bool {
+            let mut requests = ExtraRequests::<Block>::new("test");
+
+            let num_peers_available = peers
+                .0
+                .values()
+                .filter(|s| s.state == PeerSyncState::Available)
+                .count();
+
+            for i in 0..num_peers_available {
+                requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
+            }
+
+            let mut m = requests.matcher();
+            while let Some((peer, r)) = m.next(&peers.0) {
+                peers.0.get_mut(&peer).unwrap().state =
+                    PeerSyncState::DownloadingJustification(r.0);
+            }
+
+            assert!(requests.pending_requests.is_empty());
+
+            let active_peers = requests.active_requests.keys().cloned().collect::<Vec<_>>();
+            let previously_active = requests
+                .active_requests
+                .values()
+                .cloned()
+                .collect::<HashSet<_>>();
+
+            for peer in &active_peers {
+                requests.peer_disconnected(peer)
+            }
+
+            assert!(requests.active_requests.is_empty());
+
+            previously_active
+                == requests
+                    .pending_requests
+                    .iter()
+                    .cloned()
+                    .collect::<HashSet<_>>()
+        }
+
+        QuickCheck::new().quickcheck(property as fn(ArbitraryPeers) -> bool)
+    }
+
+    #[test]
+    fn no_response_reschedules() {
+        fn property(mut peers: ArbitraryPeers) {
+            let mut requests = ExtraRequests::<Block>::new("test");
+
+            let num_peers_available = peers
+                .0
+                .values()
+                .filter(|s| s.state == PeerSyncState::Available)
+                .count();
+
+            for i in 0..num_peers_available {
+                requests.schedule((Hash::random(), i as u64), |a, b| Ok(a[0] >= b[0]))
+            }
+
+            let mut m = requests.matcher();
+            while let Some((peer, r)) = m.next(&peers.0) {
+                peers.0.get_mut(&peer).unwrap().state =
+                    PeerSyncState::DownloadingJustification(r.0);
+            }
+
+            let active = requests
+                .active_requests
+                .iter()
+                .map(|(&p, &r)| (p, r))
+                .collect::<Vec<_>>();
+
+            for (peer, req) in &active {
+                assert!(requests.failed_requests.get(req).is_none());
+                assert!(!requests.pending_requests.contains(req));
+                assert!(requests.on_response::<()>(*peer, None).is_none());
+                assert!(requests.pending_requests.contains(req));
+                assert_eq!(
+                    1,
+                    requests
+                        .failed_requests
+                        .get(req)
+                        .unwrap()
+                        .iter()
+                        .filter(|(p, _)| p == peer)
+                        .count()
+                )
+            }
+        }
+
+        QuickCheck::new().quickcheck(property as fn(ArbitraryPeers))
+    }
+
+    #[test]
+    fn request_is_rescheduled_when_earlier_block_is_finalized() {
+        sp_tracing::try_init_simple();
+
+        let mut finality_proofs = ExtraRequests::<Block>::new("test");
+
+        let hash4 = [4; 32].into();
+        let hash5 = [5; 32].into();
+        let hash6 = [6; 32].into();
+        let hash7 = [7; 32].into();
+
+        fn is_descendent_of(base: &Hash, target: &Hash) -> Result<bool, ClientError> {
+            Ok(target[0] >= base[0])
+        }
+
+        // make #4 last finalized block
+        finality_proofs
+            .tree
+            .import(hash4, 4, (), &is_descendent_of)
+            .unwrap();
+        finality_proofs.tree.finalize_root(&hash4);
+
+        // schedule request for #6
+        finality_proofs.schedule((hash6, 6), is_descendent_of);
+
+        // receive finality proof for #5
+        finality_proofs.importing_requests.insert((hash6, 6));
+        finality_proofs
+            .on_block_finalized(&hash5, 5, is_descendent_of)
+            .unwrap();
+        finality_proofs.try_finalize_root::<()>((hash6, 6), Ok((hash5, 5)), true);
+
+        // ensure that request for #6 is still pending
+        assert_eq!(
+            finality_proofs.pending_requests.iter().collect::<Vec<_>>(),
+            vec![&(hash6, 6)]
+        );
+
+        // receive finality proof for #7
+        finality_proofs.importing_requests.insert((hash6, 6));
+        finality_proofs
+            .on_block_finalized(&hash6, 6, is_descendent_of)
+            .unwrap();
+        finality_proofs
+            .on_block_finalized(&hash7, 7, is_descendent_of)
+            .unwrap();
+        finality_proofs.try_finalize_root::<()>((hash6, 6), Ok((hash7, 7)), true);
+
+        // ensure that there's no request for #6
+        assert_eq!(
+            finality_proofs.pending_requests.iter().collect::<Vec<_>>(),
+            Vec::<&(Hash, u64)>::new()
+        );
+    }
+
+    #[test]
+    fn ancestor_roots_are_finalized_when_finality_notification_is_missed() {
+        let mut finality_proofs = ExtraRequests::<Block>::new("test");
+
+        let hash4 = [4; 32].into();
+        let hash5 = [5; 32].into();
+
+        fn is_descendent_of(base: &Hash, target: &Hash) -> Result<bool, ClientError> {
+            Ok(target[0] >= base[0])
+        }
+
+        // schedule request for #4
+        finality_proofs.schedule((hash4, 4), is_descendent_of);
+
+        // receive finality notification for #5 (missing notification for #4!!!)
+        finality_proofs.importing_requests.insert((hash4, 5));
+        finality_proofs
+            .on_block_finalized(&hash5, 5, is_descendent_of)
+            .unwrap();
+        assert_eq!(finality_proofs.tree.roots().count(), 0);
+    }
+
+    // Some Arbitrary instances to allow easy construction of random peer sets:
+
+    #[derive(Debug, Clone)]
+    struct ArbitraryPeerSyncState(PeerSyncState<Block>);
+
+    impl Arbitrary for ArbitraryPeerSyncState {
+        fn arbitrary(g: &mut Gen) -> Self {
+            let s = match u8::arbitrary(g) % 4 {
+                0 => PeerSyncState::Available,
+                // TODO: 1 => PeerSyncState::AncestorSearch(g.gen(), AncestorSearchState<B>),
+                1 => PeerSyncState::DownloadingNew(BlockNumber::arbitrary(g)),
+                2 => PeerSyncState::DownloadingStale(Hash::random()),
+                _ => PeerSyncState::DownloadingJustification(Hash::random()),
+            };
+            ArbitraryPeerSyncState(s)
+        }
+    }
+
+    #[derive(Debug, Clone)]
+    struct ArbitraryPeerSync(PeerSync<Block>);
+
+    impl Arbitrary for ArbitraryPeerSync {
+        fn arbitrary(g: &mut Gen) -> Self {
+            let ps = PeerSync {
+                peer_id: PeerId::random(),
+                common_number: u64::arbitrary(g),
+                best_hash: Hash::random(),
+                best_number: u64::arbitrary(g),
+                state: ArbitraryPeerSyncState::arbitrary(g).0,
+            };
+            ArbitraryPeerSync(ps)
+        }
+    }
+
+    #[derive(Debug, Clone)]
+    struct ArbitraryPeers(HashMap<PeerId, PeerSync<Block>>);
+
+    impl Arbitrary for ArbitraryPeers {
+        fn arbitrary(g: &mut Gen) -> Self {
+            let mut peers = HashMap::with_capacity(g.size());
+            for _ in 0..g.size() {
+                let ps = ArbitraryPeerSync::arbitrary(g).0;
+                peers.insert(ps.peer_id, ps);
+            }
+            ArbitraryPeers(peers)
+        }
+    }
+}
diff --git a/libp2p/sync/src/futures_stream.rs b/libp2p/sync/src/futures_stream.rs
new file mode 100644
index 000000000..a8e1170b2
--- /dev/null
+++ b/libp2p/sync/src/futures_stream.rs
@@ -0,0 +1,137 @@
+// 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/>.
+
+//! A wrapper for [`FuturesUnordered`] that wakes the task up once a new future is pushed
+//! for it to be polled automatically. It's [`Stream`] never terminates.
+
+use futures::{stream::FuturesUnordered, Future, Stream, StreamExt};
+use std::{
+    pin::Pin,
+    task::{Context, Poll, Waker},
+};
+
+/// Wrapper around [`FuturesUnordered`] that wakes a task up automatically.
+pub struct FuturesStream<F> {
+    futures: FuturesUnordered<F>,
+    waker: Option<Waker>,
+}
+
+/// Surprizingly, `#[derive(Default)]` doesn't work on [`FuturesStream`].
+impl<F> Default for FuturesStream<F> {
+    fn default() -> FuturesStream<F> {
+        FuturesStream {
+            futures: Default::default(),
+            waker: None,
+        }
+    }
+}
+
+impl<F> FuturesStream<F> {
+    /// Push a future for processing.
+    pub fn push(&mut self, future: F) {
+        self.futures.push(future);
+
+        if let Some(waker) = self.waker.take() {
+            waker.wake();
+        }
+    }
+
+    /// The number of futures in the stream.
+    pub fn len(&self) -> usize {
+        self.futures.len()
+    }
+}
+
+impl<F: Future> Stream for FuturesStream<F> {
+    type Item = <F as Future>::Output;
+
+    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        let Poll::Ready(Some(result)) = self.futures.poll_next_unpin(cx) else {
+            self.waker = Some(cx.waker().clone());
+
+            return Poll::Pending;
+        };
+
+        Poll::Ready(Some(result))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use futures::future::{BoxFuture, FutureExt};
+
+    /// [`Stream`] implementation for [`FuturesStream`] relies on the undocumented
+    /// feature that [`FuturesUnordered`] can be polled and repeatedly yield
+    /// `Poll::Ready(None)` before any futures are added into it.
+    #[tokio::test]
+    async fn empty_futures_unordered_can_be_polled() {
+        let mut unordered = FuturesUnordered::<BoxFuture<()>>::default();
+
+        futures::future::poll_fn(|cx| {
+            assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
+            assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
+
+            Poll::Ready(())
+        })
+        .await;
+    }
+
+    /// [`Stream`] implementation for [`FuturesStream`] relies on the undocumented
+    /// feature that [`FuturesUnordered`] can be polled and repeatedly yield
+    /// `Poll::Ready(None)` after all the futures in it have resolved.
+    #[tokio::test]
+    async fn deplenished_futures_unordered_can_be_polled() {
+        let mut unordered = FuturesUnordered::<BoxFuture<()>>::default();
+
+        unordered.push(futures::future::ready(()).boxed());
+        assert_eq!(unordered.next().await, Some(()));
+
+        futures::future::poll_fn(|cx| {
+            assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
+            assert_eq!(unordered.poll_next_unpin(cx), Poll::Ready(None));
+
+            Poll::Ready(())
+        })
+        .await;
+    }
+
+    #[tokio::test]
+    async fn empty_futures_stream_yields_pending() {
+        let mut stream = FuturesStream::<BoxFuture<()>>::default();
+
+        futures::future::poll_fn(|cx| {
+            assert_eq!(stream.poll_next_unpin(cx), Poll::Pending);
+            Poll::Ready(())
+        })
+        .await;
+    }
+
+    #[tokio::test]
+    async fn futures_stream_resolves_futures_and_yields_pending() {
+        let mut stream = FuturesStream::default();
+        stream.push(futures::future::ready(17));
+
+        futures::future::poll_fn(|cx| {
+            assert_eq!(stream.poll_next_unpin(cx), Poll::Ready(Some(17)));
+            assert_eq!(stream.poll_next_unpin(cx), Poll::Pending);
+            Poll::Ready(())
+        })
+        .await;
+    }
+}
diff --git a/libp2p/sync/src/lib.rs b/libp2p/sync/src/lib.rs
new file mode 100644
index 000000000..494e3b87a
--- /dev/null
+++ b/libp2p/sync/src/lib.rs
@@ -0,0 +1,44 @@
+// 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/>.
+
+//! Blockchain syncing implementation in Substrate.
+
+pub use service::syncing_service::SyncingService;
+pub use strategy::warp::{WarpSyncParams, WarpSyncPhase, WarpSyncProgress};
+pub use types::{SyncEvent, SyncEventStream, SyncState, SyncStatus, SyncStatusProvider};
+
+mod block_announce_validator;
+mod extra_requests;
+mod futures_stream;
+mod pending_responses;
+mod request_metrics;
+mod schema;
+mod types;
+
+pub mod block_relay_protocol;
+pub mod block_request_handler;
+pub mod blocks;
+pub mod engine;
+pub mod mock;
+pub mod service;
+pub mod state_request_handler;
+pub mod strategy;
+pub mod warp_request_handler;
+
+/// Log target for this crate.
+const LOG_TARGET: &str = "sync";
diff --git a/libp2p/sync/src/mock.rs b/libp2p/sync/src/mock.rs
new file mode 100644
index 000000000..fbd83caa4
--- /dev/null
+++ b/libp2p/sync/src/mock.rs
@@ -0,0 +1,45 @@
+// 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/>.
+
+//! Contains mock implementations of `ChainSync` and 'BlockDownloader'.
+
+use crate::block_relay_protocol::{BlockDownloader as BlockDownloaderT, BlockResponseError};
+
+use futures::channel::oneshot;
+use libp2p::PeerId;
+use sc_network::{ProtocolName, RequestFailure};
+use sc_network_common::sync::message::{BlockData, BlockRequest};
+use sp_runtime::traits::Block as BlockT;
+
+mockall::mock! {
+    pub BlockDownloader<Block: BlockT> {}
+
+    #[async_trait::async_trait]
+    impl<Block: BlockT> BlockDownloaderT<Block> for BlockDownloader<Block> {
+        async fn download_blocks(
+            &self,
+            who: PeerId,
+            request: BlockRequest<Block>,
+        ) -> Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>;
+        fn block_response_into_blocks(
+            &self,
+            request: &BlockRequest<Block>,
+            response: Vec<u8>,
+        ) -> Result<Vec<BlockData<Block>>, BlockResponseError>;
+    }
+}
diff --git a/libp2p/sync/src/pending_responses.rs b/libp2p/sync/src/pending_responses.rs
new file mode 100644
index 000000000..0c2b107fe
--- /dev/null
+++ b/libp2p/sync/src/pending_responses.rs
@@ -0,0 +1,136 @@
+// 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/>.
+
+//! [`PendingResponses`] is responsible for keeping track of pending responses and
+//! polling them. [`Stream`] implemented by [`PendingResponses`] never terminates.
+
+use crate::{types::PeerRequest, LOG_TARGET};
+use futures::{
+    channel::oneshot,
+    future::BoxFuture,
+    stream::{BoxStream, FusedStream, Stream},
+    FutureExt, StreamExt,
+};
+use libp2p::PeerId;
+use log::error;
+use sc_network::{request_responses::RequestFailure, types::ProtocolName};
+use sp_runtime::traits::Block as BlockT;
+use std::task::{Context, Poll, Waker};
+use tokio_stream::StreamMap;
+
+/// Response result.
+type ResponseResult = Result<Result<(Vec<u8>, ProtocolName), RequestFailure>, oneshot::Canceled>;
+
+/// A future yielding [`ResponseResult`].
+type ResponseFuture = BoxFuture<'static, ResponseResult>;
+
+/// An event we receive once a pending response future resolves.
+pub(crate) struct ResponseEvent<B: BlockT> {
+    pub peer_id: PeerId,
+    pub request: PeerRequest<B>,
+    pub response: ResponseResult,
+}
+
+/// Stream taking care of polling pending responses.
+pub(crate) struct PendingResponses<B: BlockT> {
+    /// Pending responses
+    pending_responses: StreamMap<PeerId, BoxStream<'static, (PeerRequest<B>, ResponseResult)>>,
+    /// Waker to implement never terminating stream
+    waker: Option<Waker>,
+}
+
+impl<B: BlockT> PendingResponses<B> {
+    pub fn new() -> Self {
+        Self {
+            pending_responses: StreamMap::new(),
+            waker: None,
+        }
+    }
+
+    pub fn insert(
+        &mut self,
+        peer_id: PeerId,
+        request: PeerRequest<B>,
+        response_future: ResponseFuture,
+    ) {
+        let request_type = request.get_type();
+
+        if self
+            .pending_responses
+            .insert(
+                peer_id,
+                Box::pin(async move { (request, response_future.await) }.into_stream()),
+            )
+            .is_some()
+        {
+            error!(
+                target: LOG_TARGET,
+                "Discarded pending response from peer {peer_id}, request type: {request_type:?}.",
+            );
+            debug_assert!(false);
+        }
+
+        if let Some(waker) = self.waker.take() {
+            waker.wake();
+        }
+    }
+
+    pub fn remove(&mut self, peer_id: &PeerId) -> bool {
+        self.pending_responses.remove(peer_id).is_some()
+    }
+
+    pub fn len(&self) -> usize {
+        self.pending_responses.len()
+    }
+}
+
+impl<B: BlockT> Stream for PendingResponses<B> {
+    type Item = ResponseEvent<B>;
+
+    fn poll_next(
+        mut self: std::pin::Pin<&mut Self>,
+        cx: &mut Context<'_>,
+    ) -> Poll<Option<Self::Item>> {
+        match self.pending_responses.poll_next_unpin(cx) {
+            Poll::Ready(Some((peer_id, (request, response)))) => {
+                // We need to manually remove the stream, because `StreamMap` doesn't know yet that
+                // it's going to yield `None`, so may not remove it before the next request is made
+                // to the same peer.
+                self.pending_responses.remove(&peer_id);
+
+                Poll::Ready(Some(ResponseEvent {
+                    peer_id,
+                    request,
+                    response,
+                }))
+            }
+            Poll::Ready(None) | Poll::Pending => {
+                self.waker = Some(cx.waker().clone());
+
+                Poll::Pending
+            }
+        }
+    }
+}
+
+// As [`PendingResponses`] never terminates, we can easily implement [`FusedStream`] for it.
+impl<B: BlockT> FusedStream for PendingResponses<B> {
+    fn is_terminated(&self) -> bool {
+        false
+    }
+}
diff --git a/libp2p/sync/src/request_metrics.rs b/libp2p/sync/src/request_metrics.rs
new file mode 100644
index 000000000..f9bf4573c
--- /dev/null
+++ b/libp2p/sync/src/request_metrics.rs
@@ -0,0 +1,25 @@
+// 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/>.
+
+#[derive(Debug)]
+pub struct Metrics {
+    pub pending_requests: u32,
+    pub active_requests: u32,
+    pub importing_requests: u32,
+    pub failed_requests: u32,
+}
diff --git a/libp2p/sync/src/schema.rs b/libp2p/sync/src/schema.rs
new file mode 100644
index 000000000..a182a891a
--- /dev/null
+++ b/libp2p/sync/src/schema.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+//! Include sources generated from protobuf definitions.
+
+pub(crate) mod v1 {
+    include!(concat!(env!("OUT_DIR"), "/api.v1.rs"));
+}
diff --git a/libp2p/sync/src/schema/api.v1.proto b/libp2p/sync/src/schema/api.v1.proto
new file mode 100644
index 000000000..1490f61a4
--- /dev/null
+++ b/libp2p/sync/src/schema/api.v1.proto
@@ -0,0 +1,106 @@
+// Schema definition for block request/response messages.
+
+syntax = "proto3";
+
+package api.v1;
+
+// Block enumeration direction.
+enum Direction {
+	// Enumerate in ascending order (from child to parent).
+	Ascending = 0;
+	// Enumerate in descending order (from parent to canonical child).
+	Descending = 1;
+}
+
+// Request block data from a peer.
+message BlockRequest {
+	// Bits of block data to request.
+	uint32 fields = 1;
+	// Start from this block.
+	oneof from_block {
+		// Start with given hash.
+		bytes hash = 2;
+		// Start with given block number.
+		bytes number = 3;
+	}
+	// Sequence direction.
+	// If missing, should be interpreted as "Ascending".
+	Direction direction = 5;
+	// Maximum number of blocks to return. An implementation defined maximum is used when unspecified.
+	uint32 max_blocks = 6; // optional
+	// Indicate to the receiver that we support multiple justifications. If the responder also
+	// supports this it will populate the multiple justifications field in `BlockData` instead of
+	// the single justification field.
+	bool support_multiple_justifications = 7; // optional
+}
+
+// Response to `BlockRequest`
+message BlockResponse {
+	// Block data for the requested sequence.
+	repeated BlockData blocks = 1;
+}
+
+// Block data sent in the response.
+message BlockData {
+	// Block header hash.
+	bytes hash = 1;
+	// Block header if requested.
+	bytes header = 2; // optional
+	// Block body if requested.
+	repeated bytes body = 3; // optional
+	// Block receipt if requested.
+	bytes receipt = 4; // optional
+	// Block message queue if requested.
+	bytes message_queue = 5; // optional
+	// Justification if requested.
+	bytes justification = 6; // optional
+	// True if justification should be treated as present but empty.
+	// This hack is unfortunately necessary because shortcomings in the protobuf format otherwise
+	// doesn't make in possible to differentiate between a lack of justification and an empty
+	// justification.
+	bool is_empty_justification = 7; // optional, false if absent
+	// Justifications if requested.
+	// Unlike the field for a single justification, this field does not required an associated
+	// boolean to differentiate between the lack of justifications and empty justification(s). This
+	// is because empty justifications, like all justifications, are paired with a non-empty
+	// consensus engine ID.
+	bytes justifications = 8; // optional
+	// Indexed block body if requestd.
+	repeated bytes indexed_body = 9; // optional
+}
+
+// Request storage data from a peer.
+message StateRequest {
+	// Block header hash.
+	bytes block = 1;
+	// Start from this key.
+	// Multiple keys used for nested state start.
+	repeated bytes start = 2; // optional
+	// if 'true' indicates that response should contain raw key-values, rather than proof.
+	bool no_proof = 3;
+}
+
+message StateResponse {
+	// A collection of keys-values states. Only populated if `no_proof` is `true`
+	repeated KeyValueStateEntry entries = 1;
+	// If `no_proof` is false in request, this contains proof nodes.
+	bytes proof = 2;
+}
+
+// A key value state.
+message KeyValueStateEntry {
+	// Root of for this level, empty length bytes
+	// if top level.
+	bytes state_root = 1;
+	// A collection of keys-values.
+	repeated StateEntry entries = 2;
+	// Set to true when there are no more keys to return.
+	bool complete = 3;
+}
+
+// A key-value pair.
+message StateEntry {
+	bytes key = 1;
+	bytes value = 2;
+}
+
diff --git a/libp2p/sync/src/service/mock.rs b/libp2p/sync/src/service/mock.rs
new file mode 100644
index 000000000..b48b0fa42
--- /dev/null
+++ b/libp2p/sync/src/service/mock.rs
@@ -0,0 +1,142 @@
+// 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 futures::channel::oneshot;
+use libp2p::{Multiaddr, PeerId};
+
+use sc_network::{
+    config::MultiaddrWithPeerId,
+    request_responses::{IfDisconnected, RequestFailure},
+    types::ProtocolName,
+    NetworkNotification, NetworkPeers, NetworkRequest, NetworkSyncForkRequest,
+    NotificationSenderError, NotificationSenderT, ReputationChange,
+};
+use sc_network_common::role::ObservedRole;
+use sp_runtime::traits::{Block as BlockT, NumberFor};
+
+use std::collections::HashSet;
+
+mockall::mock! {
+    pub ChainSyncInterface<B: BlockT> {
+        pub fn justification_sync_link_request_justification(&self, hash: &B::Hash, number: NumberFor<B>);
+        pub fn justification_sync_link_clear_justification_requests(&self);
+    }
+
+    impl<B: BlockT + 'static> NetworkSyncForkRequest<B::Hash, NumberFor<B>>
+        for ChainSyncInterface<B>
+    {
+        fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: B::Hash, number: NumberFor<B>);
+    }
+
+    impl<B: BlockT> sc_consensus::Link<B> for ChainSyncInterface<B> {
+        fn blocks_processed(
+            &mut self,
+            imported: usize,
+            count: usize,
+            results: Vec<crate::service::syncing_service::ProcessedBlock<B, B::Hash>>,
+        );
+        fn justification_imported(
+            &mut self,
+            who: PeerId,
+            hash: &B::Hash,
+            number: NumberFor<B>,
+            success: bool,
+        );
+        fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>);
+    }
+}
+
+impl<B: BlockT> sc_consensus::JustificationSyncLink<B> for MockChainSyncInterface<B> {
+    fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
+        self.justification_sync_link_request_justification(hash, number);
+    }
+
+    fn clear_justification_requests(&self) {
+        self.justification_sync_link_clear_justification_requests();
+    }
+}
+
+mockall::mock! {
+    pub NetworkServiceHandle {}
+}
+
+// Mocked `Network` for `ChainSync`-related tests
+mockall::mock! {
+    pub Network {}
+
+    impl NetworkPeers for Network {
+        fn set_authorized_peers(&self, peers: HashSet<PeerId>);
+        fn set_authorized_only(&self, reserved_only: bool);
+        fn add_known_address(&self, peer_id: PeerId, addr: Multiaddr);
+        fn report_peer(&self, peer_id: PeerId, cost_benefit: ReputationChange);
+        fn peer_reputation(&self, peer_id: &PeerId) -> i32;
+        fn disconnect_peer(&self, peer_id: PeerId, protocol: ProtocolName);
+        fn accept_unreserved_peers(&self);
+        fn deny_unreserved_peers(&self);
+        fn add_reserved_peer(&self, peer: MultiaddrWithPeerId) -> Result<(), String>;
+        fn remove_reserved_peer(&self, peer_id: PeerId);
+        fn set_reserved_peers(
+            &self,
+            protocol: ProtocolName,
+            peers: HashSet<Multiaddr>,
+        ) -> Result<(), String>;
+        fn add_peers_to_reserved_set(
+            &self,
+            protocol: ProtocolName,
+            peers: HashSet<Multiaddr>,
+        ) -> Result<(), String>;
+        fn remove_peers_from_reserved_set(
+            &self,
+            protocol: ProtocolName,
+            peers: Vec<PeerId>
+        ) -> Result<(), String>;
+        fn sync_num_connected(&self) -> usize;
+        fn peer_role(&self, peer_id: PeerId, handshake: Vec<u8>) -> Option<ObservedRole>;
+    }
+
+    #[async_trait::async_trait]
+    impl NetworkRequest for Network {
+        async fn request(
+            &self,
+            target: PeerId,
+            protocol: ProtocolName,
+            request: Vec<u8>,
+            fallback_request: Option<(Vec<u8>, ProtocolName)>,
+            connect: IfDisconnected,
+        ) -> Result<(Vec<u8>, ProtocolName), RequestFailure>;
+        fn start_request(
+            &self,
+            target: PeerId,
+            protocol: ProtocolName,
+            request: Vec<u8>,
+            fallback_request: Option<(Vec<u8>, ProtocolName)>,
+            tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+            connect: IfDisconnected,
+        );
+    }
+
+    impl NetworkNotification for Network {
+        fn write_notification(&self, target: PeerId, protocol: ProtocolName, message: Vec<u8>);
+        fn notification_sender(
+            &self,
+            target: PeerId,
+            protocol: ProtocolName,
+        ) -> Result<Box<dyn NotificationSenderT>, NotificationSenderError>;
+        fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>);
+    }
+}
diff --git a/libp2p/sync/src/service/mod.rs b/libp2p/sync/src/service/mod.rs
new file mode 100644
index 000000000..d045af26e
--- /dev/null
+++ b/libp2p/sync/src/service/mod.rs
@@ -0,0 +1,23 @@
+// 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/>.
+
+//! `SyncingEngine`-related service code
+
+pub mod mock;
+pub mod network;
+pub mod syncing_service;
diff --git a/libp2p/sync/src/service/network.rs b/libp2p/sync/src/service/network.rs
new file mode 100644
index 000000000..ee79f989d
--- /dev/null
+++ b/libp2p/sync/src/service/network.rs
@@ -0,0 +1,193 @@
+// 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 futures::{channel::oneshot, StreamExt};
+use libp2p::PeerId;
+
+use sc_network::{
+    request_responses::{IfDisconnected, RequestFailure},
+    types::ProtocolName,
+    NetworkNotification, NetworkPeers, NetworkRequest, ReputationChange,
+};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+
+use std::sync::Arc;
+
+/// Network-related services required by `sc-network-sync`
+pub trait Network: NetworkPeers + NetworkRequest + NetworkNotification {}
+
+impl<T> Network for T where T: NetworkPeers + NetworkRequest + NetworkNotification {}
+
+/// Network service provider for `ChainSync`
+///
+/// It runs as an asynchronous task and listens to commands coming from `ChainSync` and
+/// calls the `NetworkService` on its behalf.
+pub struct NetworkServiceProvider {
+    rx: TracingUnboundedReceiver<ToServiceCommand>,
+}
+
+/// Commands that `ChainSync` wishes to send to `NetworkService`
+pub enum ToServiceCommand {
+    /// Call `NetworkPeers::disconnect_peer()`
+    DisconnectPeer(PeerId, ProtocolName),
+
+    /// Call `NetworkPeers::report_peer()`
+    ReportPeer(PeerId, ReputationChange),
+
+    /// Call `NetworkRequest::start_request()`
+    StartRequest(
+        PeerId,
+        ProtocolName,
+        Vec<u8>,
+        oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        IfDisconnected,
+    ),
+
+    /// Call `NetworkNotification::write_notification()`
+    WriteNotification(PeerId, ProtocolName, Vec<u8>),
+
+    /// Call `NetworkNotification::set_notification_handshake()`
+    SetNotificationHandshake(ProtocolName, Vec<u8>),
+}
+
+/// Handle that is (temporarily) passed to `ChainSync` so it can
+/// communicate with `NetworkService` through `SyncingEngine`
+#[derive(Clone)]
+pub struct NetworkServiceHandle {
+    tx: TracingUnboundedSender<ToServiceCommand>,
+}
+
+impl NetworkServiceHandle {
+    /// Create new service handle
+    pub fn new(tx: TracingUnboundedSender<ToServiceCommand>) -> NetworkServiceHandle {
+        Self { tx }
+    }
+
+    /// Report peer
+    pub fn report_peer(&self, who: PeerId, cost_benefit: ReputationChange) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::ReportPeer(who, cost_benefit));
+    }
+
+    /// Disconnect peer
+    pub fn disconnect_peer(&self, who: PeerId, protocol: ProtocolName) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::DisconnectPeer(who, protocol));
+    }
+
+    /// Send request to peer
+    pub fn start_request(
+        &self,
+        who: PeerId,
+        protocol: ProtocolName,
+        request: Vec<u8>,
+        tx: oneshot::Sender<Result<(Vec<u8>, ProtocolName), RequestFailure>>,
+        connect: IfDisconnected,
+    ) {
+        let _ = self.tx.unbounded_send(ToServiceCommand::StartRequest(
+            who, protocol, request, tx, connect,
+        ));
+    }
+
+    /// Send notification to peer
+    pub fn write_notification(&self, who: PeerId, protocol: ProtocolName, message: Vec<u8>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::WriteNotification(who, protocol, message));
+    }
+
+    /// Set handshake for the notification protocol.
+    pub fn set_notification_handshake(&self, protocol: ProtocolName, handshake: Vec<u8>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::SetNotificationHandshake(
+                protocol, handshake,
+            ));
+    }
+}
+
+impl NetworkServiceProvider {
+    /// Create new `NetworkServiceProvider`
+    pub fn new() -> (Self, NetworkServiceHandle) {
+        let (tx, rx) = tracing_unbounded("mpsc_network_service_provider", 100_000);
+
+        (Self { rx }, NetworkServiceHandle::new(tx))
+    }
+
+    /// Run the `NetworkServiceProvider`
+    pub async fn run(mut self, service: Arc<dyn Network + Send + Sync>) {
+        while let Some(inner) = self.rx.next().await {
+            match inner {
+                ToServiceCommand::DisconnectPeer(peer, protocol_name) => {
+                    service.disconnect_peer(peer, protocol_name)
+                }
+                ToServiceCommand::ReportPeer(peer, reputation_change) => {
+                    service.report_peer(peer, reputation_change)
+                }
+                ToServiceCommand::StartRequest(peer, protocol, request, tx, connect) => {
+                    service.start_request(peer, protocol, request, None, tx, connect)
+                }
+                ToServiceCommand::WriteNotification(peer, protocol, message) => {
+                    service.write_notification(peer, protocol, message)
+                }
+                ToServiceCommand::SetNotificationHandshake(protocol, handshake) => {
+                    service.set_notification_handshake(protocol, handshake)
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::service::mock::MockNetwork;
+
+    // typical pattern in `Protocol` code where peer is disconnected
+    // and then reported
+    #[tokio::test]
+    async fn disconnect_and_report_peer() {
+        let (provider, handle) = NetworkServiceProvider::new();
+
+        let peer = PeerId::random();
+        let proto = ProtocolName::from("test-protocol");
+        let proto_clone = proto.clone();
+        let change = sc_network::ReputationChange::new_fatal("test-change");
+
+        let mut mock_network = MockNetwork::new();
+        mock_network
+            .expect_disconnect_peer()
+            .withf(move |in_peer, in_proto| &peer == in_peer && &proto == in_proto)
+            .once()
+            .returning(|_, _| ());
+        mock_network
+            .expect_report_peer()
+            .withf(move |in_peer, in_change| &peer == in_peer && &change == in_change)
+            .once()
+            .returning(|_, _| ());
+
+        tokio::spawn(async move {
+            provider.run(Arc::new(mock_network)).await;
+        });
+
+        handle.disconnect_peer(peer, proto_clone);
+        handle.report_peer(peer, change);
+    }
+}
diff --git a/libp2p/sync/src/service/syncing_service.rs b/libp2p/sync/src/service/syncing_service.rs
new file mode 100644
index 000000000..2225af5c0
--- /dev/null
+++ b/libp2p/sync/src/service/syncing_service.rs
@@ -0,0 +1,282 @@
+// 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::types::{ExtendedPeerInfo, SyncEvent, SyncEventStream, SyncStatus, SyncStatusProvider};
+
+use futures::{channel::oneshot, Stream};
+use libp2p::PeerId;
+
+use sc_consensus::{BlockImportError, BlockImportStatus, JustificationSyncLink, Link};
+use sc_network::{NetworkBlock, NetworkSyncForkRequest};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
+use sp_runtime::traits::{Block as BlockT, NumberFor};
+
+use std::{
+    pin::Pin,
+    sync::{
+        atomic::{AtomicBool, AtomicUsize, Ordering},
+        Arc,
+    },
+};
+
+pub type ProcessedBlock<B, C> = (Result<BlockImportStatus<NumberFor<B>>, BlockImportError>, C);
+
+/// Commands send to `SyncingEngine`
+pub enum ToServiceCommand<B: BlockT> {
+    SetSyncForkRequest(Vec<PeerId>, B::Hash, NumberFor<B>),
+    RequestJustification(B::Hash, NumberFor<B>),
+    ClearJustificationRequests,
+    BlocksProcessed(usize, usize, Vec<ProcessedBlock<B, B::Hash>>),
+    JustificationImported(PeerId, B::Hash, NumberFor<B>, bool),
+    AnnounceBlock(B::Hash, Option<Vec<u8>>),
+    NewBestBlockImported(B::Hash, NumberFor<B>),
+    EventStream(TracingUnboundedSender<SyncEvent>),
+    Status(oneshot::Sender<SyncStatus<B>>),
+    NumActivePeers(oneshot::Sender<usize>),
+    SyncState(oneshot::Sender<SyncStatus<B>>),
+    BestSeenBlock(oneshot::Sender<Option<NumberFor<B>>>),
+    NumSyncPeers(oneshot::Sender<u32>),
+    NumQueuedBlocks(oneshot::Sender<u32>),
+    NumDownloadedBlocks(oneshot::Sender<usize>),
+    NumSyncRequests(oneshot::Sender<usize>),
+    PeersInfo(oneshot::Sender<Vec<(PeerId, ExtendedPeerInfo<B>)>>),
+    OnBlockFinalized(B::Hash, B::Header),
+    // Status {
+    // 	pending_response: oneshot::Sender<SyncStatus<B>>,
+    // },
+}
+
+/// Handle for communicating with `SyncingEngine` asynchronously
+#[derive(Clone)]
+pub struct SyncingService<B: BlockT> {
+    tx: TracingUnboundedSender<ToServiceCommand<B>>,
+    /// Number of peers we're connected to.
+    num_connected: Arc<AtomicUsize>,
+    /// Are we actively catching up with the chain?
+    is_major_syncing: Arc<AtomicBool>,
+}
+
+impl<B: BlockT> SyncingService<B> {
+    /// Create new handle
+    pub fn new(
+        tx: TracingUnboundedSender<ToServiceCommand<B>>,
+        num_connected: Arc<AtomicUsize>,
+        is_major_syncing: Arc<AtomicBool>,
+    ) -> Self {
+        Self {
+            tx,
+            num_connected,
+            is_major_syncing,
+        }
+    }
+
+    /// Get the number of active peers.
+    pub async fn num_active_peers(&self) -> Result<usize, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self.tx.unbounded_send(ToServiceCommand::NumActivePeers(tx));
+
+        rx.await
+    }
+
+    /// Get best seen block.
+    pub async fn best_seen_block(&self) -> Result<Option<NumberFor<B>>, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self.tx.unbounded_send(ToServiceCommand::BestSeenBlock(tx));
+
+        rx.await
+    }
+
+    /// Get the number of sync peers.
+    pub async fn num_sync_peers(&self) -> Result<u32, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self.tx.unbounded_send(ToServiceCommand::NumSyncPeers(tx));
+
+        rx.await
+    }
+
+    /// Get the number of queued blocks.
+    pub async fn num_queued_blocks(&self) -> Result<u32, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::NumQueuedBlocks(tx));
+
+        rx.await
+    }
+
+    /// Get the number of downloaded blocks.
+    pub async fn num_downloaded_blocks(&self) -> Result<usize, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::NumDownloadedBlocks(tx));
+
+        rx.await
+    }
+
+    /// Get the number of sync requests.
+    pub async fn num_sync_requests(&self) -> Result<usize, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::NumSyncRequests(tx));
+
+        rx.await
+    }
+
+    /// Get peer information.
+    pub async fn peers_info(
+        &self,
+    ) -> Result<Vec<(PeerId, ExtendedPeerInfo<B>)>, oneshot::Canceled> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self.tx.unbounded_send(ToServiceCommand::PeersInfo(tx));
+
+        rx.await
+    }
+
+    /// Notify the `SyncingEngine` that a block has been finalized.
+    pub fn on_block_finalized(&self, hash: B::Hash, header: B::Header) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::OnBlockFinalized(hash, header));
+    }
+
+    /// Get sync status
+    ///
+    /// Returns an error if `SyncingEngine` has terminated.
+    pub async fn status(&self) -> Result<SyncStatus<B>, ()> {
+        let (tx, rx) = oneshot::channel();
+        let _ = self.tx.unbounded_send(ToServiceCommand::Status(tx));
+
+        rx.await.map_err(|_| ())
+    }
+}
+
+impl<B: BlockT + 'static> NetworkSyncForkRequest<B::Hash, NumberFor<B>> for SyncingService<B> {
+    /// Configure an explicit fork sync request.
+    ///
+    /// Note that this function should not be used for recent blocks.
+    /// Sync should be able to download all the recent forks normally.
+    /// `set_sync_fork_request` should only be used if external code detects that there's
+    /// a stale fork missing.
+    ///
+    /// Passing empty `peers` set effectively removes the sync request.
+    fn set_sync_fork_request(&self, peers: Vec<PeerId>, hash: B::Hash, number: NumberFor<B>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::SetSyncForkRequest(peers, hash, number));
+    }
+}
+
+impl<B: BlockT> JustificationSyncLink<B> for SyncingService<B> {
+    /// Request a justification for the given block from the network.
+    ///
+    /// On success, the justification will be passed to the import queue that was part at
+    /// initialization as part of the configuration.
+    fn request_justification(&self, hash: &B::Hash, number: NumberFor<B>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::RequestJustification(*hash, number));
+    }
+
+    fn clear_justification_requests(&self) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::ClearJustificationRequests);
+    }
+}
+
+#[async_trait::async_trait]
+impl<B: BlockT> SyncStatusProvider<B> for SyncingService<B> {
+    /// Get high-level view of the syncing status.
+    async fn status(&self) -> Result<SyncStatus<B>, ()> {
+        let (rtx, rrx) = oneshot::channel();
+
+        let _ = self.tx.unbounded_send(ToServiceCommand::Status(rtx));
+        rrx.await.map_err(|_| ())
+    }
+}
+
+impl<B: BlockT> Link<B> for SyncingService<B> {
+    fn blocks_processed(
+        &mut self,
+        imported: usize,
+        count: usize,
+        results: Vec<(
+            Result<BlockImportStatus<NumberFor<B>>, BlockImportError>,
+            B::Hash,
+        )>,
+    ) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::BlocksProcessed(imported, count, results));
+    }
+
+    fn justification_imported(
+        &mut self,
+        who: PeerId,
+        hash: &B::Hash,
+        number: NumberFor<B>,
+        success: bool,
+    ) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::JustificationImported(
+                who, *hash, number, success,
+            ));
+    }
+
+    fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::RequestJustification(*hash, number));
+    }
+}
+
+impl<B: BlockT> SyncEventStream for SyncingService<B> {
+    /// Get syncing event stream.
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>> {
+        let (tx, rx) = tracing_unbounded(name, 100_000);
+        let _ = self.tx.unbounded_send(ToServiceCommand::EventStream(tx));
+        Box::pin(rx)
+    }
+}
+
+impl<B: BlockT> NetworkBlock<B::Hash, NumberFor<B>> for SyncingService<B> {
+    fn announce_block(&self, hash: B::Hash, data: Option<Vec<u8>>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::AnnounceBlock(hash, data));
+    }
+
+    fn new_best_block_imported(&self, hash: B::Hash, number: NumberFor<B>) {
+        let _ = self
+            .tx
+            .unbounded_send(ToServiceCommand::NewBestBlockImported(hash, number));
+    }
+}
+
+impl<B: BlockT> sp_consensus::SyncOracle for SyncingService<B> {
+    fn is_major_syncing(&self) -> bool {
+        self.is_major_syncing.load(Ordering::Relaxed)
+    }
+
+    fn is_offline(&self) -> bool {
+        self.num_connected.load(Ordering::Relaxed) == 0
+    }
+}
diff --git a/libp2p/sync/src/state_request_handler.rs b/libp2p/sync/src/state_request_handler.rs
new file mode 100644
index 000000000..488ec4b05
--- /dev/null
+++ b/libp2p/sync/src/state_request_handler.rs
@@ -0,0 +1,312 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Helper for handling (i.e. answering) state requests from a remote peer via the
+//! `crate::request_responses::RequestResponsesBehaviour`.
+
+use crate::{
+    schema::v1::{KeyValueStateEntry, StateEntry, StateRequest, StateResponse},
+    LOG_TARGET,
+};
+
+use codec::{Decode, Encode};
+use futures::{channel::oneshot, stream::StreamExt};
+use libp2p::PeerId;
+use log::{debug, trace};
+use prost::Message;
+use schnellru::{ByLength, LruMap};
+
+use sc_client_api::{BlockBackend, ProofProvider};
+use sc_network::{
+    config::ProtocolId,
+    request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
+};
+use sp_runtime::traits::Block as BlockT;
+
+use std::{
+    hash::{Hash, Hasher},
+    sync::Arc,
+    time::Duration,
+};
+
+const MAX_RESPONSE_BYTES: usize = 2 * 1024 * 1024; // Actual reponse may be bigger.
+const MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER: usize = 2;
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+
+    /// Reputation change when a peer sent us the same request multiple times.
+    pub const SAME_REQUEST: Rep = Rep::new(i32::MIN, "Same state request multiple times");
+}
+
+/// Generates a [`ProtocolConfig`] for the state request protocol, refusing incoming requests.
+pub fn generate_protocol_config<Hash: AsRef<[u8]>>(
+    protocol_id: &ProtocolId,
+    genesis_hash: Hash,
+    fork_id: Option<&str>,
+) -> ProtocolConfig {
+    ProtocolConfig {
+        name: generate_protocol_name(genesis_hash, fork_id).into(),
+        fallback_names: std::iter::once(generate_legacy_protocol_name(protocol_id).into())
+            .collect(),
+        max_request_size: 1024 * 1024,
+        max_response_size: 16 * 1024 * 1024,
+        request_timeout: Duration::from_secs(40),
+        inbound_queue: None,
+    }
+}
+
+/// Generate the state protocol name from the genesis hash and fork id.
+fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
+    let genesis_hash = genesis_hash.as_ref();
+    if let Some(fork_id) = fork_id {
+        format!(
+            "/{}/{}/state/2",
+            array_bytes::bytes2hex("", genesis_hash),
+            fork_id
+        )
+    } else {
+        format!("/{}/state/2", array_bytes::bytes2hex("", genesis_hash))
+    }
+}
+
+/// Generate the legacy state protocol name from chain specific protocol identifier.
+fn generate_legacy_protocol_name(protocol_id: &ProtocolId) -> String {
+    format!("/{}/state/2", protocol_id.as_ref())
+}
+
+/// The key of [`BlockRequestHandler::seen_requests`].
+#[derive(Eq, PartialEq, Clone)]
+struct SeenRequestsKey<B: BlockT> {
+    peer: PeerId,
+    block: B::Hash,
+    start: Vec<Vec<u8>>,
+}
+
+#[allow(clippy::derived_hash_with_manual_eq)]
+impl<B: BlockT> Hash for SeenRequestsKey<B> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.peer.hash(state);
+        self.block.hash(state);
+        self.start.hash(state);
+    }
+}
+
+/// The value of [`StateRequestHandler::seen_requests`].
+enum SeenRequestsValue {
+    /// First time we have seen the request.
+    First,
+    /// We have fulfilled the request `n` times.
+    Fulfilled(usize),
+}
+
+/// Handler for incoming block requests from a remote peer.
+pub struct StateRequestHandler<B: BlockT, Client> {
+    client: Arc<Client>,
+    request_receiver: async_channel::Receiver<IncomingRequest>,
+    /// Maps from request to number of times we have seen this request.
+    ///
+    /// This is used to check if a peer is spamming us with the same request.
+    seen_requests: LruMap<SeenRequestsKey<B>, SeenRequestsValue>,
+}
+
+impl<B, Client> StateRequestHandler<B, Client>
+where
+    B: BlockT,
+    Client: BlockBackend<B> + ProofProvider<B> + Send + Sync + 'static,
+{
+    /// Create a new [`StateRequestHandler`].
+    pub fn new(
+        protocol_id: &ProtocolId,
+        fork_id: Option<&str>,
+        client: Arc<Client>,
+        num_peer_hint: usize,
+    ) -> (Self, ProtocolConfig) {
+        // Reserve enough request slots for one request per peer when we are at the maximum
+        // number of peers.
+        let capacity = std::cmp::max(num_peer_hint, 1);
+        let (tx, request_receiver) = async_channel::bounded(capacity);
+
+        let mut protocol_config = generate_protocol_config(
+            protocol_id,
+            client
+                .block_hash(0u32.into())
+                .ok()
+                .flatten()
+                .expect("Genesis block exists; qed"),
+            fork_id,
+        );
+        protocol_config.inbound_queue = Some(tx);
+
+        let capacity = ByLength::new(num_peer_hint.max(1) as u32 * 2);
+        let seen_requests = LruMap::new(capacity);
+
+        (
+            Self {
+                client,
+                request_receiver,
+                seen_requests,
+            },
+            protocol_config,
+        )
+    }
+
+    /// Run [`StateRequestHandler`].
+    pub async fn run(mut self) {
+        while let Some(request) = self.request_receiver.next().await {
+            let IncomingRequest {
+                peer,
+                payload,
+                pending_response,
+            } = request;
+
+            match self.handle_request(payload, pending_response, &peer) {
+                Ok(()) => debug!(target: LOG_TARGET, "Handled block request from {}.", peer),
+                Err(e) => debug!(
+                    target: LOG_TARGET,
+                    "Failed to handle state request from {}: {}", peer, e,
+                ),
+            }
+        }
+    }
+
+    fn handle_request(
+        &mut self,
+        payload: Vec<u8>,
+        pending_response: oneshot::Sender<OutgoingResponse>,
+        peer: &PeerId,
+    ) -> Result<(), HandleRequestError> {
+        let request = StateRequest::decode(&payload[..])?;
+        let block: B::Hash = Decode::decode(&mut request.block.as_ref())?;
+
+        let key = SeenRequestsKey {
+            peer: *peer,
+            block,
+            start: request.start.clone(),
+        };
+
+        let mut reputation_changes = Vec::new();
+
+        match self.seen_requests.get(&key) {
+            Some(SeenRequestsValue::First) => {}
+            Some(SeenRequestsValue::Fulfilled(ref mut requests)) => {
+                *requests = requests.saturating_add(1);
+
+                if *requests > MAX_NUMBER_OF_SAME_REQUESTS_PER_PEER {
+                    reputation_changes.push(rep::SAME_REQUEST);
+                }
+            }
+            None => {
+                self.seen_requests
+                    .insert(key.clone(), SeenRequestsValue::First);
+            }
+        }
+
+        trace!(
+            target: LOG_TARGET,
+            "Handling state request from {}: Block {:?}, Starting at {:x?}, no_proof={}",
+            peer,
+            request.block,
+            &request.start,
+            request.no_proof,
+        );
+
+        let result = if reputation_changes.is_empty() {
+            let mut response = StateResponse::default();
+
+            if !request.no_proof {
+                let (proof, _count) = self.client.read_proof_collection(
+                    block,
+                    request.start.as_slice(),
+                    MAX_RESPONSE_BYTES,
+                )?;
+                response.proof = proof.encode();
+            } else {
+                let entries = self.client.storage_collection(
+                    block,
+                    request.start.as_slice(),
+                    MAX_RESPONSE_BYTES,
+                )?;
+                response.entries = entries
+                    .into_iter()
+                    .map(|(state, complete)| KeyValueStateEntry {
+                        state_root: state.state_root,
+                        entries: state
+                            .key_values
+                            .into_iter()
+                            .map(|(key, value)| StateEntry { key, value })
+                            .collect(),
+                        complete,
+                    })
+                    .collect();
+            }
+
+            trace!(
+                target: LOG_TARGET,
+                "StateResponse contains {} keys, {}, proof nodes, from {:?} to {:?}",
+                response.entries.len(),
+                response.proof.len(),
+                response.entries.first().and_then(|top| top
+                    .entries
+                    .first()
+                    .map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key))),
+                response.entries.first().and_then(|top| top
+                    .entries
+                    .last()
+                    .map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key))),
+            );
+            if let Some(value) = self.seen_requests.get(&key) {
+                // If this is the first time we have processed this request, we need to change
+                // it to `Fulfilled`.
+                if let SeenRequestsValue::First = value {
+                    *value = SeenRequestsValue::Fulfilled(1);
+                }
+            }
+
+            let mut data = Vec::with_capacity(response.encoded_len());
+            response.encode(&mut data)?;
+            Ok(data)
+        } else {
+            Err(())
+        };
+
+        pending_response
+            .send(OutgoingResponse {
+                result,
+                reputation_changes,
+                sent_feedback: None,
+            })
+            .map_err(|_| HandleRequestError::SendResponse)
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum HandleRequestError {
+    #[error("Failed to decode request: {0}.")]
+    DecodeProto(#[from] prost::DecodeError),
+
+    #[error("Failed to encode response: {0}.")]
+    EncodeProto(#[from] prost::EncodeError),
+
+    #[error("Failed to decode block hash: {0}.")]
+    InvalidHash(#[from] codec::Error),
+
+    #[error(transparent)]
+    Client(#[from] sp_blockchain::Error),
+
+    #[error("Failed to send response.")]
+    SendResponse,
+}
diff --git a/libp2p/sync/src/strategy.rs b/libp2p/sync/src/strategy.rs
new file mode 100644
index 000000000..25c5aef9d
--- /dev/null
+++ b/libp2p/sync/src/strategy.rs
@@ -0,0 +1,542 @@
+// 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/>.
+
+//! [`SyncingStrategy`] is a proxy between [`crate::engine::SyncingEngine`]
+//! and specific syncing algorithms.
+
+pub mod chain_sync;
+mod state;
+pub mod state_sync;
+pub mod warp;
+
+use crate::service::syncing_service::ProcessedBlock;
+use crate::{
+    types::{BadPeer, OpaqueStateRequest, OpaqueStateResponse, SyncStatus},
+    LOG_TARGET,
+};
+use chain_sync::{ChainSync, ChainSyncAction, ChainSyncMode};
+use libp2p::PeerId;
+use log::{error, info};
+use prometheus_endpoint::Registry;
+use sc_client_api::{BlockBackend, ProofProvider};
+use sc_consensus::IncomingBlock;
+use sc_network_common::sync::{
+    message::{BlockAnnounce, BlockData, BlockRequest},
+    SyncMode,
+};
+use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata};
+use sp_consensus::BlockOrigin;
+use sp_runtime::{
+    traits::{Block as BlockT, NumberFor},
+    Justifications,
+};
+use state::{StateStrategy, StateStrategyAction};
+use std::sync::Arc;
+use warp::{EncodedProof, WarpProofRequest, WarpSync, WarpSyncAction, WarpSyncConfig};
+
+/// Corresponding `ChainSync` mode.
+fn chain_sync_mode(sync_mode: SyncMode) -> ChainSyncMode {
+    match sync_mode {
+        SyncMode::Full => ChainSyncMode::Full,
+        SyncMode::LightState {
+            skip_proofs,
+            storage_chain_mode,
+        } => ChainSyncMode::LightState {
+            skip_proofs,
+            storage_chain_mode,
+        },
+        SyncMode::Warp => ChainSyncMode::Full,
+    }
+}
+
+/// Syncing configuration containing data for all strategies.
+#[derive(Clone, Debug)]
+pub struct SyncingConfig {
+    /// Syncing mode.
+    pub mode: SyncMode,
+    /// The number of parallel downloads to guard against slow peers.
+    pub max_parallel_downloads: u32,
+    /// Maximum number of blocks to request.
+    pub max_blocks_per_request: u32,
+    /// Prometheus metrics registry.
+    pub metrics_registry: Option<Registry>,
+}
+
+#[derive(Debug)]
+pub enum SyncingAction<B: BlockT> {
+    /// Send block request to peer. Always implies dropping a stale block request to the same peer.
+    SendBlockRequest {
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+    },
+    /// Drop stale block request.
+    CancelBlockRequest { peer_id: PeerId },
+    /// Send state request to peer.
+    SendStateRequest {
+        peer_id: PeerId,
+        request: OpaqueStateRequest,
+    },
+    /// Send warp proof request to peer.
+    SendWarpProofRequest {
+        peer_id: PeerId,
+        request: WarpProofRequest<B>,
+    },
+    /// Peer misbehaved. Disconnect, report it and cancel any requests to it.
+    DropPeer(BadPeer),
+    /// Import blocks.
+    ImportBlocks {
+        origin: BlockOrigin,
+        blocks: Vec<IncomingBlock<B>>,
+    },
+    /// Import justifications.
+    ImportJustifications {
+        peer_id: PeerId,
+        hash: B::Hash,
+        number: NumberFor<B>,
+        justifications: Justifications,
+    },
+    /// Syncing strategy has finished.
+    Finished,
+}
+
+/// Proxy to specific syncing strategies.
+pub enum SyncingStrategy<B: BlockT, Client> {
+    WarpSyncStrategy(WarpSync<B, Client>),
+    StateSyncStrategy(StateStrategy<B>),
+    ChainSyncStrategy(ChainSync<B, Client>),
+}
+
+impl<B: BlockT, Client> SyncingStrategy<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B>
+        + BlockBackend<B>
+        + HeaderMetadata<B, Error = sp_blockchain::Error>
+        + ProofProvider<B>
+        + Send
+        + Sync
+        + 'static,
+{
+    /// Initialize a new syncing startegy.
+    pub fn new(
+        config: SyncingConfig,
+        client: Arc<Client>,
+        warp_sync_config: Option<WarpSyncConfig<B>>,
+    ) -> Result<Self, ClientError> {
+        if let SyncMode::Warp = config.mode {
+            let warp_sync_config = warp_sync_config
+                .expect("Warp sync configuration must be supplied in warp sync mode.");
+            Ok(Self::WarpSyncStrategy(WarpSync::new(
+                client.clone(),
+                warp_sync_config,
+            )))
+        } else {
+            Ok(Self::ChainSyncStrategy(ChainSync::new(
+                chain_sync_mode(config.mode),
+                client.clone(),
+                config.max_parallel_downloads,
+                config.max_blocks_per_request,
+                config.metrics_registry,
+            )?))
+        }
+    }
+
+    /// Notify that a new peer has connected.
+    pub fn add_peer(&mut self, peer_id: PeerId, best_hash: B::Hash, best_number: NumberFor<B>) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => {
+                strategy.add_peer(peer_id, best_hash, best_number)
+            }
+            SyncingStrategy::StateSyncStrategy(strategy) => {
+                strategy.add_peer(peer_id, best_hash, best_number)
+            }
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.add_peer(peer_id, best_hash, best_number)
+            }
+        }
+    }
+
+    /// Notify that a peer has disconnected.
+    pub fn remove_peer(&mut self, peer_id: &PeerId) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => strategy.remove_peer(peer_id),
+            SyncingStrategy::StateSyncStrategy(strategy) => strategy.remove_peer(peer_id),
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.remove_peer(peer_id),
+        }
+    }
+
+    /// Submit a validated block announcement.
+    ///
+    /// Returns new best hash & best number of the peer if they are updated.
+    pub fn on_validated_block_announce(
+        &mut self,
+        is_best: bool,
+        peer_id: PeerId,
+        announce: &BlockAnnounce<B::Header>,
+    ) -> Option<(B::Hash, NumberFor<B>)> {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => {
+                strategy.on_validated_block_announce(is_best, peer_id, announce)
+            }
+            SyncingStrategy::StateSyncStrategy(strategy) => {
+                strategy.on_validated_block_announce(is_best, peer_id, announce)
+            }
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_validated_block_announce(is_best, peer_id, announce)
+            }
+        }
+    }
+
+    /// Configure an explicit fork sync request in case external code has detected that there is a
+    /// stale fork missing.
+    pub fn set_sync_fork_request(
+        &mut self,
+        peers: Vec<PeerId>,
+        hash: &B::Hash,
+        number: NumberFor<B>,
+    ) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.set_sync_fork_request(peers, hash, number)
+            }
+        }
+    }
+
+    /// Request extra justification.
+    pub fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.request_justification(hash, number)
+            }
+        }
+    }
+
+    /// Clear extra justification requests.
+    pub fn clear_justification_requests(&mut self) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.clear_justification_requests(),
+        }
+    }
+
+    /// Report a justification import (successful or not).
+    pub fn on_justification_import(&mut self, hash: B::Hash, number: NumberFor<B>, success: bool) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_justification_import(hash, number, success)
+            }
+        }
+    }
+
+    /// Process block response.
+    pub fn on_block_response(
+        &mut self,
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+        blocks: Vec<BlockData<B>>,
+    ) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => {
+                strategy.on_block_response(peer_id, request, blocks)
+            }
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_block_response(peer_id, request, blocks)
+            }
+        }
+    }
+
+    /// Process state response.
+    pub fn on_state_response(&mut self, peer_id: PeerId, response: OpaqueStateResponse) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(strategy) => {
+                strategy.on_state_response(peer_id, response)
+            }
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_state_response(peer_id, response)
+            }
+        }
+    }
+
+    /// Process warp proof response.
+    pub fn on_warp_proof_response(&mut self, peer_id: &PeerId, response: EncodedProof) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => {
+                strategy.on_warp_proof_response(peer_id, response)
+            }
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(_) => {}
+        }
+    }
+
+    /// A batch of blocks have been processed, with or without errors.
+    pub fn on_blocks_processed(
+        &mut self,
+        imported: usize,
+        count: usize,
+        results: Vec<ProcessedBlock<B, B::Hash>>,
+    ) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(strategy) => {
+                strategy.on_blocks_processed(imported, count, results)
+            }
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_blocks_processed(imported, count, results)
+            }
+        }
+    }
+
+    /// Notify a syncing strategy that a block has been finalized.
+    pub fn on_block_finalized(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.on_block_finalized(hash, number)
+            }
+        }
+    }
+
+    /// Inform sync about a new best imported block.
+    pub fn update_chain_info(&mut self, best_hash: &B::Hash, best_number: NumberFor<B>) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.update_chain_info(best_hash, best_number)
+            }
+        }
+    }
+
+    // Are we in major sync mode?
+    pub fn is_major_syncing(&self) -> bool {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => true,
+            SyncingStrategy::StateSyncStrategy(_) => true,
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                strategy.status().state.is_major_syncing()
+            }
+        }
+    }
+
+    /// Get the number of peers known to the syncing strategy.
+    pub fn num_peers(&self) -> usize {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => strategy.num_peers(),
+            SyncingStrategy::StateSyncStrategy(strategy) => strategy.num_peers(),
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.num_peers(),
+        }
+    }
+
+    /// Returns the current sync status.
+    pub fn status(&self) -> SyncStatus<B> {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => strategy.status(),
+            SyncingStrategy::StateSyncStrategy(strategy) => strategy.status(),
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.status(),
+        }
+    }
+
+    /// Get the total number of downloaded blocks.
+    pub fn num_downloaded_blocks(&self) -> usize {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => 0,
+            SyncingStrategy::StateSyncStrategy(_) => 0,
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.num_downloaded_blocks(),
+        }
+    }
+
+    /// Get an estimate of the number of parallel sync requests.
+    pub fn num_sync_requests(&self) -> usize {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => 0,
+            SyncingStrategy::StateSyncStrategy(_) => 0,
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.num_sync_requests(),
+        }
+    }
+
+    /// Report Prometheus metrics
+    pub fn report_metrics(&self) {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(_) => {}
+            SyncingStrategy::StateSyncStrategy(_) => {}
+            SyncingStrategy::ChainSyncStrategy(strategy) => strategy.report_metrics(),
+        }
+    }
+
+    /// Get actions that should be performed by the owner on the strategy's behalf
+    #[must_use]
+    pub fn actions(&mut self) -> Box<dyn Iterator<Item = SyncingAction<B>>> {
+        match self {
+            SyncingStrategy::WarpSyncStrategy(strategy) => {
+                Box::new(strategy.actions().map(|action| match action {
+                    WarpSyncAction::SendWarpProofRequest { peer_id, request } => {
+                        SyncingAction::SendWarpProofRequest { peer_id, request }
+                    }
+                    WarpSyncAction::SendBlockRequest { peer_id, request } => {
+                        SyncingAction::SendBlockRequest { peer_id, request }
+                    }
+                    WarpSyncAction::DropPeer(bad_peer) => SyncingAction::DropPeer(bad_peer),
+                    WarpSyncAction::Finished => SyncingAction::Finished,
+                }))
+            }
+            SyncingStrategy::StateSyncStrategy(strategy) => {
+                Box::new(strategy.actions().map(|action| match action {
+                    StateStrategyAction::SendStateRequest { peer_id, request } => {
+                        SyncingAction::SendStateRequest { peer_id, request }
+                    }
+                    StateStrategyAction::DropPeer(bad_peer) => SyncingAction::DropPeer(bad_peer),
+                    StateStrategyAction::ImportBlocks { origin, blocks } => {
+                        SyncingAction::ImportBlocks { origin, blocks }
+                    }
+                    StateStrategyAction::Finished => SyncingAction::Finished,
+                }))
+            }
+            SyncingStrategy::ChainSyncStrategy(strategy) => {
+                Box::new(strategy.actions().map(|action| match action {
+                    ChainSyncAction::SendBlockRequest { peer_id, request } => {
+                        SyncingAction::SendBlockRequest { peer_id, request }
+                    }
+                    ChainSyncAction::CancelBlockRequest { peer_id } => {
+                        SyncingAction::CancelBlockRequest { peer_id }
+                    }
+                    ChainSyncAction::SendStateRequest { peer_id, request } => {
+                        SyncingAction::SendStateRequest { peer_id, request }
+                    }
+                    ChainSyncAction::DropPeer(bad_peer) => SyncingAction::DropPeer(bad_peer),
+                    ChainSyncAction::ImportBlocks { origin, blocks } => {
+                        SyncingAction::ImportBlocks { origin, blocks }
+                    }
+                    ChainSyncAction::ImportJustifications {
+                        peer_id,
+                        hash,
+                        number,
+                        justifications,
+                    } => SyncingAction::ImportJustifications {
+                        peer_id,
+                        hash,
+                        number,
+                        justifications,
+                    },
+                }))
+            }
+        }
+    }
+
+    /// Switch to next strategy if the active one finished.
+    pub fn switch_to_next(
+        &mut self,
+        config: SyncingConfig,
+        client: Arc<Client>,
+        connected_peers: impl Iterator<Item = (PeerId, B::Hash, NumberFor<B>)>,
+    ) -> Result<(), ClientError> {
+        match self {
+            Self::WarpSyncStrategy(warp_sync) => {
+                match warp_sync.take_result() {
+                    Some(res) => {
+                        info!(
+                            target: LOG_TARGET,
+                            "Warp sync is complete, continuing with state sync."
+                        );
+                        let state_sync = StateStrategy::new(
+                            client,
+                            res.target_header,
+                            res.target_body,
+                            res.target_justifications,
+                            // skip proofs, only set to `true` in `FastUnsafe` sync mode
+                            false,
+                            connected_peers
+                                .map(|(peer_id, _best_hash, best_number)| (peer_id, best_number)),
+                        );
+
+                        *self = Self::StateSyncStrategy(state_sync);
+                    }
+                    None => {
+                        error!(
+                            target: LOG_TARGET,
+                            "Warp sync failed. Falling back to full sync."
+                        );
+                        let mut chain_sync = match ChainSync::new(
+                            chain_sync_mode(config.mode),
+                            client,
+                            config.max_parallel_downloads,
+                            config.max_blocks_per_request,
+                            config.metrics_registry,
+                        ) {
+                            Ok(chain_sync) => chain_sync,
+                            Err(e) => {
+                                error!(target: LOG_TARGET, "Failed to start `ChainSync`.");
+                                return Err(e);
+                            }
+                        };
+                        // Let `ChainSync` know about connected peers.
+                        connected_peers.into_iter().for_each(
+                            |(peer_id, best_hash, best_number)| {
+                                chain_sync.add_peer(peer_id, best_hash, best_number)
+                            },
+                        );
+
+                        *self = Self::ChainSyncStrategy(chain_sync);
+                    }
+                }
+            }
+            Self::StateSyncStrategy(state_sync) => {
+                if state_sync.is_succeded() {
+                    info!(target: LOG_TARGET, "State sync is complete, continuing with block sync.");
+                } else {
+                    error!(target: LOG_TARGET, "State sync failed. Falling back to full sync.");
+                }
+                let mut chain_sync = match ChainSync::new(
+                    chain_sync_mode(config.mode),
+                    client,
+                    config.max_parallel_downloads,
+                    config.max_blocks_per_request,
+                    config.metrics_registry,
+                ) {
+                    Ok(chain_sync) => chain_sync,
+                    Err(e) => {
+                        error!(target: LOG_TARGET, "Failed to start `ChainSync`.");
+                        return Err(e);
+                    }
+                };
+                // Let `ChainSync` know about connected peers.
+                connected_peers
+                    .into_iter()
+                    .for_each(|(peer_id, best_hash, best_number)| {
+                        chain_sync.add_peer(peer_id, best_hash, best_number)
+                    });
+
+                *self = Self::ChainSyncStrategy(chain_sync);
+            }
+            Self::ChainSyncStrategy(_) => {
+                error!(target: LOG_TARGET, "`ChainSyncStrategy` is final startegy, cannot switch to next.");
+                debug_assert!(false);
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/libp2p/sync/src/strategy/chain_sync.rs b/libp2p/sync/src/strategy/chain_sync.rs
new file mode 100644
index 000000000..874d5e682
--- /dev/null
+++ b/libp2p/sync/src/strategy/chain_sync.rs
@@ -0,0 +1,2338 @@
+// 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/>.
+
+//! Contains the state of the chain synchronization process
+//!
+//! At any given point in time, a running node tries as much as possible to be at the head of the
+//! chain. This module handles the logic of which blocks to request from remotes, and processing
+//! responses. It yields blocks to check and potentially move to the database.
+//!
+//! # Usage
+//!
+//! The `ChainSync` struct maintains the state of the block requests. Whenever something happens on
+//! the network, or whenever a block has been successfully verified, call the appropriate method in
+//! order to update it.
+
+use crate::{
+    blocks::BlockCollection,
+    extra_requests::ExtraRequests,
+    schema::v1::StateResponse,
+    strategy::{
+        state_sync::{ImportResult, StateSync, StateSyncProvider},
+        warp::{WarpSyncPhase, WarpSyncProgress},
+    },
+    types::{BadPeer, OpaqueStateRequest, OpaqueStateResponse, SyncState, SyncStatus},
+    LOG_TARGET,
+};
+
+use codec::Encode;
+use libp2p::PeerId;
+use log::{debug, error, info, trace, warn};
+use prometheus_endpoint::{register, Gauge, GaugeVec, Opts, PrometheusError, Registry, U64};
+use sc_client_api::{BlockBackend, ProofProvider};
+use sc_consensus::{BlockImportError, BlockImportStatus, IncomingBlock};
+use sc_network_common::sync::message::{
+    BlockAnnounce, BlockAttributes, BlockData, BlockRequest, BlockResponse, Direction, FromBlock,
+};
+use sp_arithmetic::traits::Saturating;
+use sp_blockchain::{Error as ClientError, HeaderBackend, HeaderMetadata};
+use sp_consensus::{BlockOrigin, BlockStatus};
+use sp_runtime::{
+    traits::{
+        Block as BlockT, CheckedSub, Hash, HashingFor, Header as HeaderT, NumberFor, One,
+        SaturatedConversion, Zero,
+    },
+    EncodedJustification, Justifications,
+};
+
+use crate::service::syncing_service::ProcessedBlock;
+use std::{
+    collections::{HashMap, HashSet},
+    ops::Range,
+    sync::Arc,
+};
+
+#[cfg(test)]
+mod test;
+
+/// Maximum blocks to store in the import queue.
+const MAX_IMPORTING_BLOCKS: usize = 2048;
+
+/// Maximum blocks to download ahead of any gap.
+const MAX_DOWNLOAD_AHEAD: u32 = 2048;
+
+/// Maximum blocks to look backwards. The gap is the difference between the highest block and the
+/// common block of a node.
+const MAX_BLOCKS_TO_LOOK_BACKWARDS: u32 = MAX_DOWNLOAD_AHEAD / 2;
+
+/// Pick the state to sync as the latest finalized number minus this.
+const STATE_SYNC_FINALITY_THRESHOLD: u32 = 8;
+
+/// We use a heuristic that with a high likelihood, by the time
+/// `MAJOR_SYNC_BLOCKS` have been imported we'll be on the same
+/// chain as (or at least closer to) the peer so we want to delay
+/// the ancestor search to not waste time doing that when we are
+/// so far behind.
+const MAJOR_SYNC_BLOCKS: u8 = 5;
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+    /// Reputation change when a peer sent us a message that led to a
+    /// database read error.
+    pub const BLOCKCHAIN_READ_ERROR: Rep = Rep::new(-(1 << 16), "DB Error");
+
+    /// Reputation change when a peer sent us a status message with a different
+    /// genesis than us.
+    pub const GENESIS_MISMATCH: Rep = Rep::new(i32::MIN, "Genesis mismatch");
+
+    /// Reputation change for peers which send us a block with an incomplete header.
+    pub const INCOMPLETE_HEADER: Rep = Rep::new(-(1 << 20), "Incomplete header");
+
+    /// Reputation change for peers which send us a block which we fail to verify.
+    pub const VERIFICATION_FAIL: Rep = Rep::new(-(1 << 29), "Block verification failed");
+
+    /// Reputation change for peers which send us a known bad block.
+    pub const BAD_BLOCK: Rep = Rep::new(-(1 << 29), "Bad block");
+
+    /// Peer did not provide us with advertised block data.
+    pub const NO_BLOCK: Rep = Rep::new(-(1 << 29), "No requested block data");
+
+    /// Reputation change for peers which send us non-requested block data.
+    pub const NOT_REQUESTED: Rep = Rep::new(-(1 << 29), "Not requested block data");
+
+    /// Reputation change for peers which send us a block with bad justifications.
+    pub const BAD_JUSTIFICATION: Rep = Rep::new(-(1 << 16), "Bad justification");
+
+    /// Reputation change when a peer sent us invlid ancestry result.
+    pub const UNKNOWN_ANCESTOR: Rep = Rep::new(-(1 << 16), "DB Error");
+
+    /// Peer response data does not have requested bits.
+    pub const BAD_RESPONSE: Rep = Rep::new(-(1 << 12), "Incomplete response");
+}
+
+struct Metrics {
+    queued_blocks: Gauge<U64>,
+    fork_targets: Gauge<U64>,
+    justifications: GaugeVec<U64>,
+}
+
+impl Metrics {
+    fn register(r: &Registry) -> Result<Self, PrometheusError> {
+        Ok(Self {
+            queued_blocks: {
+                let g = Gauge::new(
+                    "substrate_sync_queued_blocks",
+                    "Number of blocks in import queue",
+                )?;
+                register(g, r)?
+            },
+            fork_targets: {
+                let g = Gauge::new("substrate_sync_fork_targets", "Number of fork sync targets")?;
+                register(g, r)?
+            },
+            justifications: {
+                let g = GaugeVec::new(
+                    Opts::new(
+                        "substrate_sync_extra_justifications",
+                        "Number of extra justifications requests",
+                    ),
+                    &["status"],
+                )?;
+                register(g, r)?
+            },
+        })
+    }
+}
+
+enum AllowedRequests {
+    Some(HashSet<PeerId>),
+    All,
+}
+
+impl AllowedRequests {
+    fn add(&mut self, id: &PeerId) {
+        if let Self::Some(ref mut set) = self {
+            set.insert(*id);
+        }
+    }
+
+    fn take(&mut self) -> Self {
+        std::mem::take(self)
+    }
+
+    fn set_all(&mut self) {
+        *self = Self::All;
+    }
+
+    fn contains(&self, id: &PeerId) -> bool {
+        match self {
+            Self::Some(set) => set.contains(id),
+            Self::All => true,
+        }
+    }
+
+    fn is_empty(&self) -> bool {
+        match self {
+            Self::Some(set) => set.is_empty(),
+            Self::All => false,
+        }
+    }
+
+    fn clear(&mut self) {
+        std::mem::take(self);
+    }
+}
+
+impl Default for AllowedRequests {
+    fn default() -> Self {
+        Self::Some(HashSet::default())
+    }
+}
+
+struct GapSync<B: BlockT> {
+    blocks: BlockCollection<B>,
+    best_queued_number: NumberFor<B>,
+    target: NumberFor<B>,
+}
+
+/// Action that the parent of [`ChainSync`] should perform after reporting a network or block event.
+#[derive(Debug)]
+pub enum ChainSyncAction<B: BlockT> {
+    /// Send block request to peer. Always implies dropping a stale block request to the same peer.
+    SendBlockRequest {
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+    },
+    /// Drop stale block request.
+    CancelBlockRequest { peer_id: PeerId },
+    /// Send state request to peer.
+    SendStateRequest {
+        peer_id: PeerId,
+        request: OpaqueStateRequest,
+    },
+    /// Peer misbehaved. Disconnect, report it and cancel the block request to it.
+    DropPeer(BadPeer),
+    /// Import blocks.
+    ImportBlocks {
+        origin: BlockOrigin,
+        blocks: Vec<IncomingBlock<B>>,
+    },
+    /// Import justifications.
+    ImportJustifications {
+        peer_id: PeerId,
+        hash: B::Hash,
+        number: NumberFor<B>,
+        justifications: Justifications,
+    },
+}
+
+/// Sync operation mode.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub enum ChainSyncMode {
+    /// Full block download and verification.
+    Full,
+    /// Download blocks and the latest state.
+    LightState {
+        /// Skip state proof download and verification.
+        skip_proofs: bool,
+        /// Download indexed transactions for recent blocks.
+        storage_chain_mode: bool,
+    },
+}
+
+/// The main data structure which contains all the state for a chains
+/// active syncing strategy.
+pub struct ChainSync<B: BlockT, Client> {
+    /// Chain client.
+    client: Arc<Client>,
+    /// The active peers that we are using to sync and their PeerSync status
+    peers: HashMap<PeerId, PeerSync<B>>,
+    /// A `BlockCollection` of blocks that are being downloaded from peers
+    blocks: BlockCollection<B>,
+    /// The best block number in our queue of blocks to import
+    best_queued_number: NumberFor<B>,
+    /// The best block hash in our queue of blocks to import
+    best_queued_hash: B::Hash,
+    /// Current mode (full/light)
+    mode: ChainSyncMode,
+    /// Any extra justification requests.
+    extra_justifications: ExtraRequests<B>,
+    /// A set of hashes of blocks that are being downloaded or have been
+    /// downloaded and are queued for import.
+    queue_blocks: HashSet<B::Hash>,
+    /// Fork sync targets.
+    fork_targets: HashMap<B::Hash, ForkTarget<B>>,
+    /// A set of peers for which there might be potential block requests
+    allowed_requests: AllowedRequests,
+    /// Maximum number of peers to ask the same blocks in parallel.
+    max_parallel_downloads: u32,
+    /// Maximum blocks per request.
+    max_blocks_per_request: u32,
+    /// Total number of downloaded blocks.
+    downloaded_blocks: usize,
+    /// State sync in progress, if any.
+    state_sync: Option<StateSync<B, Client>>,
+    /// Enable importing existing blocks. This is used used after the state download to
+    /// catch up to the latest state while re-importing blocks.
+    import_existing: bool,
+    /// Gap download process.
+    gap_sync: Option<GapSync<B>>,
+    /// Pending actions.
+    actions: Vec<ChainSyncAction<B>>,
+    /// Prometheus metrics.
+    metrics: Option<Metrics>,
+}
+
+/// All the data we have about a Peer that we are trying to sync with
+#[derive(Debug, Clone)]
+pub(crate) struct PeerSync<B: BlockT> {
+    /// Peer id of this peer.
+    pub peer_id: PeerId,
+    /// The common number is the block number that is a common point of
+    /// ancestry for both our chains (as far as we know).
+    pub common_number: NumberFor<B>,
+    /// The hash of the best block that we've seen for this peer.
+    pub best_hash: B::Hash,
+    /// The number of the best block that we've seen for this peer.
+    pub best_number: NumberFor<B>,
+    /// The state of syncing this peer is in for us, generally categories
+    /// into `Available` or "busy" with something as defined by `PeerSyncState`.
+    pub state: PeerSyncState<B>,
+}
+
+impl<B: BlockT> PeerSync<B> {
+    /// Update the `common_number` iff `new_common > common_number`.
+    fn update_common_number(&mut self, new_common: NumberFor<B>) {
+        if self.common_number < new_common {
+            trace!(
+                target: LOG_TARGET,
+                "Updating peer {} common number from={} => to={}.",
+                self.peer_id,
+                self.common_number,
+                new_common,
+            );
+            self.common_number = new_common;
+        }
+    }
+}
+
+struct ForkTarget<B: BlockT> {
+    number: NumberFor<B>,
+    parent_hash: Option<B::Hash>,
+    peers: HashSet<PeerId>,
+}
+
+/// The state of syncing between a Peer and ourselves.
+///
+/// Generally two categories, "busy" or `Available`. If busy, the enum
+/// defines what we are busy with.
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub(crate) enum PeerSyncState<B: BlockT> {
+    /// Available for sync requests.
+    Available,
+    /// Searching for ancestors the Peer has in common with us.
+    AncestorSearch {
+        start: NumberFor<B>,
+        current: NumberFor<B>,
+        state: AncestorSearchState<B>,
+    },
+    /// Actively downloading new blocks, starting from the given Number.
+    DownloadingNew(NumberFor<B>),
+    /// Downloading a stale block with given Hash. Stale means that it is a
+    /// block with a number that is lower than our best number. It might be
+    /// from a fork and not necessarily already imported.
+    DownloadingStale(B::Hash),
+    /// Downloading justification for given block hash.
+    DownloadingJustification(B::Hash),
+    /// Downloading state.
+    DownloadingState,
+    /// Actively downloading block history after warp sync.
+    DownloadingGap(NumberFor<B>),
+}
+
+impl<B: BlockT> PeerSyncState<B> {
+    pub fn is_available(&self) -> bool {
+        matches!(self, Self::Available)
+    }
+}
+
+impl<B, Client> ChainSync<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B>
+        + BlockBackend<B>
+        + HeaderMetadata<B, Error = sp_blockchain::Error>
+        + ProofProvider<B>
+        + Send
+        + Sync
+        + 'static,
+{
+    /// Create a new instance.
+    pub fn new(
+        mode: ChainSyncMode,
+        client: Arc<Client>,
+        max_parallel_downloads: u32,
+        max_blocks_per_request: u32,
+        metrics_registry: Option<Registry>,
+    ) -> Result<Self, ClientError> {
+        let mut sync = Self {
+            client,
+            peers: HashMap::new(),
+            blocks: BlockCollection::new(),
+            best_queued_hash: Default::default(),
+            best_queued_number: Zero::zero(),
+            extra_justifications: ExtraRequests::new("justification"),
+            mode,
+            queue_blocks: Default::default(),
+            fork_targets: Default::default(),
+            allowed_requests: Default::default(),
+            max_parallel_downloads,
+            max_blocks_per_request,
+            downloaded_blocks: 0,
+            state_sync: None,
+            import_existing: false,
+            gap_sync: None,
+            actions: Vec::new(),
+            metrics: metrics_registry.and_then(|r| match Metrics::register(&r) {
+                Ok(metrics) => Some(metrics),
+                Err(err) => {
+                    log::error!(
+                        target: LOG_TARGET,
+                        "Failed to register `ChainSync` metrics {err:?}",
+                    );
+                    None
+                }
+            }),
+        };
+
+        sync.reset_sync_start_point()?;
+        Ok(sync)
+    }
+
+    /// Returns the current sync status.
+    pub fn status(&self) -> SyncStatus<B> {
+        let median_seen = self.median_seen();
+        let best_seen_block =
+            median_seen.and_then(|median| (median > self.best_queued_number).then_some(median));
+        let sync_state = if let Some(target) = median_seen {
+            // A chain is classified as downloading if the provided best block is
+            // more than `MAJOR_SYNC_BLOCKS` behind the best block or as importing
+            // if the same can be said about queued blocks.
+            let best_block = self.client.info().best_number;
+            if target > best_block && target - best_block > MAJOR_SYNC_BLOCKS.into() {
+                // If target is not queued, we're downloading, otherwise importing.
+                if target > self.best_queued_number {
+                    SyncState::Downloading { target }
+                } else {
+                    SyncState::Importing { target }
+                }
+            } else {
+                SyncState::Idle
+            }
+        } else {
+            SyncState::Idle
+        };
+
+        let warp_sync_progress = self.gap_sync.as_ref().map(|gap_sync| WarpSyncProgress {
+            phase: WarpSyncPhase::DownloadingBlocks(gap_sync.best_queued_number),
+            total_bytes: 0,
+        });
+
+        SyncStatus {
+            state: sync_state,
+            best_seen_block,
+            num_peers: self.peers.len() as u32,
+            num_connected_peers: 0u32,
+            queued_blocks: self.queue_blocks.len() as u32,
+            state_sync: self.state_sync.as_ref().map(|s| s.progress()),
+            warp_sync: warp_sync_progress,
+        }
+    }
+
+    /// Get an estimate of the number of parallel sync requests.
+    pub fn num_sync_requests(&self) -> usize {
+        self.fork_targets
+            .values()
+            .filter(|f| f.number <= self.best_queued_number)
+            .count()
+    }
+
+    /// Get the total number of downloaded blocks.
+    pub fn num_downloaded_blocks(&self) -> usize {
+        self.downloaded_blocks
+    }
+
+    /// Get the number of peers known to the syncing state machine.
+    pub fn num_peers(&self) -> usize {
+        self.peers.len()
+    }
+
+    /// Notify syncing state machine that a new sync peer has connected.
+    pub fn add_peer(&mut self, peer_id: PeerId, best_hash: B::Hash, best_number: NumberFor<B>) {
+        match self.add_peer_inner(peer_id, best_hash, best_number) {
+            Ok(Some(request)) => self
+                .actions
+                .push(ChainSyncAction::SendBlockRequest { peer_id, request }),
+            Ok(None) => {}
+            Err(bad_peer) => self.actions.push(ChainSyncAction::DropPeer(bad_peer)),
+        }
+    }
+
+    fn add_peer_inner(
+        &mut self,
+        peer_id: PeerId,
+        best_hash: B::Hash,
+        best_number: NumberFor<B>,
+    ) -> Result<Option<BlockRequest<B>>, BadPeer> {
+        // There is nothing sync can get from the node that has no blockchain data.
+        match self.block_status(&best_hash) {
+            Err(e) => {
+                debug!(target: LOG_TARGET, "Error reading blockchain: {e}");
+                Err(BadPeer(peer_id, rep::BLOCKCHAIN_READ_ERROR))
+            }
+            Ok(BlockStatus::KnownBad) => {
+                info!(
+                    "💔 New peer {peer_id} with known bad best block {best_hash} ({best_number})."
+                );
+                Err(BadPeer(peer_id, rep::BAD_BLOCK))
+            }
+            Ok(BlockStatus::Unknown) => {
+                if best_number.is_zero() {
+                    info!(
+                        "💔 New peer {} with unknown genesis hash {} ({}).",
+                        peer_id, best_hash, best_number,
+                    );
+                    return Err(BadPeer(peer_id, rep::GENESIS_MISMATCH));
+                }
+
+                // If there are more than `MAJOR_SYNC_BLOCKS` in the import queue then we have
+                // enough to do in the import queue that it's not worth kicking off
+                // an ancestor search, which is what we do in the next match case below.
+                if self.queue_blocks.len() > MAJOR_SYNC_BLOCKS.into() {
+                    debug!(
+                        target: LOG_TARGET,
+                        "New peer {} with unknown best hash {} ({}), assuming common block.",
+                        peer_id,
+                        self.best_queued_hash,
+                        self.best_queued_number
+                    );
+                    self.peers.insert(
+                        peer_id,
+                        PeerSync {
+                            peer_id,
+                            common_number: self.best_queued_number,
+                            best_hash,
+                            best_number,
+                            state: PeerSyncState::Available,
+                        },
+                    );
+                    return Ok(None);
+                }
+
+                // If we are at genesis, just start downloading.
+                let (state, req) = if self.best_queued_number.is_zero() {
+                    debug!(
+                        target: LOG_TARGET,
+                        "New peer {peer_id} with best hash {best_hash} ({best_number}).",
+                    );
+
+                    (PeerSyncState::Available, None)
+                } else {
+                    let common_best = std::cmp::min(self.best_queued_number, best_number);
+
+                    debug!(
+                        target: LOG_TARGET,
+                        "New peer {} with unknown best hash {} ({}), searching for common ancestor.",
+                        peer_id,
+                        best_hash,
+                        best_number
+                    );
+
+                    (
+                        PeerSyncState::AncestorSearch {
+                            current: common_best,
+                            start: self.best_queued_number,
+                            state: AncestorSearchState::ExponentialBackoff(One::one()),
+                        },
+                        Some(ancestry_request::<B>(common_best)),
+                    )
+                };
+
+                self.allowed_requests.add(&peer_id);
+                self.peers.insert(
+                    peer_id,
+                    PeerSync {
+                        peer_id,
+                        common_number: Zero::zero(),
+                        best_hash,
+                        best_number,
+                        state,
+                    },
+                );
+
+                Ok(req)
+            }
+            Ok(BlockStatus::Queued)
+            | Ok(BlockStatus::InChainWithState)
+            | Ok(BlockStatus::InChainPruned) => {
+                debug!(
+                    target: LOG_TARGET,
+                    "New peer {peer_id} with known best hash {best_hash} ({best_number}).",
+                );
+                self.peers.insert(
+                    peer_id,
+                    PeerSync {
+                        peer_id,
+                        common_number: std::cmp::min(self.best_queued_number, best_number),
+                        best_hash,
+                        best_number,
+                        state: PeerSyncState::Available,
+                    },
+                );
+                self.allowed_requests.add(&peer_id);
+                Ok(None)
+            }
+        }
+    }
+
+    /// Inform sync about a new best imported block.
+    pub fn update_chain_info(&mut self, best_hash: &B::Hash, best_number: NumberFor<B>) {
+        self.on_block_queued(best_hash, best_number);
+    }
+
+    /// Request extra justification.
+    pub fn request_justification(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        let client = &self.client;
+        self.extra_justifications
+            .schedule((*hash, number), |base, block| {
+                is_descendent_of(&**client, base, block)
+            })
+    }
+
+    /// Clear extra justification requests.
+    pub fn clear_justification_requests(&mut self) {
+        self.extra_justifications.reset();
+    }
+
+    /// Configure an explicit fork sync request in case external code has detected that there is a
+    /// stale fork missing.
+    ///
+    /// Note that this function should not be used for recent blocks.
+    /// Sync should be able to download all the recent forks normally.
+    ///
+    /// Passing empty `peers` set effectively removes the sync request.
+    // The implementation is similar to `on_validated_block_announce` with unknown parent hash.
+    pub fn set_sync_fork_request(
+        &mut self,
+        mut peers: Vec<PeerId>,
+        hash: &B::Hash,
+        number: NumberFor<B>,
+    ) {
+        if peers.is_empty() {
+            peers = self
+                .peers
+                .iter()
+                // Only request blocks from peers who are ahead or on a par.
+                .filter(|(_, peer)| peer.best_number >= number)
+                .map(|(id, _)| *id)
+                .collect();
+
+            debug!(
+                target: LOG_TARGET,
+                "Explicit sync request for block {hash:?} with no peers specified. \
+                Syncing from these peers {peers:?} instead.",
+            );
+        } else {
+            debug!(
+                target: LOG_TARGET,
+                "Explicit sync request for block {hash:?} with {peers:?}",
+            );
+        }
+
+        if self.is_known(hash) {
+            debug!(target: LOG_TARGET, "Refusing to sync known hash {hash:?}");
+            return;
+        }
+
+        trace!(target: LOG_TARGET, "Downloading requested old fork {hash:?}");
+        for peer_id in &peers {
+            if let Some(peer) = self.peers.get_mut(peer_id) {
+                if let PeerSyncState::AncestorSearch { .. } = peer.state {
+                    continue;
+                }
+
+                if number > peer.best_number {
+                    peer.best_number = number;
+                    peer.best_hash = *hash;
+                }
+                self.allowed_requests.add(peer_id);
+            }
+        }
+
+        self.fork_targets
+            .entry(*hash)
+            .or_insert_with(|| ForkTarget {
+                number,
+                peers: Default::default(),
+                parent_hash: None,
+            })
+            .peers
+            .extend(peers);
+    }
+
+    /// Submit a block response for processing.
+    fn on_block_data(
+        &mut self,
+        peer_id: &PeerId,
+        request: Option<BlockRequest<B>>,
+        response: BlockResponse<B>,
+    ) -> Result<(), BadPeer> {
+        self.downloaded_blocks += response.blocks.len();
+        let mut gap = false;
+        let new_blocks: Vec<IncomingBlock<B>> = if let Some(peer) = self.peers.get_mut(peer_id) {
+            let mut blocks = response.blocks;
+            if request
+                .as_ref()
+                .map_or(false, |r| r.direction == Direction::Descending)
+            {
+                trace!(target: LOG_TARGET, "Reversing incoming block list");
+                blocks.reverse()
+            }
+            self.allowed_requests.add(peer_id);
+            if let Some(request) = request {
+                match &mut peer.state {
+                    PeerSyncState::DownloadingNew(_) => {
+                        self.blocks.clear_peer_download(peer_id);
+                        peer.state = PeerSyncState::Available;
+                        if let Some(start_block) =
+                            validate_blocks::<B>(&blocks, peer_id, Some(request))?
+                        {
+                            self.blocks.insert(start_block, blocks, *peer_id);
+                        }
+                        self.ready_blocks()
+                    }
+                    PeerSyncState::DownloadingGap(_) => {
+                        peer.state = PeerSyncState::Available;
+                        if let Some(gap_sync) = &mut self.gap_sync {
+                            gap_sync.blocks.clear_peer_download(peer_id);
+                            if let Some(start_block) =
+                                validate_blocks::<B>(&blocks, peer_id, Some(request))?
+                            {
+                                gap_sync.blocks.insert(start_block, blocks, *peer_id);
+                            }
+                            gap = true;
+                            let blocks: Vec<_> = gap_sync
+                                .blocks
+                                .ready_blocks(gap_sync.best_queued_number + One::one())
+                                .into_iter()
+                                .map(|block_data| {
+                                    let justifications =
+                                        block_data.block.justifications.or_else(|| {
+                                            legacy_justification_mapping(
+                                                block_data.block.justification,
+                                            )
+                                        });
+                                    IncomingBlock {
+                                        hash: block_data.block.hash,
+                                        header: block_data.block.header,
+                                        body: block_data.block.body,
+                                        indexed_body: block_data.block.indexed_body,
+                                        justifications,
+                                        origin: block_data.origin,
+                                        allow_missing_state: true,
+                                        import_existing: self.import_existing,
+                                        skip_execution: true,
+                                        state: None,
+                                    }
+                                })
+                                .collect();
+                            debug!(
+                                target: LOG_TARGET,
+                                "Drained {} gap blocks from {}",
+                                blocks.len(),
+                                gap_sync.best_queued_number,
+                            );
+                            blocks
+                        } else {
+                            debug!(target: LOG_TARGET, "Unexpected gap block response from {peer_id}");
+                            return Err(BadPeer(*peer_id, rep::NO_BLOCK));
+                        }
+                    }
+                    PeerSyncState::DownloadingStale(_) => {
+                        peer.state = PeerSyncState::Available;
+                        if blocks.is_empty() {
+                            debug!(target: LOG_TARGET, "Empty block response from {peer_id}");
+                            return Err(BadPeer(*peer_id, rep::NO_BLOCK));
+                        }
+                        validate_blocks::<B>(&blocks, peer_id, Some(request))?;
+                        blocks
+                            .into_iter()
+                            .map(|b| {
+                                let justifications = b
+                                    .justifications
+                                    .or_else(|| legacy_justification_mapping(b.justification));
+                                IncomingBlock {
+                                    hash: b.hash,
+                                    header: b.header,
+                                    body: b.body,
+                                    indexed_body: None,
+                                    justifications,
+                                    origin: Some(*peer_id),
+                                    allow_missing_state: true,
+                                    import_existing: self.import_existing,
+                                    skip_execution: self.skip_execution(),
+                                    state: None,
+                                }
+                            })
+                            .collect()
+                    }
+                    PeerSyncState::AncestorSearch {
+                        current,
+                        start,
+                        state,
+                    } => {
+                        let matching_hash = match (blocks.first(), self.client.hash(*current)) {
+                            (Some(block), Ok(maybe_our_block_hash)) => {
+                                trace!(
+                                    target: LOG_TARGET,
+                                    "Got ancestry block #{} ({}) from peer {}",
+                                    current,
+                                    block.hash,
+                                    peer_id,
+                                );
+                                maybe_our_block_hash.filter(|x| x == &block.hash)
+                            }
+                            (None, _) => {
+                                debug!(
+                                    target: LOG_TARGET,
+                                    "Invalid response when searching for ancestor from {peer_id}",
+                                );
+                                return Err(BadPeer(*peer_id, rep::UNKNOWN_ANCESTOR));
+                            }
+                            (_, Err(e)) => {
+                                info!(
+                                    target: LOG_TARGET,
+                                    "❌ Error answering legitimate blockchain query: {e}",
+                                );
+                                return Err(BadPeer(*peer_id, rep::BLOCKCHAIN_READ_ERROR));
+                            }
+                        };
+                        if matching_hash.is_some() {
+                            if *start < self.best_queued_number
+                                && self.best_queued_number <= peer.best_number
+                            {
+                                // We've made progress on this chain since the search was started.
+                                // Opportunistically set common number to updated number
+                                // instead of the one that started the search.
+                                trace!(
+                                    target: LOG_TARGET,
+                                    "Ancestry search: opportunistically updating peer {} common number from={} => to={}.",
+                                    *peer_id,
+                                    peer.common_number,
+                                    self.best_queued_number,
+                                );
+                                peer.common_number = self.best_queued_number;
+                            } else if peer.common_number < *current {
+                                trace!(
+                                    target: LOG_TARGET,
+                                    "Ancestry search: updating peer {} common number from={} => to={}.",
+                                    *peer_id,
+                                    peer.common_number,
+                                    *current,
+                                );
+                                peer.common_number = *current;
+                            }
+                        }
+                        if matching_hash.is_none() && current.is_zero() {
+                            trace!(
+                                target: LOG_TARGET,
+                                "Ancestry search: genesis mismatch for peer {peer_id}",
+                            );
+                            return Err(BadPeer(*peer_id, rep::GENESIS_MISMATCH));
+                        }
+                        if let Some((next_state, next_num)) =
+                            handle_ancestor_search_state(state, *current, matching_hash.is_some())
+                        {
+                            peer.state = PeerSyncState::AncestorSearch {
+                                current: next_num,
+                                start: *start,
+                                state: next_state,
+                            };
+                            let request = ancestry_request::<B>(next_num);
+                            self.actions.push(ChainSyncAction::SendBlockRequest {
+                                peer_id: *peer_id,
+                                request,
+                            });
+                            return Ok(());
+                        } else {
+                            // Ancestry search is complete. Check if peer is on a stale fork unknown
+                            // to us and add it to sync targets if necessary.
+                            trace!(
+                                target: LOG_TARGET,
+                                "Ancestry search complete. Ours={} ({}), Theirs={} ({}), Common={:?} ({})",
+                                self.best_queued_hash,
+                                self.best_queued_number,
+                                peer.best_hash,
+                                peer.best_number,
+                                matching_hash,
+                                peer.common_number,
+                            );
+                            if peer.common_number < peer.best_number
+                                && peer.best_number < self.best_queued_number
+                            {
+                                trace!(
+                                    target: LOG_TARGET,
+                                    "Added fork target {} for {}",
+                                    peer.best_hash,
+                                    peer_id,
+                                );
+                                self.fork_targets
+                                    .entry(peer.best_hash)
+                                    .or_insert_with(|| ForkTarget {
+                                        number: peer.best_number,
+                                        parent_hash: None,
+                                        peers: Default::default(),
+                                    })
+                                    .peers
+                                    .insert(*peer_id);
+                            }
+                            peer.state = PeerSyncState::Available;
+                            return Ok(());
+                        }
+                    }
+                    PeerSyncState::Available
+                    | PeerSyncState::DownloadingJustification(..)
+                    | PeerSyncState::DownloadingState => Vec::new(),
+                }
+            } else {
+                // When request.is_none() this is a block announcement. Just accept blocks.
+                validate_blocks::<B>(&blocks, peer_id, None)?;
+                blocks
+                    .into_iter()
+                    .map(|b| {
+                        let justifications = b
+                            .justifications
+                            .or_else(|| legacy_justification_mapping(b.justification));
+                        IncomingBlock {
+                            hash: b.hash,
+                            header: b.header,
+                            body: b.body,
+                            indexed_body: None,
+                            justifications,
+                            origin: Some(*peer_id),
+                            allow_missing_state: true,
+                            import_existing: false,
+                            skip_execution: true,
+                            state: None,
+                        }
+                    })
+                    .collect()
+            }
+        } else {
+            // We don't know of this peer, so we also did not request anything from it.
+            return Err(BadPeer(*peer_id, rep::NOT_REQUESTED));
+        };
+
+        self.validate_and_queue_blocks(new_blocks, gap);
+
+        Ok(())
+    }
+
+    /// Submit a justification response for processing.
+    fn on_block_justification(
+        &mut self,
+        peer_id: PeerId,
+        response: BlockResponse<B>,
+    ) -> Result<(), BadPeer> {
+        let peer = if let Some(peer) = self.peers.get_mut(&peer_id) {
+            peer
+        } else {
+            error!(
+                target: LOG_TARGET,
+                "💔 Called on_block_justification with a peer ID of an unknown peer",
+            );
+            return Ok(());
+        };
+
+        self.allowed_requests.add(&peer_id);
+        if let PeerSyncState::DownloadingJustification(hash) = peer.state {
+            peer.state = PeerSyncState::Available;
+
+            // We only request one justification at a time
+            let justification = if let Some(block) = response.blocks.into_iter().next() {
+                if hash != block.hash {
+                    warn!(
+                        target: LOG_TARGET,
+                        "💔 Invalid block justification provided by {}: requested: {:?} got: {:?}",
+                        peer_id,
+                        hash,
+                        block.hash,
+                    );
+                    return Err(BadPeer(peer_id, rep::BAD_JUSTIFICATION));
+                }
+
+                block
+                    .justifications
+                    .or_else(|| legacy_justification_mapping(block.justification))
+            } else {
+                // we might have asked the peer for a justification on a block that we assumed it
+                // had but didn't (regardless of whether it had a justification for it or not).
+                trace!(
+                    target: LOG_TARGET,
+                    "Peer {peer_id:?} provided empty response for justification request {hash:?}",
+                );
+
+                None
+            };
+
+            if let Some((peer_id, hash, number, justifications)) = self
+                .extra_justifications
+                .on_response(peer_id, justification)
+            {
+                self.actions.push(ChainSyncAction::ImportJustifications {
+                    peer_id,
+                    hash,
+                    number,
+                    justifications,
+                });
+                return Ok(());
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Report a justification import (successful or not).
+    pub fn on_justification_import(&mut self, hash: B::Hash, number: NumberFor<B>, success: bool) {
+        let finalization_result = if success { Ok((hash, number)) } else { Err(()) };
+        self.extra_justifications
+            .try_finalize_root((hash, number), finalization_result, true);
+        self.allowed_requests.set_all();
+    }
+
+    /// Notify sync that a block has been finalized.
+    pub fn on_block_finalized(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        let client = &self.client;
+        let r = self
+            .extra_justifications
+            .on_block_finalized(hash, number, |base, block| {
+                is_descendent_of(&**client, base, block)
+            });
+
+        if let ChainSyncMode::LightState { skip_proofs, .. } = &self.mode {
+            if self.state_sync.is_none() && !self.peers.is_empty() && self.queue_blocks.is_empty() {
+                // Finalized a recent block.
+                let mut heads: Vec<_> = self.peers.values().map(|peer| peer.best_number).collect();
+                heads.sort();
+                let median = heads[heads.len() / 2];
+                if number + STATE_SYNC_FINALITY_THRESHOLD.saturated_into() >= median {
+                    if let Ok(Some(header)) = self.client.header(*hash) {
+                        log::debug!(
+                            target: LOG_TARGET,
+                            "Starting state sync for #{number} ({hash})",
+                        );
+                        self.state_sync = Some(StateSync::new(
+                            self.client.clone(),
+                            header,
+                            None,
+                            None,
+                            *skip_proofs,
+                        ));
+                        self.allowed_requests.set_all();
+                    }
+                }
+            }
+        }
+
+        if let Err(err) = r {
+            warn!(
+                target: LOG_TARGET,
+                "💔 Error cleaning up pending extra justification data requests: {err}",
+            );
+        }
+    }
+
+    /// Submit a validated block announcement.
+    ///
+    /// Returns new best hash & best number of the peer if they are updated.
+    #[must_use]
+    pub fn on_validated_block_announce(
+        &mut self,
+        is_best: bool,
+        peer_id: PeerId,
+        announce: &BlockAnnounce<B::Header>,
+    ) -> Option<(B::Hash, NumberFor<B>)> {
+        let number = *announce.header.number();
+        let hash = announce.header.hash();
+        let parent_status = self
+            .block_status(announce.header.parent_hash())
+            .unwrap_or(BlockStatus::Unknown);
+        let known_parent = parent_status != BlockStatus::Unknown;
+        let ancient_parent = parent_status == BlockStatus::InChainPruned;
+
+        let known = self.is_known(&hash);
+        let peer = if let Some(peer) = self.peers.get_mut(&peer_id) {
+            peer
+        } else {
+            error!(target: LOG_TARGET, "💔 Called `on_validated_block_announce` with a bad peer ID");
+            return Some((hash, number));
+        };
+
+        if let PeerSyncState::AncestorSearch { .. } = peer.state {
+            trace!(target: LOG_TARGET, "Peer {} is in the ancestor search state.", peer_id);
+            return None;
+        }
+
+        let peer_info = is_best.then(|| {
+            // update their best block
+            peer.best_number = number;
+            peer.best_hash = hash;
+
+            (hash, number)
+        });
+
+        // If the announced block is the best they have and is not ahead of us, our common number
+        // is either one further ahead or it's the one they just announced, if we know about it.
+        if is_best {
+            if known && self.best_queued_number >= number {
+                self.update_peer_common_number(&peer_id, number);
+            } else if announce.header.parent_hash() == &self.best_queued_hash
+                || known_parent && self.best_queued_number >= number
+            {
+                self.update_peer_common_number(&peer_id, number.saturating_sub(One::one()));
+            }
+        }
+        self.allowed_requests.add(&peer_id);
+
+        // known block case
+        if known || self.is_already_downloading(&hash) {
+            trace!(target: LOG_TARGET, "Known block announce from {}: {}", peer_id, hash);
+            if let Some(target) = self.fork_targets.get_mut(&hash) {
+                target.peers.insert(peer_id);
+            }
+            return peer_info;
+        }
+
+        if ancient_parent {
+            trace!(
+                target: LOG_TARGET,
+                "Ignored ancient block announced from {}: {} {:?}",
+                peer_id,
+                hash,
+                announce.header,
+            );
+            return peer_info;
+        }
+
+        if self.status().state == SyncState::Idle {
+            trace!(
+                target: LOG_TARGET,
+                "Added sync target for block announced from {}: {} {:?}",
+                peer_id,
+                hash,
+                announce.summary(),
+            );
+            self.fork_targets
+                .entry(hash)
+                .or_insert_with(|| ForkTarget {
+                    number,
+                    parent_hash: Some(*announce.header.parent_hash()),
+                    peers: Default::default(),
+                })
+                .peers
+                .insert(peer_id);
+        }
+
+        peer_info
+    }
+
+    /// Notify that a sync peer has disconnected.
+    pub fn remove_peer(&mut self, peer_id: &PeerId) {
+        self.blocks.clear_peer_download(peer_id);
+        if let Some(gap_sync) = &mut self.gap_sync {
+            gap_sync.blocks.clear_peer_download(peer_id)
+        }
+        self.peers.remove(peer_id);
+        self.extra_justifications.peer_disconnected(peer_id);
+        self.allowed_requests.set_all();
+        self.fork_targets.retain(|_, target| {
+            target.peers.remove(peer_id);
+            !target.peers.is_empty()
+        });
+
+        let blocks = self.ready_blocks();
+
+        if !blocks.is_empty() {
+            self.validate_and_queue_blocks(blocks, false);
+        }
+    }
+
+    /// Report prometheus metrics.
+    pub fn report_metrics(&self) {
+        if let Some(metrics) = &self.metrics {
+            metrics
+                .fork_targets
+                .set(self.fork_targets.len().try_into().unwrap_or(std::u64::MAX));
+            metrics
+                .queued_blocks
+                .set(self.queue_blocks.len().try_into().unwrap_or(std::u64::MAX));
+
+            let justifications_metrics = self.extra_justifications.metrics();
+            metrics
+                .justifications
+                .with_label_values(&["pending"])
+                .set(justifications_metrics.pending_requests.into());
+            metrics
+                .justifications
+                .with_label_values(&["active"])
+                .set(justifications_metrics.active_requests.into());
+            metrics
+                .justifications
+                .with_label_values(&["failed"])
+                .set(justifications_metrics.failed_requests.into());
+            metrics
+                .justifications
+                .with_label_values(&["importing"])
+                .set(justifications_metrics.importing_requests.into());
+        }
+    }
+
+    /// Returns the median seen block number.
+    fn median_seen(&self) -> Option<NumberFor<B>> {
+        let mut best_seens = self
+            .peers
+            .values()
+            .map(|p| p.best_number)
+            .collect::<Vec<_>>();
+
+        if best_seens.is_empty() {
+            None
+        } else {
+            let middle = best_seens.len() / 2;
+
+            // Not the "perfect median" when we have an even number of peers.
+            Some(*best_seens.select_nth_unstable(middle).1)
+        }
+    }
+
+    fn required_block_attributes(&self) -> BlockAttributes {
+        match self.mode {
+            ChainSyncMode::Full => {
+                BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY
+            }
+            ChainSyncMode::LightState {
+                storage_chain_mode: false,
+                ..
+            } => BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION | BlockAttributes::BODY,
+            ChainSyncMode::LightState {
+                storage_chain_mode: true,
+                ..
+            } => {
+                BlockAttributes::HEADER
+                    | BlockAttributes::JUSTIFICATION
+                    | BlockAttributes::INDEXED_BODY
+            }
+        }
+    }
+
+    fn skip_execution(&self) -> bool {
+        match self.mode {
+            ChainSyncMode::Full => false,
+            ChainSyncMode::LightState { .. } => true,
+        }
+    }
+
+    fn validate_and_queue_blocks(&mut self, mut new_blocks: Vec<IncomingBlock<B>>, gap: bool) {
+        let orig_len = new_blocks.len();
+        new_blocks.retain(|b| !self.queue_blocks.contains(&b.hash));
+        if new_blocks.len() != orig_len {
+            debug!(
+                target: LOG_TARGET,
+                "Ignoring {} blocks that are already queued",
+                orig_len - new_blocks.len(),
+            );
+        }
+
+        let origin = if !gap && !self.status().state.is_major_syncing() {
+            BlockOrigin::NetworkBroadcast
+        } else {
+            BlockOrigin::NetworkInitialSync
+        };
+
+        if let Some((h, n)) = new_blocks
+            .last()
+            .and_then(|b| b.header.as_ref().map(|h| (&b.hash, *h.number())))
+        {
+            trace!(
+                target: LOG_TARGET,
+                "Accepted {} blocks ({:?}) with origin {:?}",
+                new_blocks.len(),
+                h,
+                origin,
+            );
+            self.on_block_queued(h, n)
+        }
+        self.queue_blocks.extend(new_blocks.iter().map(|b| b.hash));
+
+        self.actions.push(ChainSyncAction::ImportBlocks {
+            origin,
+            blocks: new_blocks,
+        })
+    }
+
+    fn update_peer_common_number(&mut self, peer_id: &PeerId, new_common: NumberFor<B>) {
+        if let Some(peer) = self.peers.get_mut(peer_id) {
+            peer.update_common_number(new_common);
+        }
+    }
+
+    /// Called when a block has been queued for import.
+    ///
+    /// Updates our internal state for best queued block and then goes
+    /// through all peers to update our view of their state as well.
+    fn on_block_queued(&mut self, hash: &B::Hash, number: NumberFor<B>) {
+        if self.fork_targets.remove(hash).is_some() {
+            trace!(target: LOG_TARGET, "Completed fork sync {hash:?}");
+        }
+        if let Some(gap_sync) = &mut self.gap_sync {
+            if number > gap_sync.best_queued_number && number <= gap_sync.target {
+                gap_sync.best_queued_number = number;
+            }
+        }
+        if number > self.best_queued_number {
+            self.best_queued_number = number;
+            self.best_queued_hash = *hash;
+            // Update common blocks
+            for (n, peer) in self.peers.iter_mut() {
+                if let PeerSyncState::AncestorSearch { .. } = peer.state {
+                    // Wait for ancestry search to complete first.
+                    continue;
+                }
+                let new_common_number = if peer.best_number >= number {
+                    number
+                } else {
+                    peer.best_number
+                };
+                trace!(
+                    target: LOG_TARGET,
+                    "Updating peer {} info, ours={}, common={}->{}, their best={}",
+                    n,
+                    number,
+                    peer.common_number,
+                    new_common_number,
+                    peer.best_number,
+                );
+                peer.common_number = new_common_number;
+            }
+        }
+        self.allowed_requests.set_all();
+    }
+
+    /// Restart the sync process. This will reset all pending block requests and return an iterator
+    /// of new block requests to make to peers. Peers that were downloading finality data (i.e.
+    /// their state was `DownloadingJustification`) are unaffected and will stay in the same state.
+    fn restart(&mut self) {
+        self.blocks.clear();
+        if let Err(e) = self.reset_sync_start_point() {
+            warn!(target: LOG_TARGET, "💔  Unable to restart sync: {e}");
+        }
+        self.allowed_requests.set_all();
+        debug!(
+            target: LOG_TARGET,
+            "Restarted with {} ({})",
+            self.best_queued_number,
+            self.best_queued_hash,
+        );
+        let old_peers = std::mem::take(&mut self.peers);
+
+        old_peers.into_iter().for_each(|(peer_id, mut p)| {
+			// peers that were downloading justifications
+			// should be kept in that state.
+			if let PeerSyncState::DownloadingJustification(_) = p.state {
+				// We make sure our commmon number is at least something we have.
+				trace!(
+					target: LOG_TARGET,
+					"Keeping peer {} after restart, updating common number from={} => to={} (our best).",
+					peer_id,
+					p.common_number,
+					self.best_queued_number,
+				);
+				p.common_number = self.best_queued_number;
+				self.peers.insert(peer_id, p);
+				return
+			}
+
+			// handle peers that were in other states.
+			let action = match self.add_peer_inner(peer_id, p.best_hash, p.best_number) {
+				// since the request is not a justification, remove it from pending responses
+				Ok(None) => ChainSyncAction::CancelBlockRequest { peer_id },
+				// update the request if the new one is available
+				Ok(Some(request)) => ChainSyncAction::SendBlockRequest { peer_id, request },
+				// this implies that we need to drop pending response from the peer
+				Err(bad_peer) => ChainSyncAction::DropPeer(bad_peer),
+			};
+
+			self.actions.push(action);
+		});
+    }
+
+    /// Find a block to start sync from. If we sync with state, that's the latest block we have
+    /// state for.
+    fn reset_sync_start_point(&mut self) -> Result<(), ClientError> {
+        let info = self.client.info();
+        if matches!(self.mode, ChainSyncMode::LightState { .. }) && info.finalized_state.is_some() {
+            warn!(
+                target: LOG_TARGET,
+                "Can't use fast sync mode with a partially synced database. Reverting to full sync mode."
+            );
+            self.mode = ChainSyncMode::Full;
+        }
+
+        self.import_existing = false;
+        self.best_queued_hash = info.best_hash;
+        self.best_queued_number = info.best_number;
+
+        if self.mode == ChainSyncMode::Full
+            && self.client.block_status(info.best_hash)? != BlockStatus::InChainWithState
+        {
+            self.import_existing = true;
+            // Latest state is missing, start with the last finalized state or genesis instead.
+            if let Some((hash, number)) = info.finalized_state {
+                debug!(target: LOG_TARGET, "Starting from finalized state #{number}");
+                self.best_queued_hash = hash;
+                self.best_queued_number = number;
+            } else {
+                debug!(target: LOG_TARGET, "Restarting from genesis");
+                self.best_queued_hash = Default::default();
+                self.best_queued_number = Zero::zero();
+            }
+        }
+
+        if let Some((start, end)) = info.block_gap {
+            debug!(target: LOG_TARGET, "Starting gap sync #{start} - #{end}");
+            self.gap_sync = Some(GapSync {
+                best_queued_number: start - One::one(),
+                target: end,
+                blocks: BlockCollection::new(),
+            });
+        }
+        trace!(
+            target: LOG_TARGET,
+            "Restarted sync at #{} ({:?})",
+            self.best_queued_number,
+            self.best_queued_hash,
+        );
+        Ok(())
+    }
+
+    /// What is the status of the block corresponding to the given hash?
+    fn block_status(&self, hash: &B::Hash) -> Result<BlockStatus, ClientError> {
+        if self.queue_blocks.contains(hash) {
+            return Ok(BlockStatus::Queued);
+        }
+        self.client.block_status(*hash)
+    }
+
+    /// Is the block corresponding to the given hash known?
+    fn is_known(&self, hash: &B::Hash) -> bool {
+        self.block_status(hash)
+            .ok()
+            .map_or(false, |s| s != BlockStatus::Unknown)
+    }
+
+    /// Is any peer downloading the given hash?
+    fn is_already_downloading(&self, hash: &B::Hash) -> bool {
+        self.peers
+            .iter()
+            .any(|(_, p)| p.state == PeerSyncState::DownloadingStale(*hash))
+    }
+
+    /// Get the set of downloaded blocks that are ready to be queued for import.
+    fn ready_blocks(&mut self) -> Vec<IncomingBlock<B>> {
+        self.blocks
+            .ready_blocks(self.best_queued_number + One::one())
+            .into_iter()
+            .map(|block_data| {
+                let justifications = block_data
+                    .block
+                    .justifications
+                    .or_else(|| legacy_justification_mapping(block_data.block.justification));
+                IncomingBlock {
+                    hash: block_data.block.hash,
+                    header: block_data.block.header,
+                    body: block_data.block.body,
+                    indexed_body: block_data.block.indexed_body,
+                    justifications,
+                    origin: block_data.origin,
+                    allow_missing_state: true,
+                    import_existing: self.import_existing,
+                    skip_execution: self.skip_execution(),
+                    state: None,
+                }
+            })
+            .collect()
+    }
+
+    /// Submit blocks received in a response.
+    pub fn on_block_response(
+        &mut self,
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+        blocks: Vec<BlockData<B>>,
+    ) {
+        let block_response = BlockResponse::<B> {
+            id: request.id,
+            blocks,
+        };
+
+        let blocks_range = || match (
+            block_response
+                .blocks
+                .first()
+                .and_then(|b| b.header.as_ref().map(|h| h.number())),
+            block_response
+                .blocks
+                .last()
+                .and_then(|b| b.header.as_ref().map(|h| h.number())),
+        ) {
+            (Some(first), Some(last)) if first != last => format!(" ({}..{})", first, last),
+            (Some(first), Some(_)) => format!(" ({})", first),
+            _ => Default::default(),
+        };
+        trace!(
+            target: LOG_TARGET,
+            "BlockResponse {} from {} with {} blocks {}",
+            block_response.id,
+            peer_id,
+            block_response.blocks.len(),
+            blocks_range(),
+        );
+
+        let res = if request.fields == BlockAttributes::JUSTIFICATION {
+            self.on_block_justification(peer_id, block_response)
+        } else {
+            self.on_block_data(&peer_id, Some(request), block_response)
+        };
+
+        if let Err(bad_peer) = res {
+            self.actions.push(ChainSyncAction::DropPeer(bad_peer));
+        }
+    }
+
+    /// Submit a state received in a response.
+    pub fn on_state_response(&mut self, peer_id: PeerId, response: OpaqueStateResponse) {
+        if let Err(bad_peer) = self.on_state_data(&peer_id, response) {
+            self.actions.push(ChainSyncAction::DropPeer(bad_peer));
+        }
+    }
+
+    /// Get justification requests scheduled by sync to be sent out.
+    fn justification_requests(&mut self) -> Vec<(PeerId, BlockRequest<B>)> {
+        let peers = &mut self.peers;
+        let mut matcher = self.extra_justifications.matcher();
+        std::iter::from_fn(move || {
+            if let Some((peer, request)) = matcher.next(peers) {
+                peers
+                    .get_mut(&peer)
+                    .expect(
+                        "`Matcher::next` guarantees the `PeerId` comes from the given peers; qed",
+                    )
+                    .state = PeerSyncState::DownloadingJustification(request.0);
+                let req = BlockRequest::<B> {
+                    id: 0,
+                    fields: BlockAttributes::JUSTIFICATION,
+                    from: FromBlock::Hash(request.0),
+                    direction: Direction::Ascending,
+                    max: Some(1),
+                };
+                Some((peer, req))
+            } else {
+                None
+            }
+        })
+        .collect()
+    }
+
+    /// Get block requests scheduled by sync to be sent out.
+    fn block_requests(&mut self) -> Vec<(PeerId, BlockRequest<B>)> {
+        if self.allowed_requests.is_empty() || self.state_sync.is_some() {
+            return Vec::new();
+        }
+
+        if self.queue_blocks.len() > MAX_IMPORTING_BLOCKS {
+            trace!(target: LOG_TARGET, "Too many blocks in the queue.");
+            return Vec::new();
+        }
+        let is_major_syncing = self.status().state.is_major_syncing();
+        let attrs = self.required_block_attributes();
+        let blocks = &mut self.blocks;
+        let fork_targets = &mut self.fork_targets;
+        let last_finalized =
+            std::cmp::min(self.best_queued_number, self.client.info().finalized_number);
+        let best_queued = self.best_queued_number;
+        let client = &self.client;
+        let queue = &self.queue_blocks;
+        let allowed_requests = self.allowed_requests.take();
+        let max_parallel = if is_major_syncing {
+            1
+        } else {
+            self.max_parallel_downloads
+        };
+        let max_blocks_per_request = self.max_blocks_per_request;
+        let gap_sync = &mut self.gap_sync;
+        self.peers
+			.iter_mut()
+			.filter_map(move |(&id, peer)| {
+				if !peer.state.is_available() || !allowed_requests.contains(&id) {
+					return None
+				}
+
+				// If our best queued is more than `MAX_BLOCKS_TO_LOOK_BACKWARDS` blocks away from
+				// the common number, the peer best number is higher than our best queued and the
+				// common number is smaller than the last finalized block number, we should do an
+				// ancestor search to find a better common block. If the queue is full we wait till
+				// all blocks are imported though.
+				if best_queued.saturating_sub(peer.common_number) >
+					MAX_BLOCKS_TO_LOOK_BACKWARDS.into() &&
+					best_queued < peer.best_number &&
+					peer.common_number < last_finalized &&
+					queue.len() <= MAJOR_SYNC_BLOCKS.into()
+				{
+					trace!(
+						target: LOG_TARGET,
+						"Peer {:?} common block {} too far behind of our best {}. Starting ancestry search.",
+						id,
+						peer.common_number,
+						best_queued,
+					);
+					let current = std::cmp::min(peer.best_number, best_queued);
+					peer.state = PeerSyncState::AncestorSearch {
+						current,
+						start: best_queued,
+						state: AncestorSearchState::ExponentialBackoff(One::one()),
+					};
+					Some((id, ancestry_request::<B>(current)))
+				} else if let Some((range, req)) = peer_block_request(
+					&id,
+					peer,
+					blocks,
+					attrs,
+					max_parallel,
+					max_blocks_per_request,
+					last_finalized,
+					best_queued,
+				) {
+					peer.state = PeerSyncState::DownloadingNew(range.start);
+					trace!(
+						target: LOG_TARGET,
+						"New block request for {}, (best:{}, common:{}) {:?}",
+						id,
+						peer.best_number,
+						peer.common_number,
+						req,
+					);
+					Some((id, req))
+				} else if let Some((hash, req)) = fork_sync_request(
+					&id,
+					fork_targets,
+					best_queued,
+					last_finalized,
+					attrs,
+					|hash| {
+						if queue.contains(hash) {
+							BlockStatus::Queued
+						} else {
+							client.block_status(*hash).unwrap_or(BlockStatus::Unknown)
+						}
+					},
+					max_blocks_per_request,
+				) {
+					trace!(target: LOG_TARGET, "Downloading fork {hash:?} from {id}");
+					peer.state = PeerSyncState::DownloadingStale(hash);
+					Some((id, req))
+				} else if let Some((range, req)) = gap_sync.as_mut().and_then(|sync| {
+					peer_gap_block_request(
+						&id,
+						peer,
+						&mut sync.blocks,
+						attrs,
+						sync.target,
+						sync.best_queued_number,
+						max_blocks_per_request,
+					)
+				}) {
+					peer.state = PeerSyncState::DownloadingGap(range.start);
+					trace!(
+						target: LOG_TARGET,
+						"New gap block request for {}, (best:{}, common:{}) {:?}",
+						id,
+						peer.best_number,
+						peer.common_number,
+						req,
+					);
+					Some((id, req))
+				} else {
+					None
+				}
+			})
+			.collect()
+    }
+
+    /// Get a state request scheduled by sync to be sent out (if any).
+    fn state_request(&mut self) -> Option<(PeerId, OpaqueStateRequest)> {
+        if self.allowed_requests.is_empty() {
+            return None;
+        }
+        if self.state_sync.is_some()
+            && self
+                .peers
+                .iter()
+                .any(|(_, peer)| peer.state == PeerSyncState::DownloadingState)
+        {
+            // Only one pending state request is allowed.
+            return None;
+        }
+        if let Some(sync) = &self.state_sync {
+            if sync.is_complete() {
+                return None;
+            }
+
+            for (id, peer) in self.peers.iter_mut() {
+                if peer.state.is_available() && peer.common_number >= sync.target_number() {
+                    peer.state = PeerSyncState::DownloadingState;
+                    let request = sync.next_request();
+                    trace!(target: LOG_TARGET, "New StateRequest for {}: {:?}", id, request);
+                    self.allowed_requests.clear();
+                    return Some((*id, OpaqueStateRequest(Box::new(request))));
+                }
+            }
+        }
+        None
+    }
+
+    fn on_state_data(
+        &mut self,
+        peer_id: &PeerId,
+        response: OpaqueStateResponse,
+    ) -> Result<(), BadPeer> {
+        let response: Box<StateResponse> = response.0.downcast().map_err(|_error| {
+            error!(
+                target: LOG_TARGET,
+                "Failed to downcast opaque state response, this is an implementation bug."
+            );
+
+            BadPeer(*peer_id, rep::BAD_RESPONSE)
+        })?;
+
+        if let Some(peer) = self.peers.get_mut(peer_id) {
+            if let PeerSyncState::DownloadingState = peer.state {
+                peer.state = PeerSyncState::Available;
+                self.allowed_requests.set_all();
+            }
+        }
+        let import_result = if let Some(sync) = &mut self.state_sync {
+            debug!(
+                target: LOG_TARGET,
+                "Importing state data from {} with {} keys, {} proof nodes.",
+                peer_id,
+                response.entries.len(),
+                response.proof.len(),
+            );
+            sync.import(*response)
+        } else {
+            debug!(target: LOG_TARGET, "Ignored obsolete state response from {peer_id}");
+            return Err(BadPeer(*peer_id, rep::NOT_REQUESTED));
+        };
+
+        match import_result {
+            ImportResult::Import(hash, header, state, body, justifications) => {
+                let origin = BlockOrigin::NetworkInitialSync;
+                let block = IncomingBlock {
+                    hash,
+                    header: Some(header),
+                    body,
+                    indexed_body: None,
+                    justifications,
+                    origin: None,
+                    allow_missing_state: true,
+                    import_existing: true,
+                    skip_execution: self.skip_execution(),
+                    state: Some(state),
+                };
+                debug!(target: LOG_TARGET, "State download is complete. Import is queued");
+                self.actions.push(ChainSyncAction::ImportBlocks {
+                    origin,
+                    blocks: vec![block],
+                });
+                Ok(())
+            }
+            ImportResult::Continue => Ok(()),
+            ImportResult::BadResponse => {
+                debug!(target: LOG_TARGET, "Bad state data received from {peer_id}");
+                Err(BadPeer(*peer_id, rep::BAD_BLOCK))
+            }
+        }
+    }
+
+    /// A batch of blocks have been processed, with or without errors.
+    ///
+    /// Call this when a batch of blocks have been processed by the import
+    /// queue, with or without errors.
+    pub fn on_blocks_processed(
+        &mut self,
+        imported: usize,
+        count: usize,
+        results: Vec<ProcessedBlock<B, B::Hash>>,
+    ) {
+        trace!(target: LOG_TARGET, "Imported {imported} of {count}");
+
+        let mut has_error = false;
+        for (_, hash) in &results {
+            self.queue_blocks.remove(hash);
+            self.blocks.clear_queued(hash);
+            if let Some(gap_sync) = &mut self.gap_sync {
+                gap_sync.blocks.clear_queued(hash);
+            }
+        }
+        for (result, hash) in results {
+            if has_error {
+                break;
+            }
+
+            has_error |= result.is_err();
+
+            match result {
+                Ok(BlockImportStatus::ImportedKnown(number, peer_id)) => {
+                    if let Some(peer) = peer_id {
+                        self.update_peer_common_number(&peer, number);
+                    }
+                }
+                Ok(BlockImportStatus::ImportedUnknown(number, aux, peer_id)) => {
+                    if aux.clear_justification_requests {
+                        trace!(
+                            target: LOG_TARGET,
+                            "Block imported clears all pending justification requests {number}: {hash:?}",
+                        );
+                        self.clear_justification_requests();
+                    }
+
+                    if aux.needs_justification {
+                        trace!(
+                            target: LOG_TARGET,
+                            "Block imported but requires justification {number}: {hash:?}",
+                        );
+                        self.request_justification(&hash, number);
+                    }
+
+                    if aux.bad_justification {
+                        if let Some(ref peer) = peer_id {
+                            warn!("💔 Sent block with bad justification to import");
+                            self.actions.push(ChainSyncAction::DropPeer(BadPeer(
+                                *peer,
+                                rep::BAD_JUSTIFICATION,
+                            )));
+                        }
+                    }
+
+                    if let Some(peer) = peer_id {
+                        self.update_peer_common_number(&peer, number);
+                    }
+                    let state_sync_complete = self
+                        .state_sync
+                        .as_ref()
+                        .map_or(false, |s| s.target_hash() == hash);
+                    if state_sync_complete {
+                        info!(
+                            target: LOG_TARGET,
+                            "State sync is complete ({} MiB), restarting block sync.",
+                            self.state_sync.as_ref().map_or(0, |s| s.progress().size / (1024 * 1024)),
+                        );
+                        self.state_sync = None;
+                        self.mode = ChainSyncMode::Full;
+                        self.restart();
+                    }
+                    let gap_sync_complete =
+                        self.gap_sync.as_ref().map_or(false, |s| s.target == number);
+                    if gap_sync_complete {
+                        info!(
+                            target: LOG_TARGET,
+                            "Block history download is complete."
+                        );
+                        self.gap_sync = None;
+                    }
+                }
+                Err(BlockImportError::IncompleteHeader(peer_id)) => {
+                    if let Some(peer) = peer_id {
+                        warn!(
+                            target: LOG_TARGET,
+                            "💔 Peer sent block with incomplete header to import",
+                        );
+                        self.actions.push(ChainSyncAction::DropPeer(BadPeer(
+                            peer,
+                            rep::INCOMPLETE_HEADER,
+                        )));
+                        self.restart();
+                    }
+                }
+                Err(BlockImportError::VerificationFailed(peer_id, e)) => {
+                    let extra_message = peer_id
+                        .map_or_else(|| "".into(), |peer| format!(" received from ({peer})"));
+
+                    warn!(
+                        target: LOG_TARGET,
+                        "💔 Verification failed for block {hash:?}{extra_message}: {e:?}",
+                    );
+
+                    if let Some(peer) = peer_id {
+                        self.actions.push(ChainSyncAction::DropPeer(BadPeer(
+                            peer,
+                            rep::VERIFICATION_FAIL,
+                        )));
+                    }
+
+                    self.restart();
+                }
+                Err(BlockImportError::BadBlock(peer_id)) => {
+                    if let Some(peer) = peer_id {
+                        warn!(
+                            target: LOG_TARGET,
+                            "💔 Block {hash:?} received from peer {peer} has been blacklisted",
+                        );
+                        self.actions
+                            .push(ChainSyncAction::DropPeer(BadPeer(peer, rep::BAD_BLOCK)));
+                    }
+                }
+                Err(BlockImportError::MissingState) => {
+                    // This may happen if the chain we were requesting upon has been discarded
+                    // in the meantime because other chain has been finalized.
+                    // Don't mark it as bad as it still may be synced if explicitly requested.
+                    trace!(target: LOG_TARGET, "Obsolete block {hash:?}");
+                }
+                e @ Err(BlockImportError::UnknownParent) | e @ Err(BlockImportError::Other(_)) => {
+                    warn!(target: LOG_TARGET, "💔 Error importing block {hash:?}: {}", e.unwrap_err());
+                    self.state_sync = None;
+                    self.restart();
+                }
+                Err(BlockImportError::Cancelled) => {}
+            };
+        }
+
+        self.allowed_requests.set_all();
+    }
+
+    /// Get pending actions to perform.
+    pub fn actions(&mut self) -> impl Iterator<Item = ChainSyncAction<B>> {
+        let block_requests = self
+            .block_requests()
+            .into_iter()
+            .map(|(peer_id, request)| ChainSyncAction::SendBlockRequest { peer_id, request });
+        self.actions.extend(block_requests);
+
+        let justification_requests = self
+            .justification_requests()
+            .into_iter()
+            .map(|(peer_id, request)| ChainSyncAction::SendBlockRequest { peer_id, request });
+        self.actions.extend(justification_requests);
+
+        let state_request = self
+            .state_request()
+            .into_iter()
+            .map(|(peer_id, request)| ChainSyncAction::SendStateRequest { peer_id, request });
+        self.actions.extend(state_request);
+
+        std::mem::take(&mut self.actions).into_iter()
+    }
+
+    /// A version of `actions()` that doesn't schedule extra requests. For testing only.
+    #[cfg(test)]
+    fn take_actions(&mut self) -> impl Iterator<Item = ChainSyncAction<B>> {
+        std::mem::take(&mut self.actions).into_iter()
+    }
+}
+
+// This is purely during a backwards compatible transitionary period and should be removed
+// once we can assume all nodes can send and receive multiple Justifications
+// The ID tag is hardcoded here to avoid depending on the GRANDPA crate.
+// See: https://github.com/paritytech/substrate/issues/8172
+fn legacy_justification_mapping(
+    justification: Option<EncodedJustification>,
+) -> Option<Justifications> {
+    justification.map(|just| (*b"FRNK", just).into())
+}
+
+/// Request the ancestry for a block. Sends a request for header and justification for the given
+/// block number. Used during ancestry search.
+fn ancestry_request<B: BlockT>(block: NumberFor<B>) -> BlockRequest<B> {
+    BlockRequest::<B> {
+        id: 0,
+        fields: BlockAttributes::HEADER | BlockAttributes::JUSTIFICATION,
+        from: FromBlock::Number(block),
+        direction: Direction::Ascending,
+        max: Some(1),
+    }
+}
+
+/// The ancestor search state expresses which algorithm, and its stateful parameters, we are using
+/// to try to find an ancestor block
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub(crate) enum AncestorSearchState<B: BlockT> {
+    /// Use exponential backoff to find an ancestor, then switch to binary search.
+    /// We keep track of the exponent.
+    ExponentialBackoff(NumberFor<B>),
+    /// Using binary search to find the best ancestor.
+    /// We keep track of left and right bounds.
+    BinarySearch(NumberFor<B>, NumberFor<B>),
+}
+
+/// This function handles the ancestor search strategy used. The goal is to find a common point
+/// that both our chains agree on that is as close to the tip as possible.
+/// The way this works is we first have an exponential backoff strategy, where we try to step
+/// forward until we find a block hash mismatch. The size of the step doubles each step we take.
+///
+/// When we've found a block hash mismatch we then fall back to a binary search between the two
+/// last known points to find the common block closest to the tip.
+fn handle_ancestor_search_state<B: BlockT>(
+    state: &AncestorSearchState<B>,
+    curr_block_num: NumberFor<B>,
+    block_hash_match: bool,
+) -> Option<(AncestorSearchState<B>, NumberFor<B>)> {
+    let two = <NumberFor<B>>::one() + <NumberFor<B>>::one();
+    match state {
+        AncestorSearchState::ExponentialBackoff(next_distance_to_tip) => {
+            let next_distance_to_tip = *next_distance_to_tip;
+            if block_hash_match && next_distance_to_tip == One::one() {
+                // We found the ancestor in the first step so there is no need to execute binary
+                // search.
+                return None;
+            }
+            if block_hash_match {
+                let left = curr_block_num;
+                let right = left + next_distance_to_tip / two;
+                let middle = left + (right - left) / two;
+                Some((AncestorSearchState::BinarySearch(left, right), middle))
+            } else {
+                let next_block_num = curr_block_num
+                    .checked_sub(&next_distance_to_tip)
+                    .unwrap_or_else(Zero::zero);
+                let next_distance_to_tip = next_distance_to_tip * two;
+                Some((
+                    AncestorSearchState::ExponentialBackoff(next_distance_to_tip),
+                    next_block_num,
+                ))
+            }
+        }
+        AncestorSearchState::BinarySearch(mut left, mut right) => {
+            if left >= curr_block_num {
+                return None;
+            }
+            if block_hash_match {
+                left = curr_block_num;
+            } else {
+                right = curr_block_num;
+            }
+            assert!(right >= left);
+            let middle = left + (right - left) / two;
+            if middle == curr_block_num {
+                None
+            } else {
+                Some((AncestorSearchState::BinarySearch(left, right), middle))
+            }
+        }
+    }
+}
+
+/// Get a new block request for the peer if any.
+#[allow(clippy::too_many_arguments)]
+fn peer_block_request<B: BlockT>(
+    id: &PeerId,
+    peer: &PeerSync<B>,
+    blocks: &mut BlockCollection<B>,
+    attrs: BlockAttributes,
+    max_parallel_downloads: u32,
+    max_blocks_per_request: u32,
+    finalized: NumberFor<B>,
+    best_num: NumberFor<B>,
+) -> Option<(Range<NumberFor<B>>, BlockRequest<B>)> {
+    if best_num >= peer.best_number {
+        // Will be downloaded as alternative fork instead.
+        return None;
+    } else if peer.common_number < finalized {
+        trace!(
+            target: LOG_TARGET,
+            "Requesting pre-finalized chain from {:?}, common={}, finalized={}, peer best={}, our best={}",
+            id, peer.common_number, finalized, peer.best_number, best_num,
+        );
+    }
+    let range = blocks.needed_blocks(
+        *id,
+        max_blocks_per_request,
+        peer.best_number,
+        peer.common_number,
+        max_parallel_downloads,
+        MAX_DOWNLOAD_AHEAD,
+    )?;
+
+    // The end is not part of the range.
+    let last = range.end.saturating_sub(One::one());
+
+    let from = if peer.best_number == last {
+        FromBlock::Hash(peer.best_hash)
+    } else {
+        FromBlock::Number(last)
+    };
+
+    let request = BlockRequest::<B> {
+        id: 0,
+        fields: attrs,
+        from,
+        direction: Direction::Descending,
+        max: Some((range.end - range.start).saturated_into::<u32>()),
+    };
+
+    Some((range, request))
+}
+
+/// Get a new block request for the peer if any.
+fn peer_gap_block_request<B: BlockT>(
+    id: &PeerId,
+    peer: &PeerSync<B>,
+    blocks: &mut BlockCollection<B>,
+    attrs: BlockAttributes,
+    target: NumberFor<B>,
+    common_number: NumberFor<B>,
+    max_blocks_per_request: u32,
+) -> Option<(Range<NumberFor<B>>, BlockRequest<B>)> {
+    let range = blocks.needed_blocks(
+        *id,
+        max_blocks_per_request,
+        std::cmp::min(peer.best_number, target),
+        common_number,
+        1,
+        MAX_DOWNLOAD_AHEAD,
+    )?;
+
+    // The end is not part of the range.
+    let last = range.end.saturating_sub(One::one());
+    let from = FromBlock::Number(last);
+
+    let request = BlockRequest::<B> {
+        id: 0,
+        fields: attrs,
+        from,
+        direction: Direction::Descending,
+        max: Some((range.end - range.start).saturated_into::<u32>()),
+    };
+    Some((range, request))
+}
+
+/// Get pending fork sync targets for a peer.
+fn fork_sync_request<B: BlockT>(
+    id: &PeerId,
+    targets: &mut HashMap<B::Hash, ForkTarget<B>>,
+    best_num: NumberFor<B>,
+    finalized: NumberFor<B>,
+    attributes: BlockAttributes,
+    check_block: impl Fn(&B::Hash) -> BlockStatus,
+    max_blocks_per_request: u32,
+) -> Option<(B::Hash, BlockRequest<B>)> {
+    targets.retain(|hash, r| {
+        if r.number <= finalized {
+            trace!(
+                target: LOG_TARGET,
+                "Removed expired fork sync request {:?} (#{})",
+                hash,
+                r.number,
+            );
+            return false;
+        }
+        if check_block(hash) != BlockStatus::Unknown {
+            trace!(
+                target: LOG_TARGET,
+                "Removed obsolete fork sync request {:?} (#{})",
+                hash,
+                r.number,
+            );
+            return false;
+        }
+        true
+    });
+    for (hash, r) in targets {
+        if !r.peers.contains(id) {
+            continue;
+        }
+        // Download the fork only if it is behind or not too far ahead our tip of the chain
+        // Otherwise it should be downloaded in full sync mode.
+        if r.number <= best_num
+            || (r.number - best_num).saturated_into::<u32>() < max_blocks_per_request
+        {
+            let parent_status = r
+                .parent_hash
+                .as_ref()
+                .map_or(BlockStatus::Unknown, check_block);
+            let count = if parent_status == BlockStatus::Unknown {
+                (r.number - finalized).saturated_into::<u32>() // up to the last finalized block
+            } else {
+                // request only single block
+                1
+            };
+            trace!(
+                target: LOG_TARGET,
+                "Downloading requested fork {hash:?} from {id}, {count} blocks",
+            );
+            return Some((
+                *hash,
+                BlockRequest::<B> {
+                    id: 0,
+                    fields: attributes,
+                    from: FromBlock::Hash(*hash),
+                    direction: Direction::Descending,
+                    max: Some(count),
+                },
+            ));
+        } else {
+            trace!(target: LOG_TARGET, "Fork too far in the future: {:?} (#{})", hash, r.number);
+        }
+    }
+    None
+}
+
+/// Returns `true` if the given `block` is a descendent of `base`.
+fn is_descendent_of<Block, T>(
+    client: &T,
+    base: &Block::Hash,
+    block: &Block::Hash,
+) -> sp_blockchain::Result<bool>
+where
+    Block: BlockT,
+    T: HeaderMetadata<Block, Error = sp_blockchain::Error> + ?Sized,
+{
+    if base == block {
+        return Ok(false);
+    }
+
+    let ancestor = sp_blockchain::lowest_common_ancestor(client, *block, *base)?;
+
+    Ok(ancestor.hash == *base)
+}
+
+/// Validate that the given `blocks` are correct.
+/// Returns the number of the first block in the sequence.
+///
+/// It is expected that `blocks` are in ascending order.
+pub fn validate_blocks<Block: BlockT>(
+    blocks: &Vec<BlockData<Block>>,
+    peer_id: &PeerId,
+    request: Option<BlockRequest<Block>>,
+) -> Result<Option<NumberFor<Block>>, BadPeer> {
+    if let Some(request) = request {
+        if Some(blocks.len() as _) > request.max {
+            debug!(
+                target: LOG_TARGET,
+                "Received more blocks than requested from {}. Expected in maximum {:?}, got {}.",
+                peer_id,
+                request.max,
+                blocks.len(),
+            );
+
+            return Err(BadPeer(*peer_id, rep::NOT_REQUESTED));
+        }
+
+        let block_header = if request.direction == Direction::Descending {
+            blocks.last()
+        } else {
+            blocks.first()
+        }
+        .and_then(|b| b.header.as_ref());
+
+        let expected_block = block_header.as_ref().map_or(false, |h| match request.from {
+            FromBlock::Hash(hash) => h.hash() == hash,
+            FromBlock::Number(n) => h.number() == &n,
+        });
+
+        if !expected_block {
+            debug!(
+                target: LOG_TARGET,
+                "Received block that was not requested. Requested {:?}, got {:?}.",
+                request.from,
+                block_header,
+            );
+
+            return Err(BadPeer(*peer_id, rep::NOT_REQUESTED));
+        }
+
+        if request.fields.contains(BlockAttributes::HEADER)
+            && blocks.iter().any(|b| b.header.is_none())
+        {
+            trace!(
+                target: LOG_TARGET,
+                "Missing requested header for a block in response from {peer_id}.",
+            );
+
+            return Err(BadPeer(*peer_id, rep::BAD_RESPONSE));
+        }
+
+        if request.fields.contains(BlockAttributes::BODY) && blocks.iter().any(|b| b.body.is_none())
+        {
+            trace!(
+                target: LOG_TARGET,
+                "Missing requested body for a block in response from {peer_id}.",
+            );
+
+            return Err(BadPeer(*peer_id, rep::BAD_RESPONSE));
+        }
+    }
+
+    for b in blocks {
+        if let Some(header) = &b.header {
+            let hash = header.hash();
+            if hash != b.hash {
+                debug!(
+                    target: LOG_TARGET,
+                    "Bad header received from {}. Expected hash {:?}, got {:?}",
+                    peer_id,
+                    b.hash,
+                    hash,
+                );
+                return Err(BadPeer(*peer_id, rep::BAD_BLOCK));
+            }
+        }
+        if let (Some(header), Some(body)) = (&b.header, &b.body) {
+            let expected = *header.extrinsics_root();
+            let got = HashingFor::<Block>::ordered_trie_root(
+                body.iter().map(Encode::encode).collect(),
+                sp_runtime::StateVersion::V0,
+            );
+            if expected != got {
+                debug!(
+                    target: LOG_TARGET,
+                    "Bad extrinsic root for a block {} received from {}. Expected {:?}, got {:?}",
+                    b.hash,
+                    peer_id,
+                    expected,
+                    got,
+                );
+                return Err(BadPeer(*peer_id, rep::BAD_BLOCK));
+            }
+        }
+    }
+
+    Ok(blocks
+        .first()
+        .and_then(|b| b.header.as_ref())
+        .map(|h| *h.number()))
+}
diff --git a/libp2p/sync/src/strategy/state.rs b/libp2p/sync/src/strategy/state.rs
new file mode 100644
index 000000000..e375c43ae
--- /dev/null
+++ b/libp2p/sync/src/strategy/state.rs
@@ -0,0 +1,832 @@
+// 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/>.
+
+//! State sync strategy.
+
+use crate::service::syncing_service::ProcessedBlock;
+use crate::{
+    schema::v1::StateResponse,
+    strategy::state_sync::{ImportResult, StateSync, StateSyncProvider},
+    types::{BadPeer, OpaqueStateRequest, OpaqueStateResponse, SyncState, SyncStatus},
+    LOG_TARGET,
+};
+use libp2p::PeerId;
+use log::{debug, error, trace};
+use sc_client_api::ProofProvider;
+use sc_consensus::IncomingBlock;
+use sc_network_common::sync::message::BlockAnnounce;
+use sp_consensus::BlockOrigin;
+use sp_runtime::{
+    traits::{Block as BlockT, Header, NumberFor},
+    Justifications, SaturatedConversion,
+};
+use std::{collections::HashMap, sync::Arc};
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+
+    /// Peer response data does not have requested bits.
+    pub const BAD_RESPONSE: Rep = Rep::new(-(1 << 12), "Incomplete response");
+
+    /// Reputation change for peers which send us a known bad state.
+    pub const BAD_STATE: Rep = Rep::new(-(1 << 29), "Bad state");
+}
+
+/// Action that should be performed on [`StateStrategy`]'s behalf.
+pub enum StateStrategyAction<B: BlockT> {
+    /// Send state request to peer.
+    SendStateRequest {
+        peer_id: PeerId,
+        request: OpaqueStateRequest,
+    },
+    /// Disconnect and report peer.
+    DropPeer(BadPeer),
+    /// Import blocks.
+    ImportBlocks {
+        origin: BlockOrigin,
+        blocks: Vec<IncomingBlock<B>>,
+    },
+    /// State sync has finished.
+    Finished,
+}
+
+enum PeerState {
+    Available,
+    DownloadingState,
+}
+
+impl PeerState {
+    fn is_available(&self) -> bool {
+        matches!(self, PeerState::Available)
+    }
+}
+
+struct Peer<B: BlockT> {
+    best_number: NumberFor<B>,
+    state: PeerState,
+}
+
+/// Syncing strategy that downloads and imports a recent state directly.
+pub struct StateStrategy<B: BlockT> {
+    state_sync: Box<dyn StateSyncProvider<B>>,
+    peers: HashMap<PeerId, Peer<B>>,
+    actions: Vec<StateStrategyAction<B>>,
+    succeded: bool,
+}
+
+impl<B: BlockT> StateStrategy<B> {
+    /// Create a new instance.
+    pub fn new<Client>(
+        client: Arc<Client>,
+        target_header: B::Header,
+        target_body: Option<Vec<B::Extrinsic>>,
+        target_justifications: Option<Justifications>,
+        skip_proof: bool,
+        initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
+    ) -> Self
+    where
+        Client: ProofProvider<B> + Send + Sync + 'static,
+    {
+        let peers = initial_peers
+            .map(|(peer_id, best_number)| {
+                (
+                    peer_id,
+                    Peer {
+                        best_number,
+                        state: PeerState::Available,
+                    },
+                )
+            })
+            .collect();
+        Self {
+            state_sync: Box::new(StateSync::new(
+                client,
+                target_header,
+                target_body,
+                target_justifications,
+                skip_proof,
+            )),
+            peers,
+            actions: Vec::new(),
+            succeded: false,
+        }
+    }
+
+    // Create a new instance with a custom state sync provider.
+    // Used in tests.
+    #[cfg(test)]
+    fn new_with_provider(
+        state_sync_provider: Box<dyn StateSyncProvider<B>>,
+        initial_peers: impl Iterator<Item = (PeerId, NumberFor<B>)>,
+    ) -> Self {
+        Self {
+            state_sync: state_sync_provider,
+            peers: initial_peers
+                .map(|(peer_id, best_number)| {
+                    (
+                        peer_id,
+                        Peer {
+                            best_number,
+                            state: PeerState::Available,
+                        },
+                    )
+                })
+                .collect(),
+            actions: Vec::new(),
+            succeded: false,
+        }
+    }
+
+    /// Notify that a new peer has connected.
+    pub fn add_peer(&mut self, peer_id: PeerId, _best_hash: B::Hash, best_number: NumberFor<B>) {
+        self.peers.insert(
+            peer_id,
+            Peer {
+                best_number,
+                state: PeerState::Available,
+            },
+        );
+    }
+
+    /// Notify that a peer has disconnected.
+    pub fn remove_peer(&mut self, peer_id: &PeerId) {
+        self.peers.remove(peer_id);
+    }
+
+    /// Submit a validated block announcement.
+    ///
+    /// Returns new best hash & best number of the peer if they are updated.
+    #[must_use]
+    pub fn on_validated_block_announce(
+        &mut self,
+        is_best: bool,
+        peer_id: PeerId,
+        announce: &BlockAnnounce<B::Header>,
+    ) -> Option<(B::Hash, NumberFor<B>)> {
+        is_best.then_some({
+            let best_number = *announce.header.number();
+            let best_hash = announce.header.hash();
+            if let Some(ref mut peer) = self.peers.get_mut(&peer_id) {
+                peer.best_number = best_number;
+            }
+            // Let `SyncingEngine` know that we should update the peer info.
+            (best_hash, best_number)
+        })
+    }
+
+    /// Process state response.
+    pub fn on_state_response(&mut self, peer_id: PeerId, response: OpaqueStateResponse) {
+        if let Err(bad_peer) = self.on_state_response_inner(peer_id, response) {
+            self.actions.push(StateStrategyAction::DropPeer(bad_peer));
+        }
+    }
+
+    fn on_state_response_inner(
+        &mut self,
+        peer_id: PeerId,
+        response: OpaqueStateResponse,
+    ) -> Result<(), BadPeer> {
+        if let Some(peer) = self.peers.get_mut(&peer_id) {
+            peer.state = PeerState::Available;
+        }
+
+        let response: Box<StateResponse> = response.0.downcast().map_err(|_error| {
+            error!(
+                target: LOG_TARGET,
+                "Failed to downcast opaque state response, this is an implementation bug."
+            );
+            debug_assert!(false);
+
+            BadPeer(peer_id, rep::BAD_RESPONSE)
+        })?;
+
+        debug!(
+            target: LOG_TARGET,
+            "Importing state data from {} with {} keys, {} proof nodes.",
+            peer_id,
+            response.entries.len(),
+            response.proof.len(),
+        );
+
+        match self.state_sync.import(*response) {
+            ImportResult::Import(hash, header, state, body, justifications) => {
+                let origin = BlockOrigin::NetworkInitialSync;
+                let block = IncomingBlock {
+                    hash,
+                    header: Some(header),
+                    body,
+                    indexed_body: None,
+                    justifications,
+                    origin: None,
+                    allow_missing_state: true,
+                    import_existing: true,
+                    skip_execution: true,
+                    state: Some(state),
+                };
+                debug!(target: LOG_TARGET, "State download is complete. Import is queued");
+                self.actions.push(StateStrategyAction::ImportBlocks {
+                    origin,
+                    blocks: vec![block],
+                });
+                Ok(())
+            }
+            ImportResult::Continue => Ok(()),
+            ImportResult::BadResponse => {
+                debug!(target: LOG_TARGET, "Bad state data received from {peer_id}");
+                Err(BadPeer(peer_id, rep::BAD_STATE))
+            }
+        }
+    }
+
+    /// A batch of blocks have been processed, with or without errors.
+    ///
+    /// Normally this should be called when target block with state is imported.
+    pub fn on_blocks_processed(
+        &mut self,
+        imported: usize,
+        count: usize,
+        results: Vec<ProcessedBlock<B, B::Hash>>,
+    ) {
+        trace!(target: LOG_TARGET, "State sync: imported {imported} of {count}.");
+
+        let results = results
+            .into_iter()
+            .filter_map(|(result, hash)| {
+                if hash == self.state_sync.target_hash() {
+                    Some(result)
+                } else {
+                    debug!(
+                        target: LOG_TARGET,
+                        "Unexpected block processed: {hash} with result {result:?}.",
+                    );
+                    None
+                }
+            })
+            .collect::<Vec<_>>();
+
+        if !results.is_empty() {
+            // We processed the target block
+            results
+                .iter()
+                .filter_map(|result| result.as_ref().err())
+                .for_each(|e| {
+                    error!(
+                        target: LOG_TARGET,
+                        "Failed to import target block with state: {e:?}."
+                    );
+                });
+            self.succeded |= results.into_iter().any(|result| result.is_ok());
+            self.actions.push(StateStrategyAction::Finished);
+        }
+    }
+
+    /// Produce state request.
+    fn state_request(&mut self) -> Option<(PeerId, OpaqueStateRequest)> {
+        if self.state_sync.is_complete() {
+            return None;
+        }
+
+        if self
+            .peers
+            .values()
+            .any(|peer| matches!(peer.state, PeerState::DownloadingState))
+        {
+            // Only one state request at a time is possible.
+            return None;
+        }
+
+        let peer_id =
+            self.schedule_next_peer(PeerState::DownloadingState, self.state_sync.target_number())?;
+        let request = self.state_sync.next_request();
+        trace!(
+            target: LOG_TARGET,
+            "New state request to {peer_id}: {request:?}.",
+        );
+        Some((peer_id, OpaqueStateRequest(Box::new(request))))
+    }
+
+    fn schedule_next_peer(
+        &mut self,
+        new_state: PeerState,
+        min_best_number: NumberFor<B>,
+    ) -> Option<PeerId> {
+        let mut targets: Vec<_> = self.peers.values().map(|p| p.best_number).collect();
+        if targets.is_empty() {
+            return None;
+        }
+        targets.sort();
+        let median = targets[targets.len() / 2];
+        let threshold = std::cmp::max(median, min_best_number);
+        // Find a random peer that is synced as much as peer majority and is above
+        // `min_best_number`.
+        for (peer_id, peer) in self.peers.iter_mut() {
+            if peer.state.is_available() && peer.best_number >= threshold {
+                peer.state = new_state;
+                return Some(*peer_id);
+            }
+        }
+        None
+    }
+
+    /// Returns the current sync status.
+    pub fn status(&self) -> SyncStatus<B> {
+        SyncStatus {
+            state: if self.state_sync.is_complete() {
+                SyncState::Idle
+            } else {
+                SyncState::Downloading {
+                    target: self.state_sync.target_number(),
+                }
+            },
+            best_seen_block: Some(self.state_sync.target_number()),
+            num_peers: self.peers.len().saturated_into(),
+            num_connected_peers: self.peers.len().saturated_into(),
+            queued_blocks: 0,
+            state_sync: Some(self.state_sync.progress()),
+            warp_sync: None,
+        }
+    }
+
+    /// Get the number of peers known to syncing.
+    pub fn num_peers(&self) -> usize {
+        self.peers.len()
+    }
+
+    /// Get actions that should be performed by the owner on [`WarpSync`]'s behalf
+    pub fn actions(&mut self) -> impl Iterator<Item = StateStrategyAction<B>> {
+        let state_request = self
+            .state_request()
+            .into_iter()
+            .map(|(peer_id, request)| StateStrategyAction::SendStateRequest { peer_id, request });
+        self.actions.extend(state_request);
+
+        std::mem::take(&mut self.actions).into_iter()
+    }
+
+    /// Check if state sync has succeded.
+    #[must_use]
+    pub fn is_succeded(&self) -> bool {
+        self.succeded
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::{
+        schema::v1::{StateRequest, StateResponse},
+        strategy::state_sync::{ImportResult, StateSyncProgress, StateSyncProvider},
+    };
+    use codec::Decode;
+    use sc_block_builder::BlockBuilderBuilder;
+    use sc_client_api::KeyValueStates;
+    use sc_consensus::{BlockImportError, BlockImportStatus, ImportedAux, ImportedState};
+    use sp_runtime::traits::Zero;
+    use substrate_test_runtime_client::{
+        runtime::{Block, Hash},
+        BlockBuilderExt, DefaultTestClientBuilderExt, TestClientBuilder, TestClientBuilderExt,
+    };
+
+    mockall::mock! {
+        pub StateSync<B: BlockT> {}
+
+        impl<B: BlockT> StateSyncProvider<B> for StateSync<B> {
+            fn import(&mut self, response: StateResponse) -> ImportResult<B>;
+            fn next_request(&self) -> StateRequest;
+            fn is_complete(&self) -> bool;
+            fn target_number(&self) -> NumberFor<B>;
+            fn target_hash(&self) -> B::Hash;
+            fn progress(&self) -> StateSyncProgress;
+        }
+    }
+
+    #[test]
+    fn no_peer_is_scheduled_if_no_peers_connected() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+
+        let mut state_strategy =
+            StateStrategy::new(client, target_header, None, None, false, std::iter::empty());
+
+        assert!(state_strategy
+            .schedule_next_peer(PeerState::DownloadingState, Zero::zero())
+            .is_none());
+    }
+
+    #[test]
+    fn at_least_median_synced_peer_is_scheduled() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        for _ in 0..100 {
+            let peers = (1..=10)
+                .map(|best_number| (PeerId::random(), best_number))
+                .collect::<HashMap<_, _>>();
+            let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
+
+            let mut state_strategy = StateStrategy::new(
+                client.clone(),
+                target_block.header().clone(),
+                None,
+                None,
+                false,
+                initial_peers,
+            );
+
+            let peer_id =
+                state_strategy.schedule_next_peer(PeerState::DownloadingState, Zero::zero());
+            assert!(*peers.get(&peer_id.unwrap()).unwrap() >= 6);
+        }
+    }
+
+    #[test]
+    fn min_best_number_peer_is_scheduled() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        for _ in 0..10 {
+            let peers = (1..=10)
+                .map(|best_number| (PeerId::random(), best_number))
+                .collect::<HashMap<_, _>>();
+            let initial_peers = peers.iter().map(|(p, n)| (*p, *n));
+
+            let mut state_strategy = StateStrategy::new(
+                client.clone(),
+                target_block.header().clone(),
+                None,
+                None,
+                false,
+                initial_peers,
+            );
+
+            let peer_id = state_strategy.schedule_next_peer(PeerState::DownloadingState, 10);
+            assert!(*peers.get(&peer_id.unwrap()).unwrap() == 10);
+        }
+    }
+
+    #[test]
+    fn state_request_contains_correct_hash() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
+
+        let mut state_strategy = StateStrategy::new(
+            client.clone(),
+            target_block.header().clone(),
+            None,
+            None,
+            false,
+            initial_peers,
+        );
+
+        let (_peer_id, mut opaque_request) = state_strategy.state_request().unwrap();
+        let request: &mut StateRequest = opaque_request.0.downcast_mut().unwrap();
+        let hash = Hash::decode(&mut &*request.block).unwrap();
+
+        assert_eq!(hash, target_block.header().hash());
+    }
+
+    #[test]
+    fn no_parallel_state_requests() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
+
+        let mut state_strategy = StateStrategy::new(
+            client.clone(),
+            target_block.header().clone(),
+            None,
+            None,
+            false,
+            initial_peers,
+        );
+
+        // First request is sent.
+        assert!(state_strategy.state_request().is_some());
+
+        // No parallel request is sent.
+        assert!(state_strategy.state_request().is_none());
+    }
+
+    #[test]
+    fn received_state_response_makes_peer_available_again() {
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        state_sync_provider
+            .expect_import()
+            .return_once(|_| ImportResult::Continue);
+        let peer_id = PeerId::random();
+        let initial_peers = std::iter::once((peer_id, 10));
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
+        // Manually set the peer's state.
+        state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
+
+        let dummy_response = OpaqueStateResponse(Box::<StateResponse>::default());
+        state_strategy.on_state_response(peer_id, dummy_response);
+
+        assert!(state_strategy
+            .peers
+            .get(&peer_id)
+            .unwrap()
+            .state
+            .is_available());
+    }
+
+    #[test]
+    fn bad_state_response_drops_peer() {
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        // Provider says that state response is bad.
+        state_sync_provider
+            .expect_import()
+            .return_once(|_| ImportResult::BadResponse);
+        let peer_id = PeerId::random();
+        let initial_peers = std::iter::once((peer_id, 10));
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
+        // Manually set the peer's state.
+        state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
+        let dummy_response = OpaqueStateResponse(Box::<StateResponse>::default());
+        // Receiving response drops the peer.
+        assert!(matches!(
+            state_strategy.on_state_response_inner(peer_id, dummy_response),
+            Err(BadPeer(id, _rep)) if id == peer_id,
+        ));
+    }
+
+    #[test]
+    fn partial_state_response_doesnt_generate_actions() {
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        // Sync provider says that the response is partial.
+        state_sync_provider
+            .expect_import()
+            .return_once(|_| ImportResult::Continue);
+        let peer_id = PeerId::random();
+        let initial_peers = std::iter::once((peer_id, 10));
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
+        // Manually set the peer's state .
+        state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
+
+        let dummy_response = OpaqueStateResponse(Box::<StateResponse>::default());
+        state_strategy.on_state_response(peer_id, dummy_response);
+
+        // No actions generated.
+        assert_eq!(state_strategy.actions.len(), 0)
+    }
+
+    #[test]
+    fn complete_state_response_leads_to_block_import() {
+        // Build block to use for checks.
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut block_builder = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap();
+        block_builder
+            .push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6]))
+            .unwrap();
+        let block = block_builder.build().unwrap().block;
+        let header = block.header().clone();
+        let hash = header.hash();
+        let body = Some(block.extrinsics().to_vec());
+        let state = ImportedState {
+            block: hash,
+            state: KeyValueStates(Vec::new()),
+        };
+        let justifications = Some(Justifications::from((*b"FRNK", Vec::new())));
+
+        // Prepare `StateSync`
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        let import = ImportResult::Import(
+            hash,
+            header.clone(),
+            state.clone(),
+            body.clone(),
+            justifications.clone(),
+        );
+        state_sync_provider
+            .expect_import()
+            .return_once(move |_| import);
+
+        // Reference values to check against.
+        let expected_origin = BlockOrigin::NetworkInitialSync;
+        let expected_block = IncomingBlock {
+            hash,
+            header: Some(header),
+            body,
+            indexed_body: None,
+            justifications,
+            origin: None,
+            allow_missing_state: true,
+            import_existing: true,
+            skip_execution: true,
+            state: Some(state),
+        };
+        let expected_blocks = vec![expected_block];
+
+        // Prepare `StateStrategy`.
+        let peer_id = PeerId::random();
+        let initial_peers = std::iter::once((peer_id, 10));
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
+        // Manually set the peer's state .
+        state_strategy.peers.get_mut(&peer_id).unwrap().state = PeerState::DownloadingState;
+
+        // Receive response.
+        let dummy_response = OpaqueStateResponse(Box::<StateResponse>::default());
+        state_strategy.on_state_response(peer_id, dummy_response);
+
+        assert_eq!(state_strategy.actions.len(), 1);
+        assert!(matches!(
+            &state_strategy.actions[0],
+            StateStrategyAction::ImportBlocks { origin, blocks }
+                if *origin == expected_origin && *blocks == expected_blocks,
+        ));
+    }
+
+    #[test]
+    fn importing_unknown_block_doesnt_finish_strategy() {
+        let target_hash = Hash::random();
+        let unknown_hash = Hash::random();
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        state_sync_provider
+            .expect_target_hash()
+            .return_const(target_hash);
+
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
+
+        // Unknown block imported.
+        state_strategy.on_blocks_processed(
+            1,
+            1,
+            vec![(
+                Ok(BlockImportStatus::ImportedUnknown(
+                    1,
+                    ImportedAux::default(),
+                    None,
+                )),
+                unknown_hash,
+            )],
+        );
+
+        // No actions generated.
+        assert_eq!(state_strategy.actions.len(), 0);
+    }
+
+    #[test]
+    fn succesfully_importing_target_block_finishes_strategy() {
+        let target_hash = Hash::random();
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        state_sync_provider
+            .expect_target_hash()
+            .return_const(target_hash);
+
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
+
+        // Target block imported.
+        state_strategy.on_blocks_processed(
+            1,
+            1,
+            vec![(
+                Ok(BlockImportStatus::ImportedUnknown(
+                    1,
+                    ImportedAux::default(),
+                    None,
+                )),
+                target_hash,
+            )],
+        );
+
+        // Strategy finishes.
+        assert_eq!(state_strategy.actions.len(), 1);
+        assert!(matches!(
+            &state_strategy.actions[0],
+            StateStrategyAction::Finished
+        ));
+    }
+
+    #[test]
+    fn failure_to_import_target_block_finishes_strategy() {
+        let target_hash = Hash::random();
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        state_sync_provider
+            .expect_target_hash()
+            .return_const(target_hash);
+
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), std::iter::empty());
+
+        // Target block import failed.
+        state_strategy.on_blocks_processed(
+            1,
+            1,
+            vec![(
+                Err(BlockImportError::VerificationFailed(
+                    None,
+                    String::from("test-error"),
+                )),
+                target_hash,
+            )],
+        );
+
+        // Strategy finishes.
+        assert_eq!(state_strategy.actions.len(), 1);
+        assert!(matches!(
+            &state_strategy.actions[0],
+            StateStrategyAction::Finished
+        ));
+    }
+
+    #[test]
+    fn finished_strategy_doesnt_generate_more_actions() {
+        let target_hash = Hash::random();
+        let mut state_sync_provider = MockStateSync::<Block>::new();
+        state_sync_provider
+            .expect_target_hash()
+            .return_const(target_hash);
+        state_sync_provider.expect_is_complete().return_const(true);
+
+        // Get enough peers for possible spurious requests.
+        let initial_peers = (1..=10).map(|best_number| (PeerId::random(), best_number));
+
+        let mut state_strategy =
+            StateStrategy::new_with_provider(Box::new(state_sync_provider), initial_peers);
+
+        state_strategy.on_blocks_processed(
+            1,
+            1,
+            vec![(
+                Ok(BlockImportStatus::ImportedUnknown(
+                    1,
+                    ImportedAux::default(),
+                    None,
+                )),
+                target_hash,
+            )],
+        );
+
+        // Strategy finishes.
+        let actions = state_strategy.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(&actions[0], StateStrategyAction::Finished));
+
+        // No more actions generated.
+        assert_eq!(state_strategy.actions().count(), 0);
+    }
+}
diff --git a/libp2p/sync/src/strategy/state_sync.rs b/libp2p/sync/src/strategy/state_sync.rs
new file mode 100644
index 000000000..bfa567399
--- /dev/null
+++ b/libp2p/sync/src/strategy/state_sync.rs
@@ -0,0 +1,340 @@
+// 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/>.
+
+//! State sync support.
+
+use crate::{
+    schema::v1::{StateEntry, StateRequest, StateResponse},
+    LOG_TARGET,
+};
+use codec::{Decode, Encode};
+use log::debug;
+use sc_client_api::{CompactProof, ProofProvider};
+use sc_consensus::ImportedState;
+use smallvec::SmallVec;
+use sp_core::storage::well_known_keys;
+use sp_runtime::{
+    traits::{Block as BlockT, Header, NumberFor},
+    Justifications,
+};
+use std::{collections::HashMap, fmt, sync::Arc};
+
+/// Generic state sync provider. Used for mocking in tests.
+pub trait StateSyncProvider<B: BlockT>: Send + Sync {
+    /// Validate and import a state response.
+    fn import(&mut self, response: StateResponse) -> ImportResult<B>;
+    /// Produce next state request.
+    fn next_request(&self) -> StateRequest;
+    /// Check if the state is complete.
+    fn is_complete(&self) -> bool;
+    /// Returns target block number.
+    fn target_number(&self) -> NumberFor<B>;
+    /// Returns target block hash.
+    fn target_hash(&self) -> B::Hash;
+    /// Returns state sync estimated progress.
+    fn progress(&self) -> StateSyncProgress;
+}
+
+// Reported state sync phase.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum StateSyncPhase {
+    // State download in progress.
+    DownloadingState,
+    // Download is complete, state is being imported.
+    ImportingState,
+}
+
+impl fmt::Display for StateSyncPhase {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::DownloadingState => write!(f, "Downloading state"),
+            Self::ImportingState => write!(f, "Importing state"),
+        }
+    }
+}
+
+/// Reported state download progress.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub struct StateSyncProgress {
+    /// Estimated download percentage.
+    pub percentage: u32,
+    /// Total state size in bytes downloaded so far.
+    pub size: u64,
+    /// Current state sync phase.
+    pub phase: StateSyncPhase,
+}
+
+/// Import state chunk result.
+pub enum ImportResult<B: BlockT> {
+    /// State is complete and ready for import.
+    Import(
+        B::Hash,
+        B::Header,
+        ImportedState<B>,
+        Option<Vec<B::Extrinsic>>,
+        Option<Justifications>,
+    ),
+    /// Continue downloading.
+    Continue,
+    /// Bad state chunk.
+    BadResponse,
+}
+
+/// State sync state machine. Accumulates partial state data until it
+/// is ready to be imported.
+pub struct StateSync<B: BlockT, Client> {
+    target_block: B::Hash,
+    target_header: B::Header,
+    target_root: B::Hash,
+    target_body: Option<Vec<B::Extrinsic>>,
+    target_justifications: Option<Justifications>,
+    last_key: SmallVec<[Vec<u8>; 2]>,
+    state: HashMap<Vec<u8>, StateSyncInner>,
+    complete: bool,
+    client: Arc<Client>,
+    imported_bytes: u64,
+    skip_proof: bool,
+}
+
+pub type StateSyncInner = (Vec<(Vec<u8>, Vec<u8>)>, Vec<Vec<u8>>);
+
+impl<B, Client> StateSync<B, Client>
+where
+    B: BlockT,
+    Client: ProofProvider<B> + Send + Sync + 'static,
+{
+    ///  Create a new instance.
+    pub fn new(
+        client: Arc<Client>,
+        target_header: B::Header,
+        target_body: Option<Vec<B::Extrinsic>>,
+        target_justifications: Option<Justifications>,
+        skip_proof: bool,
+    ) -> Self {
+        Self {
+            client,
+            target_block: target_header.hash(),
+            target_root: *target_header.state_root(),
+            target_header,
+            target_body,
+            target_justifications,
+            last_key: SmallVec::default(),
+            state: HashMap::default(),
+            complete: false,
+            imported_bytes: 0,
+            skip_proof,
+        }
+    }
+}
+
+impl<B, Client> StateSyncProvider<B> for StateSync<B, Client>
+where
+    B: BlockT,
+    Client: ProofProvider<B> + Send + Sync + 'static,
+{
+    ///  Validate and import a state response.
+    fn import(&mut self, response: StateResponse) -> ImportResult<B> {
+        if response.entries.is_empty() && response.proof.is_empty() {
+            debug!(target: LOG_TARGET, "Bad state response");
+            return ImportResult::BadResponse;
+        }
+        if !self.skip_proof && response.proof.is_empty() {
+            debug!(target: LOG_TARGET, "Missing proof");
+            return ImportResult::BadResponse;
+        }
+        let complete = if !self.skip_proof {
+            debug!(target: LOG_TARGET, "Importing state from {} trie nodes", response.proof.len());
+            let proof_size = response.proof.len() as u64;
+            let proof = match CompactProof::decode(&mut response.proof.as_ref()) {
+                Ok(proof) => proof,
+                Err(e) => {
+                    debug!(target: LOG_TARGET, "Error decoding proof: {:?}", e);
+                    return ImportResult::BadResponse;
+                }
+            };
+            let (values, completed) = match self.client.verify_range_proof(
+                self.target_root,
+                proof,
+                self.last_key.as_slice(),
+            ) {
+                Err(e) => {
+                    debug!(
+                        target: LOG_TARGET,
+                        "StateResponse failed proof verification: {}",
+                        e,
+                    );
+                    return ImportResult::BadResponse;
+                }
+                Ok(values) => values,
+            };
+            debug!(target: LOG_TARGET, "Imported with {} keys", values.len());
+
+            let complete = completed == 0;
+            if !complete && !values.update_last_key(completed, &mut self.last_key) {
+                debug!(target: LOG_TARGET, "Error updating key cursor, depth: {}", completed);
+            };
+
+            for values in values.0 {
+                let key_values = if values.state_root.is_empty() {
+                    // Read child trie roots.
+                    values
+                        .key_values
+                        .into_iter()
+                        .filter(|key_value| {
+                            if well_known_keys::is_child_storage_key(key_value.0.as_slice()) {
+                                self.state
+                                    .entry(key_value.1.clone())
+                                    .or_default()
+                                    .1
+                                    .push(key_value.0.clone());
+                                false
+                            } else {
+                                true
+                            }
+                        })
+                        .collect()
+                } else {
+                    values.key_values
+                };
+                let entry = self.state.entry(values.state_root).or_default();
+                if !entry.0.is_empty() && entry.1.len() > 1 {
+                    // Already imported child_trie with same root.
+                    // Warning this will not work with parallel download.
+                } else if entry.0.is_empty() {
+                    for (key, _value) in key_values.iter() {
+                        self.imported_bytes += key.len() as u64;
+                    }
+
+                    entry.0 = key_values;
+                } else {
+                    for (key, value) in key_values {
+                        self.imported_bytes += key.len() as u64;
+                        entry.0.push((key, value))
+                    }
+                }
+            }
+            self.imported_bytes += proof_size;
+            complete
+        } else {
+            let mut complete = true;
+            // if the trie is a child trie and one of its parent trie is empty,
+            // the parent cursor stays valid.
+            // Empty parent trie content only happens when all the response content
+            // is part of a single child trie.
+            if self.last_key.len() == 2 && response.entries[0].entries.is_empty() {
+                // Do not remove the parent trie position.
+                self.last_key.pop();
+            } else {
+                self.last_key.clear();
+            }
+            for state in response.entries {
+                debug!(
+                    target: LOG_TARGET,
+                    "Importing state from {:?} to {:?}",
+                    state.entries.last().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
+                    state.entries.first().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
+                );
+
+                if !state.complete {
+                    if let Some(e) = state.entries.last() {
+                        self.last_key.push(e.key.clone());
+                    }
+                    complete = false;
+                }
+                let is_top = state.state_root.is_empty();
+                let entry = self.state.entry(state.state_root).or_default();
+                if !entry.0.is_empty() && entry.1.len() > 1 {
+                    // Already imported child trie with same root.
+                } else {
+                    let mut child_roots = Vec::new();
+                    for StateEntry { key, value } in state.entries {
+                        // Skip all child key root (will be recalculated on import).
+                        if is_top && well_known_keys::is_child_storage_key(key.as_slice()) {
+                            child_roots.push((value, key));
+                        } else {
+                            self.imported_bytes += key.len() as u64;
+                            entry.0.push((key, value))
+                        }
+                    }
+                    for (root, storage_key) in child_roots {
+                        self.state.entry(root).or_default().1.push(storage_key);
+                    }
+                }
+            }
+            complete
+        };
+        if complete {
+            self.complete = true;
+            ImportResult::Import(
+                self.target_block,
+                self.target_header.clone(),
+                ImportedState {
+                    block: self.target_block,
+                    state: std::mem::take(&mut self.state).into(),
+                },
+                self.target_body.clone(),
+                self.target_justifications.clone(),
+            )
+        } else {
+            ImportResult::Continue
+        }
+    }
+
+    /// Produce next state request.
+    fn next_request(&self) -> StateRequest {
+        StateRequest {
+            block: self.target_block.encode(),
+            start: self.last_key.clone().into_vec(),
+            no_proof: self.skip_proof,
+        }
+    }
+
+    /// Check if the state is complete.
+    fn is_complete(&self) -> bool {
+        self.complete
+    }
+
+    /// Returns target block number.
+    fn target_number(&self) -> NumberFor<B> {
+        *self.target_header.number()
+    }
+
+    /// Returns target block hash.
+    fn target_hash(&self) -> B::Hash {
+        self.target_block
+    }
+
+    /// Returns state sync estimated progress.
+    fn progress(&self) -> StateSyncProgress {
+        let cursor = *self
+            .last_key
+            .first()
+            .and_then(|last| last.first())
+            .unwrap_or(&0u8);
+        let percent_done = cursor as u32 * 100 / 256;
+        StateSyncProgress {
+            percentage: percent_done,
+            size: self.imported_bytes,
+            phase: if self.complete {
+                StateSyncPhase::ImportingState
+            } else {
+                StateSyncPhase::DownloadingState
+            },
+        }
+    }
+}
diff --git a/libp2p/sync/src/strategy/warp.rs b/libp2p/sync/src/strategy/warp.rs
new file mode 100644
index 000000000..36f9baa37
--- /dev/null
+++ b/libp2p/sync/src/strategy/warp.rs
@@ -0,0 +1,1551 @@
+// 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/>.
+
+//! Warp syncing strategy. Bootstraps chain by downloading warp proofs and state.
+
+pub use sp_consensus_grandpa::{AuthorityList, SetId};
+
+use crate::{
+    strategy::chain_sync::validate_blocks,
+    types::{BadPeer, SyncState, SyncStatus},
+    LOG_TARGET,
+};
+use codec::{Decode, Encode};
+use futures::channel::oneshot;
+use libp2p::PeerId;
+use log::{debug, error, trace};
+use sc_network_common::sync::message::{
+    BlockAnnounce, BlockAttributes, BlockData, BlockRequest, Direction, FromBlock,
+};
+use sp_blockchain::HeaderBackend;
+use sp_runtime::{
+    traits::{Block as BlockT, Header, NumberFor, Zero},
+    Justifications, SaturatedConversion,
+};
+use std::{collections::HashMap, fmt, sync::Arc};
+
+/// Number of peers that need to be connected before warp sync is started.
+const MIN_PEERS_TO_START_WARP_SYNC: usize = 3;
+
+/// Scale-encoded warp sync proof response.
+pub struct EncodedProof(pub Vec<u8>);
+
+/// Warp sync request
+#[derive(Encode, Decode, Debug, Clone)]
+pub struct WarpProofRequest<B: BlockT> {
+    /// Start collecting proofs from this block.
+    pub begin: B::Hash,
+}
+
+/// Proof verification result.
+pub enum VerificationResult<Block: BlockT> {
+    /// Proof is valid, but the target was not reached.
+    Partial(SetId, AuthorityList, Block::Hash),
+    /// Target finality is proved.
+    Complete(SetId, AuthorityList, Block::Header),
+}
+
+/// Warp sync backend. Handles retrieving and verifying warp sync proofs.
+pub trait WarpSyncProvider<Block: BlockT>: Send + Sync {
+    /// Generate proof starting at given block hash. The proof is accumulated until maximum proof
+    /// size is reached.
+    fn generate(
+        &self,
+        start: Block::Hash,
+    ) -> Result<EncodedProof, Box<dyn std::error::Error + Send + Sync>>;
+    /// Verify warp proof against current set of authorities.
+    fn verify(
+        &self,
+        proof: &EncodedProof,
+        set_id: SetId,
+        authorities: AuthorityList,
+    ) -> Result<VerificationResult<Block>, Box<dyn std::error::Error + Send + Sync>>;
+    /// Get current list of authorities. This is supposed to be genesis authorities when starting
+    /// sync.
+    fn current_authorities(&self) -> AuthorityList;
+}
+
+mod rep {
+    use sc_network::ReputationChange as Rep;
+
+    /// Unexpected response received form a peer
+    pub const UNEXPECTED_RESPONSE: Rep = Rep::new(-(1 << 29), "Unexpected response");
+
+    /// Peer provided invalid warp proof data
+    pub const BAD_WARP_PROOF: Rep = Rep::new(-(1 << 29), "Bad warp proof");
+
+    /// Peer did not provide us with advertised block data.
+    pub const NO_BLOCK: Rep = Rep::new(-(1 << 29), "No requested block data");
+
+    /// Reputation change for peers which send us non-requested block data.
+    pub const NOT_REQUESTED: Rep = Rep::new(-(1 << 29), "Not requested block data");
+
+    /// Reputation change for peers which send us a block which we fail to verify.
+    pub const VERIFICATION_FAIL: Rep = Rep::new(-(1 << 29), "Block verification failed");
+}
+
+/// Reported warp sync phase.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum WarpSyncPhase<Block: BlockT> {
+    /// Waiting for peers to connect.
+    AwaitingPeers { required_peers: usize },
+    /// Waiting for target block to be received.
+    AwaitingTargetBlock,
+    /// Downloading and verifying grandpa warp proofs.
+    DownloadingWarpProofs,
+    /// Downloading target block.
+    DownloadingTargetBlock,
+    /// Downloading state data.
+    DownloadingState,
+    /// Importing state.
+    ImportingState,
+    /// Downloading block history.
+    DownloadingBlocks(NumberFor<Block>),
+    /// Warp sync is complete.
+    Complete,
+}
+
+impl<Block: BlockT> fmt::Display for WarpSyncPhase<Block> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::AwaitingPeers { required_peers } => {
+                write!(f, "Waiting for {required_peers} peers to be connected")
+            }
+            Self::AwaitingTargetBlock => write!(f, "Waiting for target block to be received"),
+            Self::DownloadingWarpProofs => write!(f, "Downloading finality proofs"),
+            Self::DownloadingTargetBlock => write!(f, "Downloading target block"),
+            Self::DownloadingState => write!(f, "Downloading state"),
+            Self::ImportingState => write!(f, "Importing state"),
+            Self::DownloadingBlocks(n) => write!(f, "Downloading block history (#{})", n),
+            Self::Complete => write!(f, "Warp sync is complete"),
+        }
+    }
+}
+
+/// Reported warp sync progress.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub struct WarpSyncProgress<Block: BlockT> {
+    /// Estimated download percentage.
+    pub phase: WarpSyncPhase<Block>,
+    /// Total bytes downloaded so far.
+    pub total_bytes: u64,
+}
+
+/// The different types of warp syncing, passed to `build_network`.
+pub enum WarpSyncParams<Block: BlockT> {
+    /// Standard warp sync for the chain.
+    WithProvider(Arc<dyn WarpSyncProvider<Block>>),
+    /// Skip downloading proofs and wait for a header of the state that should be downloaded.
+    ///
+    /// It is expected that the header provider ensures that the header is trusted.
+    WaitForTarget(oneshot::Receiver<<Block as BlockT>::Header>),
+}
+
+/// Warp sync configuration as accepted by [`WarpSync`].
+pub enum WarpSyncConfig<Block: BlockT> {
+    /// Standard warp sync for the chain.
+    WithProvider(Arc<dyn WarpSyncProvider<Block>>),
+    /// Skip downloading proofs and wait for a header of the state that should be downloaded.
+    ///
+    /// It is expected that the header provider ensures that the header is trusted.
+    WaitForTarget,
+}
+
+impl<Block: BlockT> WarpSyncParams<Block> {
+    /// Split `WarpSyncParams` into `WarpSyncConfig` and warp sync target block header receiver.
+    pub fn split(
+        self,
+    ) -> (
+        WarpSyncConfig<Block>,
+        Option<oneshot::Receiver<<Block as BlockT>::Header>>,
+    ) {
+        match self {
+            WarpSyncParams::WithProvider(provider) => {
+                (WarpSyncConfig::WithProvider(provider), None)
+            }
+            WarpSyncParams::WaitForTarget(rx) => (WarpSyncConfig::WaitForTarget, Some(rx)),
+        }
+    }
+}
+
+/// Warp sync phase used by warp sync state machine.
+enum Phase<B: BlockT> {
+    /// Waiting for enough peers to connect.
+    WaitingForPeers {
+        warp_sync_provider: Arc<dyn WarpSyncProvider<B>>,
+    },
+    /// Downloading warp proofs.
+    WarpProof {
+        set_id: SetId,
+        authorities: AuthorityList,
+        last_hash: B::Hash,
+        warp_sync_provider: Arc<dyn WarpSyncProvider<B>>,
+    },
+    /// Waiting for target block to be set externally if we skip warp proofs downloading,
+    /// and start straight from the target block (used by parachains warp sync).
+    PendingTargetBlock,
+    /// Downloading target block.
+    TargetBlock(B::Header),
+    /// Warp sync is complete.
+    Complete,
+}
+
+enum PeerState {
+    Available,
+    DownloadingProofs,
+    DownloadingTargetBlock,
+}
+
+impl PeerState {
+    fn is_available(&self) -> bool {
+        matches!(self, PeerState::Available)
+    }
+}
+
+struct Peer<B: BlockT> {
+    best_number: NumberFor<B>,
+    state: PeerState,
+}
+
+/// Action that should be performed on [`WarpSync`]'s behalf.
+pub enum WarpSyncAction<B: BlockT> {
+    /// Send warp proof request to peer.
+    SendWarpProofRequest {
+        peer_id: PeerId,
+        request: WarpProofRequest<B>,
+    },
+    /// Send block request to peer. Always implies dropping a stale block request to the same peer.
+    SendBlockRequest {
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+    },
+    /// Disconnect and report peer.
+    DropPeer(BadPeer),
+    /// Warp sync has finished.
+    Finished,
+}
+
+pub struct WarpSyncResult<B: BlockT> {
+    pub target_header: B::Header,
+    pub target_body: Option<Vec<B::Extrinsic>>,
+    pub target_justifications: Option<Justifications>,
+}
+
+/// Warp sync state machine. Accumulates warp proofs and state.
+pub struct WarpSync<B: BlockT, Client> {
+    phase: Phase<B>,
+    client: Arc<Client>,
+    total_proof_bytes: u64,
+    total_state_bytes: u64,
+    peers: HashMap<PeerId, Peer<B>>,
+    actions: Vec<WarpSyncAction<B>>,
+    result: Option<WarpSyncResult<B>>,
+}
+
+impl<B, Client> WarpSync<B, Client>
+where
+    B: BlockT,
+    Client: HeaderBackend<B> + 'static,
+{
+    /// Create a new instance. When passing a warp sync provider we will be checking for proof and
+    /// authorities. Alternatively we can pass a target block when we want to skip downloading
+    /// proofs, in this case we will continue polling until the target block is known.
+    pub fn new(client: Arc<Client>, warp_sync_config: WarpSyncConfig<B>) -> Self {
+        if client.info().finalized_state.is_some() {
+            error!(
+                target: LOG_TARGET,
+                "Can't use warp sync mode with a partially synced database. Reverting to full sync mode."
+            );
+            return Self {
+                client,
+                phase: Phase::Complete,
+                total_proof_bytes: 0,
+                total_state_bytes: 0,
+                peers: HashMap::new(),
+                actions: vec![WarpSyncAction::Finished],
+                result: None,
+            };
+        }
+
+        let phase = match warp_sync_config {
+            WarpSyncConfig::WithProvider(warp_sync_provider) => {
+                Phase::WaitingForPeers { warp_sync_provider }
+            }
+            WarpSyncConfig::WaitForTarget => Phase::PendingTargetBlock,
+        };
+
+        Self {
+            client,
+            phase,
+            total_proof_bytes: 0,
+            total_state_bytes: 0,
+            peers: HashMap::new(),
+            actions: Vec::new(),
+            result: None,
+        }
+    }
+
+    /// Set target block externally in case we skip warp proof downloading.
+    pub fn set_target_block(&mut self, header: B::Header) {
+        let Phase::PendingTargetBlock = self.phase else {
+            error!(
+                target: LOG_TARGET,
+                "Attempt to set warp sync target block in invalid phase.",
+            );
+            debug_assert!(false);
+            return;
+        };
+
+        self.phase = Phase::TargetBlock(header);
+    }
+
+    /// Notify that a new peer has connected.
+    pub fn add_peer(&mut self, peer_id: PeerId, _best_hash: B::Hash, best_number: NumberFor<B>) {
+        self.peers.insert(
+            peer_id,
+            Peer {
+                best_number,
+                state: PeerState::Available,
+            },
+        );
+
+        self.try_to_start_warp_sync();
+    }
+
+    /// Notify that a peer has disconnected.
+    pub fn remove_peer(&mut self, peer_id: &PeerId) {
+        self.peers.remove(peer_id);
+    }
+
+    /// Submit a validated block announcement.
+    ///
+    /// Returns new best hash & best number of the peer if they are updated.
+    #[must_use]
+    pub fn on_validated_block_announce(
+        &mut self,
+        is_best: bool,
+        peer_id: PeerId,
+        announce: &BlockAnnounce<B::Header>,
+    ) -> Option<(B::Hash, NumberFor<B>)> {
+        is_best.then_some({
+            let best_number = *announce.header.number();
+            let best_hash = announce.header.hash();
+            if let Some(ref mut peer) = self.peers.get_mut(&peer_id) {
+                peer.best_number = best_number;
+            }
+            // Let `SyncingEngine` know that we should update the peer info.
+            (best_hash, best_number)
+        })
+    }
+
+    /// Start warp sync as soon as we have enough peers.
+    fn try_to_start_warp_sync(&mut self) {
+        let Phase::WaitingForPeers { warp_sync_provider } = &self.phase else {
+            return;
+        };
+
+        if self.peers.len() < MIN_PEERS_TO_START_WARP_SYNC {
+            return;
+        }
+
+        self.phase = Phase::WarpProof {
+            set_id: 0,
+            authorities: warp_sync_provider.current_authorities(),
+            last_hash: self.client.info().genesis_hash,
+            warp_sync_provider: Arc::clone(warp_sync_provider),
+        };
+        trace!(target: LOG_TARGET, "Started warp sync with {} peers.", self.peers.len());
+    }
+
+    /// Process warp proof response.
+    pub fn on_warp_proof_response(&mut self, peer_id: &PeerId, response: EncodedProof) {
+        if let Some(peer) = self.peers.get_mut(peer_id) {
+            peer.state = PeerState::Available;
+        }
+
+        let Phase::WarpProof {
+            set_id,
+            authorities,
+            last_hash,
+            warp_sync_provider,
+        } = &mut self.phase
+        else {
+            debug!(target: LOG_TARGET, "Unexpected warp proof response");
+            self.actions.push(WarpSyncAction::DropPeer(BadPeer(
+                *peer_id,
+                rep::UNEXPECTED_RESPONSE,
+            )));
+            return;
+        };
+
+        match warp_sync_provider.verify(&response, *set_id, authorities.clone()) {
+            Err(e) => {
+                debug!(target: LOG_TARGET, "Bad warp proof response: {}", e);
+                self.actions.push(WarpSyncAction::DropPeer(BadPeer(
+                    *peer_id,
+                    rep::BAD_WARP_PROOF,
+                )))
+            }
+            Ok(VerificationResult::Partial(new_set_id, new_authorities, new_last_hash)) => {
+                log::debug!(target: LOG_TARGET, "Verified partial proof, set_id={:?}", new_set_id);
+                *set_id = new_set_id;
+                *authorities = new_authorities;
+                *last_hash = new_last_hash;
+                self.total_proof_bytes += response.0.len() as u64;
+            }
+            Ok(VerificationResult::Complete(new_set_id, _, header)) => {
+                log::debug!(
+                    target: LOG_TARGET,
+                    "Verified complete proof, set_id={:?}. Continuing with target block download: {} ({}).",
+                    new_set_id,
+                    header.hash(),
+                    header.number(),
+                );
+                self.total_proof_bytes += response.0.len() as u64;
+                self.phase = Phase::TargetBlock(header);
+            }
+        }
+    }
+
+    /// Process (target) block response.
+    pub fn on_block_response(
+        &mut self,
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+        blocks: Vec<BlockData<B>>,
+    ) {
+        if let Err(bad_peer) = self.on_block_response_inner(peer_id, request, blocks) {
+            self.actions.push(WarpSyncAction::DropPeer(bad_peer));
+        }
+    }
+
+    fn on_block_response_inner(
+        &mut self,
+        peer_id: PeerId,
+        request: BlockRequest<B>,
+        mut blocks: Vec<BlockData<B>>,
+    ) -> Result<(), BadPeer> {
+        if let Some(peer) = self.peers.get_mut(&peer_id) {
+            peer.state = PeerState::Available;
+        }
+
+        let Phase::TargetBlock(header) = &mut self.phase else {
+            debug!(target: LOG_TARGET, "Unexpected target block response from {peer_id}");
+            return Err(BadPeer(peer_id, rep::UNEXPECTED_RESPONSE));
+        };
+
+        if blocks.is_empty() {
+            debug!(
+                target: LOG_TARGET,
+                "Downloading target block failed: empty block response from {peer_id}",
+            );
+            return Err(BadPeer(peer_id, rep::NO_BLOCK));
+        }
+
+        if blocks.len() > 1 {
+            debug!(
+                target: LOG_TARGET,
+                "Too many blocks ({}) in warp target block response from {peer_id}",
+                blocks.len(),
+            );
+            return Err(BadPeer(peer_id, rep::NOT_REQUESTED));
+        }
+
+        validate_blocks::<B>(&blocks, &peer_id, Some(request))?;
+
+        let block = blocks.pop().expect("`blocks` len checked above; qed");
+
+        let Some(block_header) = &block.header else {
+            debug!(
+                target: LOG_TARGET,
+                "Downloading target block failed: missing header in response from {peer_id}.",
+            );
+            return Err(BadPeer(peer_id, rep::VERIFICATION_FAIL));
+        };
+
+        if block_header != header {
+            debug!(
+                target: LOG_TARGET,
+                "Downloading target block failed: different header in response from {peer_id}.",
+            );
+            return Err(BadPeer(peer_id, rep::VERIFICATION_FAIL));
+        }
+
+        if block.body.is_none() {
+            debug!(
+                target: LOG_TARGET,
+                "Downloading target block failed: missing body in response from {peer_id}.",
+            );
+            return Err(BadPeer(peer_id, rep::VERIFICATION_FAIL));
+        }
+
+        self.result = Some(WarpSyncResult {
+            target_header: header.clone(),
+            target_body: block.body,
+            target_justifications: block.justifications,
+        });
+        self.phase = Phase::Complete;
+        self.actions.push(WarpSyncAction::Finished);
+        Ok(())
+    }
+
+    /// Reserve a peer for a request assigning `new_state`.
+    fn schedule_next_peer(
+        &mut self,
+        new_state: PeerState,
+        min_best_number: Option<NumberFor<B>>,
+    ) -> Option<PeerId> {
+        let mut targets: Vec<_> = self.peers.values().map(|p| p.best_number).collect();
+        if targets.is_empty() {
+            return None;
+        }
+        targets.sort();
+        let median = targets[targets.len() / 2];
+        let threshold = std::cmp::max(median, min_best_number.unwrap_or(Zero::zero()));
+        // Find a random peer that is synced as much as peer majority and is above
+        // `min_best_number`.
+        for (peer_id, peer) in self.peers.iter_mut() {
+            if peer.state.is_available() && peer.best_number >= threshold {
+                peer.state = new_state;
+                return Some(*peer_id);
+            }
+        }
+        None
+    }
+
+    /// Produce warp proof request.
+    fn warp_proof_request(&mut self) -> Option<(PeerId, WarpProofRequest<B>)> {
+        let Phase::WarpProof { last_hash, .. } = &self.phase else {
+            return None;
+        };
+
+        // Copy `last_hash` early to cut the borrowing tie.
+        let begin = *last_hash;
+
+        if self
+            .peers
+            .values()
+            .any(|peer| matches!(peer.state, PeerState::DownloadingProofs))
+        {
+            // Only one warp proof request at a time is possible.
+            return None;
+        }
+
+        let peer_id = self.schedule_next_peer(PeerState::DownloadingProofs, None)?;
+        trace!(target: LOG_TARGET, "New WarpProofRequest to {peer_id}, begin hash: {begin}.");
+
+        Some((peer_id, WarpProofRequest { begin }))
+    }
+
+    /// Produce target block request.
+    fn target_block_request(&mut self) -> Option<(PeerId, BlockRequest<B>)> {
+        let Phase::TargetBlock(target_header) = &self.phase else {
+            return None;
+        };
+
+        if self
+            .peers
+            .values()
+            .any(|peer| matches!(peer.state, PeerState::DownloadingTargetBlock))
+        {
+            // Only one target block request at a time is possible.
+            return None;
+        }
+
+        // Cut the borrowing tie.
+        let target_hash = target_header.hash();
+        let target_number = *target_header.number();
+
+        let peer_id =
+            self.schedule_next_peer(PeerState::DownloadingTargetBlock, Some(target_number))?;
+
+        trace!(
+            target: LOG_TARGET,
+            "New target block request to {peer_id}, target: {} ({}).",
+            target_hash,
+            target_number,
+        );
+
+        Some((
+            peer_id,
+            BlockRequest::<B> {
+                id: 0,
+                fields: BlockAttributes::HEADER
+                    | BlockAttributes::BODY
+                    | BlockAttributes::JUSTIFICATION,
+                from: FromBlock::Hash(target_hash),
+                direction: Direction::Ascending,
+                max: Some(1),
+            },
+        ))
+    }
+
+    /// Returns warp sync estimated progress (stage, bytes received).
+    pub fn progress(&self) -> WarpSyncProgress<B> {
+        match &self.phase {
+            Phase::WaitingForPeers { .. } => WarpSyncProgress {
+                phase: WarpSyncPhase::AwaitingPeers {
+                    required_peers: MIN_PEERS_TO_START_WARP_SYNC,
+                },
+                total_bytes: self.total_proof_bytes,
+            },
+            Phase::WarpProof { .. } => WarpSyncProgress {
+                phase: WarpSyncPhase::DownloadingWarpProofs,
+                total_bytes: self.total_proof_bytes,
+            },
+            Phase::TargetBlock(_) => WarpSyncProgress {
+                phase: WarpSyncPhase::DownloadingTargetBlock,
+                total_bytes: self.total_proof_bytes,
+            },
+            Phase::PendingTargetBlock { .. } => WarpSyncProgress {
+                phase: WarpSyncPhase::AwaitingTargetBlock,
+                total_bytes: self.total_proof_bytes,
+            },
+            Phase::Complete => WarpSyncProgress {
+                phase: WarpSyncPhase::Complete,
+                total_bytes: self.total_proof_bytes + self.total_state_bytes,
+            },
+        }
+    }
+
+    /// Get the number of peers known to warp sync.
+    pub fn num_peers(&self) -> usize {
+        self.peers.len()
+    }
+
+    /// Returns the current sync status.
+    pub fn status(&self) -> SyncStatus<B> {
+        SyncStatus {
+            state: match &self.phase {
+                Phase::WaitingForPeers { .. } => SyncState::Downloading {
+                    target: Zero::zero(),
+                },
+                Phase::WarpProof { .. } => SyncState::Downloading {
+                    target: Zero::zero(),
+                },
+                Phase::PendingTargetBlock => SyncState::Downloading {
+                    target: Zero::zero(),
+                },
+                Phase::TargetBlock(header) => SyncState::Downloading {
+                    target: *header.number(),
+                },
+                Phase::Complete => SyncState::Idle,
+            },
+            best_seen_block: match &self.phase {
+                Phase::WaitingForPeers { .. } => None,
+                Phase::WarpProof { .. } => None,
+                Phase::PendingTargetBlock => None,
+                Phase::TargetBlock(header) => Some(*header.number()),
+                Phase::Complete => None,
+            },
+            num_peers: self.peers.len().saturated_into(),
+            num_connected_peers: self.peers.len().saturated_into(),
+            queued_blocks: 0,
+            state_sync: None,
+            warp_sync: Some(self.progress()),
+        }
+    }
+
+    /// Get actions that should be performed by the owner on [`WarpSync`]'s behalf
+    pub fn actions(&mut self) -> impl Iterator<Item = WarpSyncAction<B>> {
+        let warp_proof_request = self
+            .warp_proof_request()
+            .into_iter()
+            .map(|(peer_id, request)| WarpSyncAction::SendWarpProofRequest { peer_id, request });
+        self.actions.extend(warp_proof_request);
+
+        let target_block_request = self
+            .target_block_request()
+            .into_iter()
+            .map(|(peer_id, request)| WarpSyncAction::SendBlockRequest { peer_id, request });
+        self.actions.extend(target_block_request);
+
+        std::mem::take(&mut self.actions).into_iter()
+    }
+
+    /// Take the result of finished warp sync, returning `None` if the sync was unsuccessful.
+    #[must_use]
+    pub fn take_result(&mut self) -> Option<WarpSyncResult<B>> {
+        self.result.take()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use sc_block_builder::BlockBuilderBuilder;
+    use sp_blockchain::{BlockStatus, Error as BlockchainError, HeaderBackend, Info};
+    use sp_consensus_grandpa::{AuthorityList, SetId};
+    use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
+    use std::{io::ErrorKind, sync::Arc};
+    use substrate_test_runtime_client::{
+        runtime::{Block, Hash},
+        BlockBuilderExt, DefaultTestClientBuilderExt, TestClientBuilder, TestClientBuilderExt,
+    };
+
+    mockall::mock! {
+        pub Client<B: BlockT> {}
+
+        impl<B: BlockT> HeaderBackend<B> for Client<B> {
+            fn header(&self, hash: B::Hash) -> Result<Option<B::Header>, BlockchainError>;
+            fn info(&self) -> Info<B>;
+            fn status(&self, hash: B::Hash) -> Result<BlockStatus, BlockchainError>;
+            fn number(
+                &self,
+                hash: B::Hash,
+            ) -> Result<Option<<<B as BlockT>::Header as HeaderT>::Number>, BlockchainError>;
+            fn hash(&self, number: NumberFor<B>) -> Result<Option<B::Hash>, BlockchainError>;
+        }
+    }
+
+    mockall::mock! {
+        pub WarpSyncProvider<B: BlockT> {}
+
+        impl<B: BlockT> super::WarpSyncProvider<B> for WarpSyncProvider<B> {
+            fn generate(
+                &self,
+                start: B::Hash,
+            ) -> Result<EncodedProof, Box<dyn std::error::Error + Send + Sync>>;
+            fn verify(
+                &self,
+                proof: &EncodedProof,
+                set_id: SetId,
+                authorities: AuthorityList,
+            ) -> Result<VerificationResult<B>, Box<dyn std::error::Error + Send + Sync>>;
+            fn current_authorities(&self) -> AuthorityList;
+        }
+    }
+
+    fn mock_client_with_state() -> MockClient<Block> {
+        let mut client = MockClient::<Block>::new();
+        let genesis_hash = Hash::random();
+        client.expect_info().return_once(move || Info {
+            best_hash: genesis_hash,
+            best_number: 0,
+            genesis_hash,
+            finalized_hash: genesis_hash,
+            finalized_number: 0,
+            // We need some finalized state to render warp sync impossible.
+            finalized_state: Some((genesis_hash, 0)),
+            number_leaves: 0,
+            block_gap: None,
+        });
+
+        client
+    }
+
+    fn mock_client_without_state() -> MockClient<Block> {
+        let mut client = MockClient::<Block>::new();
+        let genesis_hash = Hash::random();
+        client.expect_info().returning(move || Info {
+            best_hash: genesis_hash,
+            best_number: 0,
+            genesis_hash,
+            finalized_hash: genesis_hash,
+            finalized_number: 0,
+            finalized_state: None,
+            number_leaves: 0,
+            block_gap: None,
+        });
+
+        client
+    }
+
+    #[test]
+    fn warp_sync_with_provider_for_db_with_finalized_state_is_noop() {
+        let client = mock_client_with_state();
+        let provider = MockWarpSyncProvider::<Block>::new();
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Warp sync instantly finishes
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(actions[0], WarpSyncAction::Finished));
+
+        // ... with no result.
+        assert!(warp_sync.take_result().is_none());
+    }
+
+    #[test]
+    fn warp_sync_to_target_for_db_with_finalized_state_is_noop() {
+        let client = mock_client_with_state();
+        let config = WarpSyncConfig::WaitForTarget;
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Warp sync instantly finishes
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(actions[0], WarpSyncAction::Finished));
+
+        // ... with no result.
+        assert!(warp_sync.take_result().is_none());
+    }
+
+    #[test]
+    fn warp_sync_with_provider_for_empty_db_doesnt_finish_instantly() {
+        let client = mock_client_without_state();
+        let provider = MockWarpSyncProvider::<Block>::new();
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // No actions are emitted.
+        assert_eq!(warp_sync.actions().count(), 0)
+    }
+
+    #[test]
+    fn warp_sync_to_target_for_empty_db_doesnt_finish_instantly() {
+        let client = mock_client_without_state();
+        let config = WarpSyncConfig::WaitForTarget;
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // No actions are emitted.
+        assert_eq!(warp_sync.actions().count(), 0)
+    }
+
+    #[test]
+    fn warp_sync_is_started_only_when_there_is_enough_peers() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Warp sync is not started when there is not enough peers.
+        for _ in 0..(MIN_PEERS_TO_START_WARP_SYNC - 1) {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), 10);
+            assert!(matches!(warp_sync.phase, Phase::WaitingForPeers { .. }))
+        }
+
+        // Now we have enough peers and warp sync is started.
+        warp_sync.add_peer(PeerId::random(), Hash::random(), 10);
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }))
+    }
+
+    #[test]
+    fn no_peer_is_scheduled_if_no_peers_connected() {
+        let client = mock_client_without_state();
+        let provider = MockWarpSyncProvider::<Block>::new();
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        assert!(warp_sync
+            .schedule_next_peer(PeerState::DownloadingProofs, None)
+            .is_none());
+    }
+
+    #[test]
+    #[allow(clippy::assertions_on_constants)]
+    fn enough_peers_are_used_in_tests() {
+        // Tests below use 10 peers. Fail early if it's less than a threshold for warp sync.
+        assert!(
+            10 >= MIN_PEERS_TO_START_WARP_SYNC,
+            "Tests must be updated to use that many initial peers.",
+        );
+    }
+
+    #[test]
+    fn at_least_median_synced_peer_is_scheduled() {
+        for _ in 0..100 {
+            let client = mock_client_without_state();
+            let mut provider = MockWarpSyncProvider::<Block>::new();
+            provider
+                .expect_current_authorities()
+                .once()
+                .return_const(AuthorityList::default());
+            let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+            let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+            for best_number in 1..11 {
+                warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+            }
+
+            let peer_id = warp_sync.schedule_next_peer(PeerState::DownloadingProofs, None);
+            assert!(warp_sync.peers.get(&peer_id.unwrap()).unwrap().best_number >= 6);
+        }
+    }
+
+    #[test]
+    fn min_best_number_peer_is_scheduled() {
+        for _ in 0..10 {
+            let client = mock_client_without_state();
+            let mut provider = MockWarpSyncProvider::<Block>::new();
+            provider
+                .expect_current_authorities()
+                .once()
+                .return_const(AuthorityList::default());
+            let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+            let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+            for best_number in 1..11 {
+                warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+            }
+
+            let peer_id = warp_sync.schedule_next_peer(PeerState::DownloadingProofs, Some(10));
+            assert!(warp_sync.peers.get(&peer_id.unwrap()).unwrap().best_number == 10);
+        }
+    }
+
+    #[test]
+    fn no_warp_proof_request_in_another_phase() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set to another phase.
+        warp_sync.phase = Phase::PendingTargetBlock;
+
+        // No request is made.
+        assert!(warp_sync.warp_proof_request().is_none());
+    }
+
+    #[test]
+    fn warp_proof_request_starts_at_last_hash() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        let known_last_hash = Hash::random();
+
+        // Manually set last hash to known value.
+        match &mut warp_sync.phase {
+            Phase::WarpProof { last_hash, .. } => {
+                *last_hash = known_last_hash;
+            }
+            _ => panic!("Invalid phase."),
+        }
+
+        let (_peer_id, request) = warp_sync.warp_proof_request().unwrap();
+        assert_eq!(request.begin, known_last_hash);
+    }
+
+    #[test]
+    fn no_parallel_warp_proof_requests() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make requests.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        // First request is made.
+        assert!(warp_sync.warp_proof_request().is_some());
+        // Second request is not made.
+        assert!(warp_sync.warp_proof_request().is_none());
+    }
+
+    #[test]
+    fn bad_warp_proof_response_drops_peer() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        // Warp proof verification fails.
+        provider
+            .expect_verify()
+            .return_once(|_proof, _set_id, _authorities| {
+                Err(Box::new(std::io::Error::new(
+                    ErrorKind::Other,
+                    "test-verification-failure",
+                )))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        // Consume `SendWarpProofRequest` action.
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        let WarpSyncAction::SendWarpProofRequest {
+            peer_id: request_peer_id,
+            ..
+        } = actions[0]
+        else {
+            panic!("Invalid action");
+        };
+
+        warp_sync.on_warp_proof_response(&request_peer_id, EncodedProof(Vec::new()));
+
+        // We only interested in alredy generated actions, not new requests.
+        let actions = std::mem::take(&mut warp_sync.actions);
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            actions[0],
+            WarpSyncAction::DropPeer(BadPeer(peer_id, _rep)) if peer_id == request_peer_id
+        ));
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+    }
+
+    #[test]
+    fn partial_warp_proof_doesnt_advance_phase() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        // Warp proof is partial.
+        provider
+            .expect_verify()
+            .return_once(|_proof, set_id, authorities| {
+                Ok(VerificationResult::Partial(
+                    set_id,
+                    authorities,
+                    Hash::random(),
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        // Consume `SendWarpProofRequest` action.
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        let WarpSyncAction::SendWarpProofRequest {
+            peer_id: request_peer_id,
+            ..
+        } = actions[0]
+        else {
+            panic!("Invalid action");
+        };
+
+        warp_sync.on_warp_proof_response(&request_peer_id, EncodedProof(Vec::new()));
+
+        assert!(warp_sync.actions.is_empty(), "No extra actions generated");
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+    }
+
+    #[test]
+    fn complete_warp_proof_advances_phase() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        // Consume `SendWarpProofRequest` action.
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        let WarpSyncAction::SendWarpProofRequest {
+            peer_id: request_peer_id,
+            ..
+        } = actions[0]
+        else {
+            panic!("Invalid action.");
+        };
+
+        warp_sync.on_warp_proof_response(&request_peer_id, EncodedProof(Vec::new()));
+
+        assert!(warp_sync.actions.is_empty(), "No extra actions generated.");
+        assert!(
+            matches!(warp_sync.phase, Phase::TargetBlock(header) if header == *target_block.header())
+        );
+    }
+
+    #[test]
+    fn no_target_block_requests_in_another_phase() {
+        let client = mock_client_without_state();
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(Arc::new(client), config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+        // We are not in `Phase::TargetBlock`
+        assert!(matches!(warp_sync.phase, Phase::WarpProof { .. }));
+
+        // No request is made.
+        assert!(warp_sync.target_block_request().is_none());
+    }
+
+    #[test]
+    fn target_block_request_is_correct() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        let (_peer_id, request) = warp_sync.target_block_request().unwrap();
+        assert_eq!(request.from, FromBlock::Hash(target_block.header().hash()));
+        assert_eq!(
+            request.fields,
+            BlockAttributes::HEADER | BlockAttributes::BODY | BlockAttributes::JUSTIFICATION
+        );
+        assert_eq!(request.max, Some(1));
+    }
+
+    #[test]
+    fn externally_set_target_block_is_requested() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+        let config = WarpSyncConfig::WaitForTarget;
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // No actions generated so far.
+        assert_eq!(warp_sync.actions().count(), 0);
+
+        warp_sync.set_target_block(target_header);
+        assert!(matches!(warp_sync.phase, Phase::TargetBlock(_)));
+
+        let (_peer_id, request) = warp_sync.target_block_request().unwrap();
+        assert_eq!(request.from, FromBlock::Hash(target_block.header().hash()));
+        assert_eq!(
+            request.fields,
+            BlockAttributes::HEADER | BlockAttributes::BODY | BlockAttributes::JUSTIFICATION
+        );
+        assert_eq!(request.max, Some(1));
+    }
+
+    #[test]
+    fn no_parallel_target_block_requests() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        // First target block request is made.
+        assert!(warp_sync.target_block_request().is_some());
+        // No parallel request is made.
+        assert!(warp_sync.target_block_request().is_none());
+    }
+
+    #[test]
+    fn target_block_response_with_no_blocks_drops_peer() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        let (peer_id, request) = warp_sync.target_block_request().unwrap();
+
+        // Empty block response received.
+        let response = Vec::new();
+        // Peer is dropped.
+        assert!(matches!(
+            warp_sync.on_block_response_inner(peer_id, request, response),
+            Err(BadPeer(id, _rep)) if id == peer_id,
+        ));
+    }
+
+    #[test]
+    fn target_block_response_with_extra_blocks_drops_peer() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        let mut extra_block_builder = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap();
+        extra_block_builder
+            .push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6]))
+            .unwrap();
+        let extra_block = extra_block_builder.build().unwrap().block;
+
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        let (peer_id, request) = warp_sync.target_block_request().unwrap();
+
+        // Block response with extra blocks received.
+        let response = vec![
+            BlockData::<Block> {
+                hash: target_block.header().hash(),
+                header: Some(target_block.header().clone()),
+                body: Some(target_block.extrinsics().to_vec()),
+                indexed_body: None,
+                receipt: None,
+                message_queue: None,
+                justification: None,
+                justifications: None,
+            },
+            BlockData::<Block> {
+                hash: extra_block.header().hash(),
+                header: Some(extra_block.header().clone()),
+                body: Some(extra_block.extrinsics().to_vec()),
+                indexed_body: None,
+                receipt: None,
+                message_queue: None,
+                justification: None,
+                justifications: None,
+            },
+        ];
+        // Peer is dropped.
+        assert!(matches!(
+            warp_sync.on_block_response_inner(peer_id, request, response),
+            Err(BadPeer(id, _rep)) if id == peer_id,
+        ));
+    }
+
+    #[test]
+    fn target_block_response_with_wrong_block_drops_peer() {
+        sp_tracing::try_init_simple();
+
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let target_block = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+
+        let mut wrong_block_builder = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap();
+        wrong_block_builder
+            .push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6]))
+            .unwrap();
+        let wrong_block = wrong_block_builder.build().unwrap().block;
+
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        let (peer_id, request) = warp_sync.target_block_request().unwrap();
+
+        // Wrong block received.
+        let response = vec![BlockData::<Block> {
+            hash: wrong_block.header().hash(),
+            header: Some(wrong_block.header().clone()),
+            body: Some(wrong_block.extrinsics().to_vec()),
+            indexed_body: None,
+            receipt: None,
+            message_queue: None,
+            justification: None,
+            justifications: None,
+        }];
+        // Peer is dropped.
+        assert!(matches!(
+            warp_sync.on_block_response_inner(peer_id, request, response),
+            Err(BadPeer(id, _rep)) if id == peer_id,
+        ));
+    }
+
+    #[test]
+    fn correct_target_block_response_sets_strategy_result() {
+        let client = Arc::new(TestClientBuilder::new().set_no_genesis().build());
+        let mut provider = MockWarpSyncProvider::<Block>::new();
+        provider
+            .expect_current_authorities()
+            .once()
+            .return_const(AuthorityList::default());
+        let mut target_block_builder = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap();
+        target_block_builder
+            .push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6]))
+            .unwrap();
+        let target_block = target_block_builder.build().unwrap().block;
+        let target_header = target_block.header().clone();
+        // Warp proof is complete.
+        provider
+            .expect_verify()
+            .return_once(move |_proof, set_id, authorities| {
+                Ok(VerificationResult::Complete(
+                    set_id,
+                    authorities,
+                    target_header,
+                ))
+            });
+        let config = WarpSyncConfig::WithProvider(Arc::new(provider));
+        let mut warp_sync = WarpSync::new(client, config);
+
+        // Make sure we have enough peers to make a request.
+        for best_number in 1..11 {
+            warp_sync.add_peer(PeerId::random(), Hash::random(), best_number);
+        }
+
+        // Manually set `TargetBlock` phase.
+        warp_sync.phase = Phase::TargetBlock(target_block.header().clone());
+
+        let (peer_id, request) = warp_sync.target_block_request().unwrap();
+
+        // Correct block received.
+        let body = Some(target_block.extrinsics().to_vec());
+        let justifications = Some(Justifications::from((*b"FRNK", Vec::new())));
+        let response = vec![BlockData::<Block> {
+            hash: target_block.header().hash(),
+            header: Some(target_block.header().clone()),
+            body: body.clone(),
+            indexed_body: None,
+            receipt: None,
+            message_queue: None,
+            justification: None,
+            justifications: justifications.clone(),
+        }];
+
+        assert!(warp_sync
+            .on_block_response_inner(peer_id, request, response)
+            .is_ok());
+
+        // Strategy finishes.
+        let actions = warp_sync.actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(actions[0], WarpSyncAction::Finished));
+
+        // With correct result.
+        let result = warp_sync.take_result().unwrap();
+        assert_eq!(result.target_header, *target_block.header());
+        assert_eq!(result.target_body, body);
+        assert_eq!(result.target_justifications, justifications);
+    }
+}
diff --git a/libp2p/sync/src/types.rs b/libp2p/sync/src/types.rs
new file mode 100644
index 000000000..6417a1628
--- /dev/null
+++ b/libp2p/sync/src/types.rs
@@ -0,0 +1,188 @@
+// 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/>.
+
+//! Common syncing types.
+
+use futures::Stream;
+use sc_network_common::{role::Roles, types::ReputationChange};
+
+use libp2p::PeerId;
+
+use crate::strategy::{state_sync::StateSyncProgress, warp::WarpSyncProgress};
+use sc_network_common::sync::message::BlockRequest;
+use sp_runtime::traits::{Block as BlockT, NumberFor};
+
+use std::{any::Any, fmt, fmt::Formatter, pin::Pin, sync::Arc};
+
+/// The sync status of a peer we are trying to sync with
+#[derive(Debug)]
+pub struct PeerInfo<Block: BlockT> {
+    /// Their best block hash.
+    pub best_hash: Block::Hash,
+    /// Their best block number.
+    pub best_number: NumberFor<Block>,
+}
+
+/// Info about a peer's known state (both full and light).
+#[derive(Clone, Debug)]
+pub struct ExtendedPeerInfo<B: BlockT> {
+    /// Roles
+    pub roles: Roles,
+    /// Peer best block hash
+    pub best_hash: B::Hash,
+    /// Peer best block number
+    pub best_number: NumberFor<B>,
+}
+
+/// Reported sync state.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum SyncState<BlockNumber> {
+    /// Initial sync is complete, keep-up sync is active.
+    Idle,
+    /// Actively catching up with the chain.
+    Downloading { target: BlockNumber },
+    /// All blocks are downloaded and are being imported.
+    Importing { target: BlockNumber },
+}
+
+impl<BlockNumber> SyncState<BlockNumber> {
+    /// Are we actively catching up with the chain?
+    pub fn is_major_syncing(&self) -> bool {
+        !matches!(self, SyncState::Idle)
+    }
+}
+
+/// Syncing status and statistics.
+#[derive(Debug, Clone)]
+pub struct SyncStatus<Block: BlockT> {
+    /// Current global sync state.
+    pub state: SyncState<NumberFor<Block>>,
+    /// Target sync block number.
+    pub best_seen_block: Option<NumberFor<Block>>,
+    /// Number of peers participating in syncing.
+    pub num_peers: u32,
+    /// Number of peers known to `SyncingEngine` (both full and light).
+    pub num_connected_peers: u32,
+    /// Number of blocks queued for import
+    pub queued_blocks: u32,
+    /// State sync status in progress, if any.
+    pub state_sync: Option<StateSyncProgress>,
+    /// Warp sync in progress, if any.
+    pub warp_sync: Option<WarpSyncProgress<Block>>,
+}
+
+/// A peer did not behave as expected and should be reported.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BadPeer(pub PeerId, pub ReputationChange);
+
+impl fmt::Display for BadPeer {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "Bad peer {}; Reputation change: {:?}", self.0, self.1)
+    }
+}
+
+impl std::error::Error for BadPeer {}
+
+#[derive(Debug)]
+pub enum PeerRequest<B: BlockT> {
+    Block(BlockRequest<B>),
+    State,
+    WarpProof,
+}
+
+#[derive(Debug)]
+pub enum PeerRequestType {
+    Block,
+    State,
+    WarpProof,
+}
+
+impl<B: BlockT> PeerRequest<B> {
+    pub fn get_type(&self) -> PeerRequestType {
+        match self {
+            PeerRequest::Block(_) => PeerRequestType::Block,
+            PeerRequest::State => PeerRequestType::State,
+            PeerRequest::WarpProof => PeerRequestType::WarpProof,
+        }
+    }
+}
+
+/// Wrapper for implementation-specific state request.
+///
+/// NOTE: Implementation must be able to encode and decode it for network purposes.
+pub struct OpaqueStateRequest(pub Box<dyn Any + Send>);
+
+impl fmt::Debug for OpaqueStateRequest {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.debug_struct("OpaqueStateRequest").finish()
+    }
+}
+
+/// Wrapper for implementation-specific state response.
+///
+/// NOTE: Implementation must be able to encode and decode it for network purposes.
+pub struct OpaqueStateResponse(pub Box<dyn Any + Send>);
+
+impl fmt::Debug for OpaqueStateResponse {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.debug_struct("OpaqueStateResponse").finish()
+    }
+}
+
+/// Provides high-level status of syncing.
+#[async_trait::async_trait]
+pub trait SyncStatusProvider<Block: BlockT>: Send + Sync {
+    /// Get high-level view of the syncing status.
+    async fn status(&self) -> Result<SyncStatus<Block>, ()>;
+}
+
+#[async_trait::async_trait]
+impl<T, Block> SyncStatusProvider<Block> for Arc<T>
+where
+    T: ?Sized,
+    T: SyncStatusProvider<Block>,
+    Block: BlockT,
+{
+    async fn status(&self) -> Result<SyncStatus<Block>, ()> {
+        T::status(self).await
+    }
+}
+
+/// Syncing-related events that other protocols can subscribe to.
+pub enum SyncEvent {
+    /// Peer that the syncing implementation is tracking connected.
+    PeerConnected(PeerId),
+
+    /// Peer that the syncing implementation was tracking disconnected.
+    PeerDisconnected(PeerId),
+}
+
+pub trait SyncEventStream: Send + Sync {
+    /// Subscribe to syncing-related events.
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>>;
+}
+
+impl<T> SyncEventStream for Arc<T>
+where
+    T: ?Sized,
+    T: SyncEventStream,
+{
+    fn event_stream(&self, name: &'static str) -> Pin<Box<dyn Stream<Item = SyncEvent> + Send>> {
+        T::event_stream(self, name)
+    }
+}
diff --git a/libp2p/sync/src/warp_request_handler.rs b/libp2p/sync/src/warp_request_handler.rs
new file mode 100644
index 000000000..bee52aca8
--- /dev/null
+++ b/libp2p/sync/src/warp_request_handler.rs
@@ -0,0 +1,171 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Helper for handling (i.e. answering) grandpa warp sync requests from a remote peer.
+
+use codec::Decode;
+use futures::{channel::oneshot, stream::StreamExt};
+use log::debug;
+
+use crate::{
+    strategy::warp::{EncodedProof, WarpProofRequest, WarpSyncProvider},
+    LOG_TARGET,
+};
+use sc_network::{
+    config::ProtocolId,
+    request_responses::{
+        IncomingRequest, OutgoingResponse, ProtocolConfig as RequestResponseConfig,
+    },
+};
+use sp_runtime::traits::Block as BlockT;
+
+use std::{sync::Arc, time::Duration};
+
+const MAX_RESPONSE_SIZE: u64 = 16 * 1024 * 1024;
+
+/// Incoming warp requests bounded queue size.
+const MAX_WARP_REQUEST_QUEUE: usize = 20;
+
+/// Generates a [`RequestResponseConfig`] for the grandpa warp sync request protocol, refusing
+/// incoming requests.
+pub fn generate_request_response_config<Hash: AsRef<[u8]>>(
+    protocol_id: ProtocolId,
+    genesis_hash: Hash,
+    fork_id: Option<&str>,
+) -> RequestResponseConfig {
+    RequestResponseConfig {
+        name: generate_protocol_name(genesis_hash, fork_id).into(),
+        fallback_names: std::iter::once(generate_legacy_protocol_name(protocol_id).into())
+            .collect(),
+        max_request_size: 32,
+        max_response_size: MAX_RESPONSE_SIZE,
+        request_timeout: Duration::from_secs(10),
+        inbound_queue: None,
+    }
+}
+
+/// Generate the grandpa warp sync protocol name from the genesi hash and fork id.
+fn generate_protocol_name<Hash: AsRef<[u8]>>(genesis_hash: Hash, fork_id: Option<&str>) -> String {
+    let genesis_hash = genesis_hash.as_ref();
+    if let Some(fork_id) = fork_id {
+        format!(
+            "/{}/{}/sync/warp",
+            array_bytes::bytes2hex("", genesis_hash),
+            fork_id
+        )
+    } else {
+        format!("/{}/sync/warp", array_bytes::bytes2hex("", genesis_hash))
+    }
+}
+
+/// Generate the legacy grandpa warp sync protocol name from chain specific protocol identifier.
+fn generate_legacy_protocol_name(protocol_id: ProtocolId) -> String {
+    format!("/{}/sync/warp", protocol_id.as_ref())
+}
+
+/// Handler for incoming grandpa warp sync requests from a remote peer.
+pub struct RequestHandler<TBlock: BlockT> {
+    backend: Arc<dyn WarpSyncProvider<TBlock>>,
+    request_receiver: async_channel::Receiver<IncomingRequest>,
+}
+
+impl<TBlock: BlockT> RequestHandler<TBlock> {
+    /// Create a new [`RequestHandler`].
+    pub fn new<Hash: AsRef<[u8]>>(
+        protocol_id: ProtocolId,
+        genesis_hash: Hash,
+        fork_id: Option<&str>,
+        backend: Arc<dyn WarpSyncProvider<TBlock>>,
+    ) -> (Self, RequestResponseConfig) {
+        let (tx, request_receiver) = async_channel::bounded(MAX_WARP_REQUEST_QUEUE);
+
+        let mut request_response_config =
+            generate_request_response_config(protocol_id, genesis_hash, fork_id);
+        request_response_config.inbound_queue = Some(tx);
+
+        (
+            Self {
+                backend,
+                request_receiver,
+            },
+            request_response_config,
+        )
+    }
+
+    fn handle_request(
+        &self,
+        payload: Vec<u8>,
+        pending_response: oneshot::Sender<OutgoingResponse>,
+    ) -> Result<(), HandleRequestError> {
+        let request = WarpProofRequest::<TBlock>::decode(&mut &payload[..])?;
+
+        let EncodedProof(proof) = self
+            .backend
+            .generate(request.begin)
+            .map_err(HandleRequestError::InvalidRequest)?;
+
+        pending_response
+            .send(OutgoingResponse {
+                result: Ok(proof),
+                reputation_changes: Vec::new(),
+                sent_feedback: None,
+            })
+            .map_err(|_| HandleRequestError::SendResponse)
+    }
+
+    /// Run [`RequestHandler`].
+    pub async fn run(mut self) {
+        while let Some(request) = self.request_receiver.next().await {
+            let IncomingRequest {
+                peer,
+                payload,
+                pending_response,
+            } = request;
+
+            match self.handle_request(payload, pending_response) {
+                Ok(()) => {
+                    debug!(target: LOG_TARGET, "Handled grandpa warp sync request from {}.", peer)
+                }
+                Err(e) => debug!(
+                    target: LOG_TARGET,
+                    "Failed to handle grandpa warp sync request from {}: {}",
+                    peer, e,
+                ),
+            }
+        }
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum HandleRequestError {
+    #[error("Failed to decode request: {0}.")]
+    DecodeProto(#[from] prost::DecodeError),
+
+    #[error("Failed to encode response: {0}.")]
+    EncodeProto(#[from] prost::EncodeError),
+
+    #[error("Failed to decode block hash: {0}.")]
+    DecodeScale(#[from] codec::Error),
+
+    #[error(transparent)]
+    Client(#[from] sp_blockchain::Error),
+
+    #[error("Invalid request {0}.")]
+    InvalidRequest(#[from] Box<dyn std::error::Error + Send + Sync>),
+
+    #[error("Failed to send response.")]
+    SendResponse,
+}
diff --git a/libp2p/transactions/Cargo.toml b/libp2p/transactions/Cargo.toml
new file mode 100644
index 000000000..9c1f5d657
--- /dev/null
+++ b/libp2p/transactions/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+description = "Substrate transaction protocol"
+name = "sc-network-transactions"
+version = "0.33.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+homepage = "https://substrate.io"
+documentation = "https://docs.rs/sc-network-transactions"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+array-bytes = "6.1"
+codec = { package = "parity-scale-codec", version = "3.6.1", features = ["derive"] }
+futures = "0.3.21"
+libp2p = "0.51.4"
+log = "0.4.17"
+prometheus-endpoint = { workspace = true }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sc-network-common = { path = "../common" }
+sc-network-sync = { path = "../sync" }
+sc-utils = { workspace = true }
+sp-runtime = { workspace = true }
+sp-consensus = { workspace = true }
diff --git a/libp2p/transactions/src/config.rs b/libp2p/transactions/src/config.rs
new file mode 100644
index 000000000..fdf81fcd9
--- /dev/null
+++ b/libp2p/transactions/src/config.rs
@@ -0,0 +1,98 @@
+// 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/>.
+
+//! Configuration of the transaction protocol
+
+use futures::prelude::*;
+use sc_network_common::ExHashT;
+use sp_runtime::traits::Block as BlockT;
+use std::{collections::HashMap, future::Future, pin::Pin, time};
+
+/// Interval at which we propagate transactions;
+pub(crate) const PROPAGATE_TIMEOUT: time::Duration = time::Duration::from_millis(2900);
+
+/// Maximum number of known transaction hashes to keep for a peer.
+///
+/// This should be approx. 2 blocks full of transactions for the network to function properly.
+pub(crate) const MAX_KNOWN_TRANSACTIONS: usize = 10240; // ~300kb per peer + overhead.
+
+/// Maximum allowed size for a transactions notification.
+pub(crate) const MAX_TRANSACTIONS_SIZE: u64 = 16 * 1024 * 1024;
+
+/// Maximum number of transaction validation request we keep at any moment.
+pub(crate) const MAX_PENDING_TRANSACTIONS: usize = 8192;
+
+/// Result of the transaction import.
+#[derive(Clone, Copy, Debug)]
+pub enum TransactionImport {
+	/// Transaction is good but already known by the transaction pool.
+	KnownGood,
+	/// Transaction is good and not yet known.
+	NewGood,
+	/// Transaction is invalid.
+	Bad,
+	/// Transaction import was not performed.
+	None,
+}
+
+/// Future resolving to transaction import result.
+pub type TransactionImportFuture = Pin<Box<dyn Future<Output = TransactionImport> + Send>>;
+
+/// Transaction pool interface
+pub trait TransactionPool<H: ExHashT, B: BlockT>: Send + Sync {
+	/// Get transactions from the pool that are ready to be propagated.
+	fn transactions(&self) -> Vec<(H, B::Extrinsic)>;
+	/// Get hash of transaction.
+	fn hash_of(&self, transaction: &B::Extrinsic) -> H;
+	/// Import a transaction into the pool.
+	///
+	/// This will return future.
+	fn import(&self, transaction: B::Extrinsic) -> TransactionImportFuture;
+	/// Notify the pool about transactions broadcast.
+	fn on_broadcasted(&self, propagations: HashMap<H, Vec<String>>);
+	/// Get transaction by hash.
+	fn transaction(&self, hash: &H) -> Option<B::Extrinsic>;
+}
+
+/// Dummy implementation of the [`TransactionPool`] trait for a transaction pool that is always
+/// empty and discards all incoming transactions.
+///
+/// Requires the "hash" type to implement the `Default` trait.
+///
+/// Useful for testing purposes.
+pub struct EmptyTransactionPool;
+
+impl<H: ExHashT + Default, B: BlockT> TransactionPool<H, B> for EmptyTransactionPool {
+	fn transactions(&self) -> Vec<(H, B::Extrinsic)> {
+		Vec::new()
+	}
+
+	fn hash_of(&self, _transaction: &B::Extrinsic) -> H {
+		Default::default()
+	}
+
+	fn import(&self, _transaction: B::Extrinsic) -> TransactionImportFuture {
+		Box::pin(future::ready(TransactionImport::KnownGood))
+	}
+
+	fn on_broadcasted(&self, _: HashMap<H, Vec<String>>) {}
+
+	fn transaction(&self, _h: &H) -> Option<B::Extrinsic> {
+		None
+	}
+}
diff --git a/libp2p/transactions/src/lib.rs b/libp2p/transactions/src/lib.rs
new file mode 100644
index 000000000..b22996674
--- /dev/null
+++ b/libp2p/transactions/src/lib.rs
@@ -0,0 +1,514 @@
+// 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/>.
+
+//! Transactions handling to plug on top of the network service.
+//!
+//! Usage:
+//!
+//! - Use [`TransactionsHandlerPrototype::new`] to create a prototype.
+//! - Pass the `NonDefaultSetConfig` returned from [`TransactionsHandlerPrototype::new`] to the
+//!   network configuration as an extra peers set.
+//! - Use [`TransactionsHandlerPrototype::build`] then [`TransactionsHandler::run`] to obtain a
+//! `Future` that processes transactions.
+
+use crate::config::*;
+
+use codec::{Decode, Encode};
+use futures::{prelude::*, stream::FuturesUnordered};
+use libp2p::{multiaddr, PeerId};
+use log::{debug, trace, warn};
+
+use prometheus_endpoint::{register, Counter, PrometheusError, Registry, U64};
+use sc_network::{
+	config::{NonDefaultSetConfig, NonReservedPeerMode, ProtocolId, SetConfig},
+	error,
+	service::traits::{NotificationEvent, NotificationService, ValidationResult},
+	types::ProtocolName,
+	utils::{interval, LruHashSet},
+	NetworkEventStream, NetworkNotification, NetworkPeers,
+};
+use sc_network_common::{role::ObservedRole, ExHashT};
+use sc_network_sync::{SyncEvent, SyncEventStream};
+use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedReceiver, TracingUnboundedSender};
+use sp_runtime::traits::Block as BlockT;
+
+use std::{
+	collections::{hash_map::Entry, HashMap},
+	iter,
+	num::NonZeroUsize,
+	pin::Pin,
+	sync::Arc,
+	task::Poll,
+};
+
+pub mod config;
+
+/// A set of transactions.
+pub type Transactions<E> = Vec<E>;
+
+mod rep {
+	use sc_network::ReputationChange as Rep;
+	/// Reputation change when a peer sends us any transaction.
+	///
+	/// This forces node to verify it, thus the negative value here. Once transaction is verified,
+	/// reputation change should be refunded with `ANY_TRANSACTION_REFUND`
+	pub const ANY_TRANSACTION: Rep = Rep::new(-(1 << 4), "Any transaction");
+	/// Reputation change when a peer sends us any transaction that is not invalid.
+	pub const ANY_TRANSACTION_REFUND: Rep = Rep::new(1 << 4, "Any transaction (refund)");
+	/// Reputation change when a peer sends us an transaction that we didn't know about.
+	pub const GOOD_TRANSACTION: Rep = Rep::new(1 << 7, "Good transaction");
+	/// Reputation change when a peer sends us a bad transaction.
+	pub const BAD_TRANSACTION: Rep = Rep::new(-(1 << 12), "Bad transaction");
+}
+
+struct Metrics {
+	propagated_transactions: Counter<U64>,
+}
+
+impl Metrics {
+	fn register(r: &Registry) -> Result<Self, PrometheusError> {
+		Ok(Self {
+			propagated_transactions: register(
+				Counter::new(
+					"substrate_sync_propagated_transactions",
+					"Number of transactions propagated to at least one peer",
+				)?,
+				r,
+			)?,
+		})
+	}
+}
+
+struct PendingTransaction<H> {
+	validation: TransactionImportFuture,
+	tx_hash: H,
+}
+
+impl<H> Unpin for PendingTransaction<H> {}
+
+impl<H: ExHashT> Future for PendingTransaction<H> {
+	type Output = (H, TransactionImport);
+
+	fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
+		if let Poll::Ready(import_result) = self.validation.poll_unpin(cx) {
+			return Poll::Ready((self.tx_hash.clone(), import_result))
+		}
+
+		Poll::Pending
+	}
+}
+
+/// Prototype for a [`TransactionsHandler`].
+pub struct TransactionsHandlerPrototype {
+	/// Name of the transaction protocol.
+	protocol_name: ProtocolName,
+
+	/// Handle that is used to communicate with `sc_network::Notifications`.
+	notification_service: Box<dyn NotificationService>,
+}
+
+impl TransactionsHandlerPrototype {
+	/// Create a new instance.
+	pub fn new<Hash: AsRef<[u8]>>(
+		protocol_id: ProtocolId,
+		genesis_hash: Hash,
+		fork_id: Option<&str>,
+	) -> (Self, NonDefaultSetConfig) {
+		let genesis_hash = genesis_hash.as_ref();
+		let protocol_name: ProtocolName = if let Some(fork_id) = fork_id {
+			format!("/{}/{}/transactions/1", array_bytes::bytes2hex("", genesis_hash), fork_id)
+		} else {
+			format!("/{}/transactions/1", array_bytes::bytes2hex("", genesis_hash))
+		}
+		.into();
+		let (config, notification_service) = NonDefaultSetConfig::new(
+			protocol_name.clone(),
+			vec![format!("/{}/transactions/1", protocol_id.as_ref()).into()],
+			MAX_TRANSACTIONS_SIZE,
+			None,
+			SetConfig {
+				in_peers: 0,
+				out_peers: 0,
+				reserved_nodes: Vec::new(),
+				non_reserved_mode: NonReservedPeerMode::Deny,
+			},
+		);
+
+		(Self { protocol_name, notification_service }, config)
+	}
+
+	/// Turns the prototype into the actual handler. Returns a controller that allows controlling
+	/// the behaviour of the handler while it's running.
+	///
+	/// Important: the transactions handler is initially disabled and doesn't gossip transactions.
+	/// Gossiping is enabled when major syncing is done.
+	pub fn build<
+		B: BlockT + 'static,
+		H: ExHashT,
+		N: NetworkPeers + NetworkEventStream + NetworkNotification,
+		S: SyncEventStream + sp_consensus::SyncOracle,
+	>(
+		self,
+		network: N,
+		sync: S,
+		transaction_pool: Arc<dyn TransactionPool<H, B>>,
+		metrics_registry: Option<&Registry>,
+	) -> error::Result<(TransactionsHandler<B, H, N, S>, TransactionsHandlerController<H>)> {
+		let sync_event_stream = sync.event_stream("transactions-handler-sync");
+		let (to_handler, from_controller) = tracing_unbounded("mpsc_transactions_handler", 100_000);
+
+		let handler = TransactionsHandler {
+			protocol_name: self.protocol_name,
+			notification_service: self.notification_service,
+			propagate_timeout: (Box::pin(interval(PROPAGATE_TIMEOUT))
+				as Pin<Box<dyn Stream<Item = ()> + Send>>)
+				.fuse(),
+			pending_transactions: FuturesUnordered::new(),
+			pending_transactions_peers: HashMap::new(),
+			network,
+			sync,
+			sync_event_stream: sync_event_stream.fuse(),
+			peers: HashMap::new(),
+			transaction_pool,
+			from_controller,
+			metrics: if let Some(r) = metrics_registry {
+				Some(Metrics::register(r)?)
+			} else {
+				None
+			},
+		};
+
+		let controller = TransactionsHandlerController { to_handler };
+
+		Ok((handler, controller))
+	}
+}
+
+/// Controls the behaviour of a [`TransactionsHandler`] it is connected to.
+pub struct TransactionsHandlerController<H: ExHashT> {
+	to_handler: TracingUnboundedSender<ToHandler<H>>,
+}
+
+impl<H: ExHashT> TransactionsHandlerController<H> {
+	/// You may call this when new transactions are imported by the transaction pool.
+	///
+	/// All transactions will be fetched from the `TransactionPool` that was passed at
+	/// initialization as part of the configuration and propagated to peers.
+	pub fn propagate_transactions(&self) {
+		let _ = self.to_handler.unbounded_send(ToHandler::PropagateTransactions);
+	}
+
+	/// You must call when new a transaction is imported by the transaction pool.
+	///
+	/// This transaction will be fetched from the `TransactionPool` that was passed at
+	/// initialization as part of the configuration and propagated to peers.
+	pub fn propagate_transaction(&self, hash: H) {
+		let _ = self.to_handler.unbounded_send(ToHandler::PropagateTransaction(hash));
+	}
+}
+
+enum ToHandler<H: ExHashT> {
+	PropagateTransactions,
+	PropagateTransaction(H),
+}
+
+/// Handler for transactions. Call [`TransactionsHandler::run`] to start the processing.
+pub struct TransactionsHandler<
+	B: BlockT + 'static,
+	H: ExHashT,
+	N: NetworkPeers + NetworkEventStream + NetworkNotification,
+	S: SyncEventStream + sp_consensus::SyncOracle,
+> {
+	protocol_name: ProtocolName,
+	/// Interval at which we call `propagate_transactions`.
+	propagate_timeout: stream::Fuse<Pin<Box<dyn Stream<Item = ()> + Send>>>,
+	/// Pending transactions verification tasks.
+	pending_transactions: FuturesUnordered<PendingTransaction<H>>,
+	/// As multiple peers can send us the same transaction, we group
+	/// these peers using the transaction hash while the transaction is
+	/// imported. This prevents that we import the same transaction
+	/// multiple times concurrently.
+	pending_transactions_peers: HashMap<H, Vec<PeerId>>,
+	/// Network service to use to send messages and manage peers.
+	network: N,
+	/// Syncing service.
+	sync: S,
+	/// Receiver for syncing-related events.
+	sync_event_stream: stream::Fuse<Pin<Box<dyn Stream<Item = SyncEvent> + Send>>>,
+	// All connected peers
+	peers: HashMap<PeerId, Peer<H>>,
+	transaction_pool: Arc<dyn TransactionPool<H, B>>,
+	from_controller: TracingUnboundedReceiver<ToHandler<H>>,
+	/// Prometheus metrics.
+	metrics: Option<Metrics>,
+	/// Handle that is used to communicate with `sc_network::Notifications`.
+	notification_service: Box<dyn NotificationService>,
+}
+
+/// Peer information
+#[derive(Debug)]
+struct Peer<H: ExHashT> {
+	/// Holds a set of transactions known to this peer.
+	known_transactions: LruHashSet<H>,
+	role: ObservedRole,
+}
+
+impl<B, H, N, S> TransactionsHandler<B, H, N, S>
+where
+	B: BlockT + 'static,
+	H: ExHashT,
+	N: NetworkPeers + NetworkEventStream + NetworkNotification,
+	S: SyncEventStream + sp_consensus::SyncOracle,
+{
+	/// Turns the [`TransactionsHandler`] into a future that should run forever and not be
+	/// interrupted.
+	pub async fn run(mut self) {
+		loop {
+			futures::select! {
+				_ = self.propagate_timeout.next() => {
+					self.propagate_transactions();
+				},
+				(tx_hash, result) = self.pending_transactions.select_next_some() => {
+					if let Some(peers) = self.pending_transactions_peers.remove(&tx_hash) {
+						peers.into_iter().for_each(|p| self.on_handle_transaction_import(p, result));
+					} else {
+						warn!(target: "sub-libp2p", "Inconsistent state, no peers for pending transaction!");
+					}
+				},
+				sync_event = self.sync_event_stream.next() => {
+					if let Some(sync_event) = sync_event {
+						self.handle_sync_event(sync_event);
+					} else {
+						// Syncing has seemingly closed. Closing as well.
+						return;
+					}
+				}
+				message = self.from_controller.select_next_some() => {
+					match message {
+						ToHandler::PropagateTransaction(hash) => self.propagate_transaction(&hash),
+						ToHandler::PropagateTransactions => self.propagate_transactions(),
+					}
+				},
+				event = self.notification_service.next_event().fuse() => {
+					if let Some(event) = event {
+						self.handle_notification_event(event)
+					} else {
+						// `Notifications` has seemingly closed. Closing as well.
+						return
+					}
+				}
+			}
+		}
+	}
+
+	fn handle_notification_event(&mut self, event: NotificationEvent) {
+		match event {
+			NotificationEvent::ValidateInboundSubstream { peer, handshake, result_tx, .. } => {
+				// only accept peers whose role can be determined
+				let result = self
+					.network
+					.peer_role(peer, handshake)
+					.map_or(ValidationResult::Reject, |_| ValidationResult::Accept);
+				let _ = result_tx.send(result);
+			},
+			NotificationEvent::NotificationStreamOpened { peer, handshake, .. } => {
+				let Some(role) = self.network.peer_role(peer, handshake) else {
+					log::debug!(target: "sub-libp2p", "role for {peer} couldn't be determined");
+					return
+				};
+
+				let _was_in = self.peers.insert(
+					peer,
+					Peer {
+						known_transactions: LruHashSet::new(
+							NonZeroUsize::new(MAX_KNOWN_TRANSACTIONS).expect("Constant is nonzero"),
+						),
+						role,
+					},
+				);
+				debug_assert!(_was_in.is_none());
+			},
+			NotificationEvent::NotificationStreamClosed { peer } => {
+				let _peer = self.peers.remove(&peer);
+				debug_assert!(_peer.is_some());
+			},
+			NotificationEvent::NotificationReceived { peer, notification } => {
+				if let Ok(m) =
+					<Transactions<B::Extrinsic> as Decode>::decode(&mut notification.as_ref())
+				{
+					self.on_transactions(peer, m);
+				} else {
+					warn!(target: "sub-libp2p", "Failed to decode transactions list");
+				}
+			},
+		}
+	}
+
+	fn handle_sync_event(&mut self, event: SyncEvent) {
+		match event {
+			SyncEvent::PeerConnected(remote) => {
+				let addr = iter::once(multiaddr::Protocol::P2p(remote.into()))
+					.collect::<multiaddr::Multiaddr>();
+				let result = self.network.add_peers_to_reserved_set(
+					self.protocol_name.clone(),
+					iter::once(addr).collect(),
+				);
+				if let Err(err) = result {
+					log::error!(target: "sync", "Add reserved peer failed: {}", err);
+				}
+			},
+			SyncEvent::PeerDisconnected(remote) => {
+				let result = self.network.remove_peers_from_reserved_set(
+					self.protocol_name.clone(),
+					iter::once(remote).collect(),
+				);
+				if let Err(err) = result {
+					log::error!(target: "sync", "Remove reserved peer failed: {}", err);
+				}
+			},
+		}
+	}
+
+	/// Called when peer sends us new transactions
+	fn on_transactions(&mut self, who: PeerId, transactions: Transactions<B::Extrinsic>) {
+		// Accept transactions only when node is not major syncing
+		if self.sync.is_major_syncing() {
+			trace!(target: "sync", "{} Ignoring transactions while major syncing", who);
+			return
+		}
+
+		trace!(target: "sync", "Received {} transactions from {}", transactions.len(), who);
+		if let Some(ref mut peer) = self.peers.get_mut(&who) {
+			for t in transactions {
+				if self.pending_transactions.len() > MAX_PENDING_TRANSACTIONS {
+					debug!(
+						target: "sync",
+						"Ignoring any further transactions that exceed `MAX_PENDING_TRANSACTIONS`({}) limit",
+						MAX_PENDING_TRANSACTIONS,
+					);
+					break
+				}
+
+				let hash = self.transaction_pool.hash_of(&t);
+				peer.known_transactions.insert(hash.clone());
+
+				self.network.report_peer(who, rep::ANY_TRANSACTION);
+
+				match self.pending_transactions_peers.entry(hash.clone()) {
+					Entry::Vacant(entry) => {
+						self.pending_transactions.push(PendingTransaction {
+							validation: self.transaction_pool.import(t),
+							tx_hash: hash,
+						});
+						entry.insert(vec![who]);
+					},
+					Entry::Occupied(mut entry) => {
+						entry.get_mut().push(who);
+					},
+				}
+			}
+		}
+	}
+
+	fn on_handle_transaction_import(&mut self, who: PeerId, import: TransactionImport) {
+		match import {
+			TransactionImport::KnownGood =>
+				self.network.report_peer(who, rep::ANY_TRANSACTION_REFUND),
+			TransactionImport::NewGood => self.network.report_peer(who, rep::GOOD_TRANSACTION),
+			TransactionImport::Bad => self.network.report_peer(who, rep::BAD_TRANSACTION),
+			TransactionImport::None => {},
+		}
+	}
+
+	/// Propagate one transaction.
+	pub fn propagate_transaction(&mut self, hash: &H) {
+		// Accept transactions only when node is not major syncing
+		if self.sync.is_major_syncing() {
+			return
+		}
+
+		debug!(target: "sync", "Propagating transaction [{:?}]", hash);
+		if let Some(transaction) = self.transaction_pool.transaction(hash) {
+			let propagated_to = self.do_propagate_transactions(&[(hash.clone(), transaction)]);
+			self.transaction_pool.on_broadcasted(propagated_to);
+		}
+	}
+
+	fn do_propagate_transactions(
+		&mut self,
+		transactions: &[(H, B::Extrinsic)],
+	) -> HashMap<H, Vec<String>> {
+		let mut propagated_to = HashMap::<_, Vec<_>>::new();
+		let mut propagated_transactions = 0;
+
+		for (who, peer) in self.peers.iter_mut() {
+			// never send transactions to the light node
+			if matches!(peer.role, ObservedRole::Light) {
+				continue
+			}
+
+			let (hashes, to_send): (Vec<_>, Vec<_>) = transactions
+				.iter()
+				.filter(|(hash, _)| peer.known_transactions.insert(hash.clone()))
+				.cloned()
+				.unzip();
+
+			propagated_transactions += hashes.len();
+
+			if !to_send.is_empty() {
+				for hash in hashes {
+					propagated_to.entry(hash).or_default().push(who.to_base58());
+				}
+				trace!(target: "sync", "Sending {} transactions to {}", to_send.len(), who);
+				// Historically, the format of a notification of the transactions protocol
+				// consisted in a (SCALE-encoded) `Vec<Transaction>`.
+				// After RFC 56, the format was modified in a backwards-compatible way to be
+				// a (SCALE-encoded) tuple `(Compact(1), Transaction)`, which is the same encoding
+				// as a `Vec` of length one. This is no coincidence, as the change was
+				// intentionally done in a backwards-compatible way.
+				// In other words, the `Vec` that is sent below **must** always have only a single
+				// element in it.
+				// See <https://github.com/polkadot-fellows/RFCs/blob/main/text/0056-one-transaction-per-notification.md>
+				for to_send in to_send {
+					let _ = self
+						.notification_service
+						.send_sync_notification(who, vec![to_send].encode());
+				}
+			}
+		}
+
+		if let Some(ref metrics) = self.metrics {
+			metrics.propagated_transactions.inc_by(propagated_transactions as _)
+		}
+
+		propagated_to
+	}
+
+	/// Call when we must propagate ready transactions to peers.
+	fn propagate_transactions(&mut self) {
+		// Accept transactions only when node is not major syncing
+		if self.sync.is_major_syncing() {
+			return
+		}
+
+		debug!(target: "sync", "Propagating transactions");
+		let transactions = self.transaction_pool.transactions();
+		let propagated_to = self.do_propagate_transactions(&transactions);
+		self.transaction_pool.on_broadcasted(propagated_to);
+	}
+}
diff --git a/protocols/bls/src/protocol/keygen.rs b/protocols/bls/src/protocol/keygen.rs
index a95296a68..7c8c941b8 100644
--- a/protocols/bls/src/protocol/keygen.rs
+++ b/protocols/bls/src/protocol/keygen.rs
@@ -176,7 +176,14 @@ pub(crate) async fn handle_public_key_broadcast<KBE: KeystoreBackend>(
     rx: &mut UnboundedReceiver<Msg<RoundPayload>>,
     user_id_to_account_id_mapping: &Arc<HashMap<UserID, ecdsa::Public>>,
 ) -> Result<
-    jobs::JobResult<MaxParticipants, MaxKeyLen, MaxSignatureLen, MaxDataLen, MaxProofLen>,
+    jobs::JobResult<
+        MaxParticipants,
+        MaxKeyLen,
+        MaxSignatureLen,
+        MaxDataLen,
+        MaxProofLen,
+        MaxAdditionalParamsLen,
+    >,
     JobError,
 > {
     let mut received_pk_shares = BTreeMap::new();
diff --git a/protocols/bls/src/protocol/signing.rs b/protocols/bls/src/protocol/signing.rs
index b0c5eedd3..3616b7473 100644
--- a/protocols/bls/src/protocol/signing.rs
+++ b/protocols/bls/src/protocol/signing.rs
@@ -229,6 +229,7 @@ pub async fn generate_protocol_from<C: ClientWithApi, N: Network, KBE: KeystoreB
                 data: BoundedVec(additional_params.input_data_to_sign.clone()),
                 signature: BoundedVec(signature),
                 verifying_key: BoundedVec(signing_key),
+                derivation_path: None,
                 __ignore: Default::default(),
             });
 
diff --git a/protocols/dfns-cggmp21/src/constants.rs b/protocols/dfns-cggmp21/src/constants.rs
index 017b35fe9..07669e97a 100644
--- a/protocols/dfns-cggmp21/src/constants.rs
+++ b/protocols/dfns-cggmp21/src/constants.rs
@@ -1,8 +1,8 @@
 // ================= Common ======================== //
-pub const DFNS_CGGMP21_KEYGEN_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keygen/1";
-pub const DFNS_CGGMP21_SIGNING_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/signing/1";
-pub const DFNS_CGGMP21_KEYREFRESH_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keyrefresh/1";
-pub const DFNS_CGGMP21_KEYROTATE_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keyrotate/1";
+pub const DFNS_CGGMP21_KEYGEN_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keygen/1.0.0";
+pub const DFNS_CGGMP21_SIGNING_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/signing/1.0.0";
+pub const DFNS_CGGMP21_KEYREFRESH_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keyrefresh/1.0.0";
+pub const DFNS_CGGMP21_KEYROTATE_PROTOCOL_NAME: &str = "/tangle/dfns-cggmp21/keyrotate/1.0.0";
 
 // ============= Signing Protocol ======================= //
 
diff --git a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
index d600f41bd..49c78f600 100644
--- a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
+++ b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs
@@ -270,6 +270,7 @@ pub async fn generate_protocol_from<C: ClientWithApi, KBE: KeystoreBackend, N: N
                         new_phase_one_id,
                         new_key: BoundedVec(new_public_key),
                         key: BoundedVec(public_key),
+                        derivation_path: None,
                         __ignore: Default::default(),
                     });
 
diff --git a/protocols/dfns-cggmp21/src/protocols/keygen.rs b/protocols/dfns-cggmp21/src/protocols/keygen.rs
index 4ede8e392..32755cbe7 100644
--- a/protocols/dfns-cggmp21/src/protocols/keygen.rs
+++ b/protocols/dfns-cggmp21/src/protocols/keygen.rs
@@ -460,7 +460,14 @@ pub async fn handle_public_key_gossip<KBE: KeystoreBackend>(
     broadcast_tx_to_outbound: UnboundedSender<PublicKeyGossipMessage>,
     mut broadcast_rx_from_gadget: futures::channel::mpsc::UnboundedReceiver<PublicKeyGossipMessage>,
 ) -> Result<
-    jobs::JobResult<MaxParticipants, MaxKeyLen, MaxSignatureLen, MaxDataLen, MaxProofLen>,
+    jobs::JobResult<
+        MaxParticipants,
+        MaxKeyLen,
+        MaxSignatureLen,
+        MaxDataLen,
+        MaxProofLen,
+        MaxAdditionalParamsLen,
+    >,
     JobError,
 > {
     let key_hashed = keccak_256(serialized_public_key);
@@ -482,7 +489,9 @@ pub async fn handle_public_key_gossip<KBE: KeystoreBackend>(
             reason: format!("Failed to send public key: {err:?}"),
         })?;
 
-    for _ in 0..t {
+    logger.debug(format!("Needs to receive {} signatures", t - 1));
+
+    for _ in 0..(t - 1) {
         let message = broadcast_rx_from_gadget
             .next()
             .await
@@ -517,11 +526,7 @@ pub async fn handle_public_key_gossip<KBE: KeystoreBackend>(
 
         received_keys.insert(from as u16, message.signature);
         received_participants.insert(from as u16, message.id);
-        logger.debug(format!(
-            "Received {}/{} signatures",
-            received_keys.len(),
-            t + 1
-        ));
+        logger.debug(format!("Received {}/{t} signatures", received_keys.len()));
     }
 
     // Order and collect the map to ensure symmetric submission to blockchain
@@ -540,9 +545,8 @@ pub async fn handle_public_key_gossip<KBE: KeystoreBackend>(
     if signatures.len() < t as usize {
         return Err(JobError {
             reason: format!(
-                "Received {} signatures, expected at least {}",
+                "Received {} signatures, expected at least {t}",
                 signatures.len(),
-                t + 1,
             ),
         });
     }
@@ -602,13 +606,13 @@ fn verify_generated_dkg_key_ecdsa(
 
     // Ensure a sufficient number of unique signers are present
     assert!(
-        known_signers.len() > data.threshold as usize,
+        known_signers.len() >= data.threshold as usize,
         "NotEnoughSigners"
     );
     logger.debug(format!(
         "Verified {}/{} signatures",
         known_signers.len(),
-        data.threshold + 1
+        data.threshold
     ));
 }
 
diff --git a/protocols/dfns-cggmp21/src/protocols/sign.rs b/protocols/dfns-cggmp21/src/protocols/sign.rs
index b69ccfb4f..49f0d7d65 100644
--- a/protocols/dfns-cggmp21/src/protocols/sign.rs
+++ b/protocols/dfns-cggmp21/src/protocols/sign.rs
@@ -271,6 +271,7 @@ pub async fn generate_protocol_from<KBE: KeystoreBackend, C: ClientWithApi, N: N
                     data: BoundedVec(data_hash.to_vec()),
                     signature: BoundedVec(signature.to_vec()),
                     verifying_key: BoundedVec(public_key),
+                    derivation_path: None,
                     __ignore: Default::default(),
                 });
 
diff --git a/protocols/silent-shard-dkls23-ll/src/lib.rs b/protocols/silent-shard-dkls23-ll/src/lib.rs
index 2dde4519d..4bd20621b 100644
--- a/protocols/silent-shard-dkls23-ll/src/lib.rs
+++ b/protocols/silent-shard-dkls23-ll/src/lib.rs
@@ -16,7 +16,7 @@ generate_protocol!(
     crate::protocol::keygen::generate_protocol_from,
     crate::protocol::keygen::create_next_job,
     jobs::JobType::DKGTSSPhaseOne(_),
-    roles::RoleType::Tss(roles::tss::ThresholdSignatureRoleType::SilentShardDKLS23Secpk256k1)
+    roles::RoleType::Tss(roles::tss::ThresholdSignatureRoleType::SilentShardDKLS23Secp256k1)
 );
 generate_protocol!(
     "Silent-Shared-DKLS23-Signing-Protocol",
@@ -25,7 +25,7 @@ generate_protocol!(
     crate::protocol::sign::generate_protocol_from,
     crate::protocol::sign::create_next_job,
     jobs::JobType::DKGTSSPhaseTwo(_),
-    roles::RoleType::Tss(roles::tss::ThresholdSignatureRoleType::SilentShardDKLS23Secpk256k1)
+    roles::RoleType::Tss(roles::tss::ThresholdSignatureRoleType::SilentShardDKLS23Secp256k1)
 );
 
 generate_setup_and_run_command!(
diff --git a/protocols/silent-shard-dkls23-ll/src/protocol/sign.rs b/protocols/silent-shard-dkls23-ll/src/protocol/sign.rs
index 0043a4041..3bc8a5b5f 100644
--- a/protocols/silent-shard-dkls23-ll/src/protocol/sign.rs
+++ b/protocols/silent-shard-dkls23-ll/src/protocol/sign.rs
@@ -201,6 +201,7 @@ pub async fn generate_protocol_from<KBE: KeystoreBackend, C: ClientWithApi, N: N
                     data: BoundedVec(data_hash.to_vec()),
                     signature: BoundedVec(signature.to_vec()),
                     verifying_key: BoundedVec(verifying_key.to_bytes().to_vec()),
+                    derivation_path: None,
                     __ignore: Default::default(),
                 });
 
diff --git a/protocols/stub/src/lib.rs b/protocols/stub/src/lib.rs
index 0e7439bdb..d9508368d 100644
--- a/protocols/stub/src/lib.rs
+++ b/protocols/stub/src/lib.rs
@@ -86,7 +86,7 @@ impl<C: ClientWithApi + 'static, N: Network, KBE: KeystoreBackend> FullProtocolC
 
     fn phase_filter(
         &self,
-        _job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen>,
+        _job: jobs::JobType<AccountId32, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>,
     ) -> bool {
         false
     }
diff --git a/protocols/zcash-frost/src/protocol/keygen.rs b/protocols/zcash-frost/src/protocol/keygen.rs
index 95bc2f151..f94c3e291 100644
--- a/protocols/zcash-frost/src/protocol/keygen.rs
+++ b/protocols/zcash-frost/src/protocol/keygen.rs
@@ -304,7 +304,14 @@ async fn handle_public_key_gossip<KBE: KeystoreBackend>(
     broadcast_tx_to_outbound: futures::channel::mpsc::UnboundedSender<PublicKeyGossipMessage>,
     mut broadcast_rx_from_gadget: futures::channel::mpsc::UnboundedReceiver<PublicKeyGossipMessage>,
 ) -> Result<
-    jobs::JobResult<MaxParticipants, MaxKeyLen, MaxSignatureLen, MaxDataLen, MaxProofLen>,
+    jobs::JobResult<
+        MaxParticipants,
+        MaxKeyLen,
+        MaxSignatureLen,
+        MaxDataLen,
+        MaxProofLen,
+        MaxAdditionalParamsLen,
+    >,
     JobError,
 > {
     let key_hashed = keccak_256(public_key_package);
diff --git a/protocols/zcash-frost/src/protocol/sign.rs b/protocols/zcash-frost/src/protocol/sign.rs
index 6ba16f7e3..afdaad6de 100644
--- a/protocols/zcash-frost/src/protocol/sign.rs
+++ b/protocols/zcash-frost/src/protocol/sign.rs
@@ -350,6 +350,7 @@ pub async fn generate_protocol_from<C: ClientWithApi, N: Network, KBE: KeystoreB
                     data: BoundedVec(additional_params.input_data_to_sign),
                     signature: BoundedVec(signature),
                     verifying_key: BoundedVec(Default::default()),
+                    derivation_path: None,
                     __ignore: Default::default(),
                 });
 
diff --git a/shell/Cargo.toml b/shell/Cargo.toml
new file mode 100644
index 000000000..80a2735b7
--- /dev/null
+++ b/shell/Cargo.toml
@@ -0,0 +1,64 @@
+[package]
+name = "gadget-shell"
+version = "0.1.0"
+authors = ["Shady Khalifa <dev+github@shadykhalifa.me>"]
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+edition = "2021"
+
+[features]
+
+[dependencies]
+color-eyre = { version = "0.6", features = ["tracing-error", "color-spantrace", "issue-url"] }
+async-trait = { workspace = true }
+tracing = { workspace = true, features = ["log"] }
+tracing-subscriber = { workspace = true, features = ["env-filter", "ansi"] }
+tokio = { workspace = true, features = ["full"] }
+url = { workspace = true, features = ["serde"] }
+
+gadget-core = { workspace = true }
+gadget-common = { workspace = true }
+
+sp-core = { workspace = true, features = ["std"] }
+sp-keystore = { workspace = true }
+sp-application-crypto = { workspace = true }
+
+sc-keystore = { workspace = true }
+
+tangle-subxt = { workspace = true }
+
+scale-info = { workspace = true }
+parity-scale-codec = { workspace = true }
+
+structopt = "0.3.26"
+toml = "0.8.11"
+serde = { workspace = true }
+hex = { workspace = true }
+sha2 = "0.10.8"
+futures = { workspace = true }
+bincode2 = { workspace = true }
+# Protocols
+dfns-cggmp21-protocol = { workspace = true }
+
+[dependencies.libp2p]
+workspace = true
+features = [
+  "tokio",
+  "gossipsub",
+  "mdns",
+  "noise",
+  "macros",
+  "yamux",
+  "tcp",
+  "quic",
+  "request-response",
+  "cbor",
+  "identify",
+  "kad",
+    "dcutr",
+    "relay",
+    "ping",
+    "dns"
+]
+
+[dev-dependencies]
+test-utils = { workspace = true }
diff --git a/shell/DEMO.md b/shell/DEMO.md
new file mode 100644
index 000000000..aba135eec
--- /dev/null
+++ b/shell/DEMO.md
@@ -0,0 +1,64 @@
+## How to run the shell locally
+
+To be able to run the demo locally, you will need a tangle node and this repository.
+
+### Setup Tangle Node
+
+1. Clone [`tangle`](https://github.com/webb-tools/tangle) Locally
+```bash
+git clone https://github.com/webb-tools/tangle.git
+```
+2. Checkout the `shady/gadget-shell-testing` branch
+```bash
+git checkout shady/gadget-shell-testing
+```
+3. Compile tangle in release mode.
+```bash
+cargo build --release
+```
+4. Run the `target/release/tangle` binary using the following script:
+```bash
+bash ./scripts/run-standalone-local.sh --clean
+```
+
+### Setup Gadget Shell
+
+1. Compile the Gadget Shell in release mode
+```bash
+cargo build --release
+```
+2. Run the `target/release/gadget-shell` binary using the following command:
+
+```bash
+./target/release/gadget-shell --config shell-configs/local-testnet-0.toml -vvv
+```
+
+> Note: The `--config` flag is used to specify the configuration file to use. In this case, we are using the `local-testnet-0.toml` configuration file.
+
+3. Run different shells with different configurations to simulate a network of shells.
+
+```bash
+./target/release/gadget-shell --config shell-configs/local-testnet-1.toml -vvv
+```
+
+```bash
+./target/release/gadget-shell --config shell-configs/local-testnet-2.toml -vvv
+```
+
+> Note: You will notice they automatically connect to each other and start exchanging messages.
+
+### Polkadot JS Apps
+
+Additionally, you may also want to run a local instance of Polkadot JS Apps to interact with the node, create jobs, etc.
+Go to https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944 and connect to the local node.
+
+### Send Jobs Using a Playground Script
+
+You can also use the `playground` script to send jobs to the network. To do this, you will need to have the `tangle` binary running and the `gadget-shell` running.
+Then, in the tangle repo, enter the `types` directory and run the following command:
+
+```bash
+ts-node playground.ts
+```
+
+This will create profiles for `Alice`, `Bob` and `Charlie`, and send a DKG Phase One job to the network. You can then use the Polkadot JS apps to see the job being processed.
diff --git a/shell/src/config.rs b/shell/src/config.rs
new file mode 100644
index 000000000..23f9faa18
--- /dev/null
+++ b/shell/src/config.rs
@@ -0,0 +1,45 @@
+use libp2p::Multiaddr;
+use sp_core::crypto::SecretString;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone)]
+pub struct ShellConfig {
+    pub keystore: KeystoreConfig,
+    pub subxt: SubxtConfig,
+    pub bind_ip: String,
+    pub bind_port: u16,
+    pub bootnodes: Vec<Multiaddr>,
+    pub node_key: [u8; 32],
+}
+
+#[derive(Debug, Clone)]
+pub struct SubxtConfig {
+    /// The URL of the Tangle Node.
+    pub endpoint: url::Url,
+}
+
+/// Configuration of the client keystore.
+#[derive(Debug, Clone)]
+#[allow(dead_code)]
+pub enum KeystoreConfig {
+    /// Keystore at a path on-disk. Recommended for native gadgets.
+    Path {
+        /// The path of the keystore.
+        path: PathBuf,
+        /// keystore's password.
+        password: Option<SecretString>,
+    },
+    /// In-memory keystore.
+    InMemory,
+}
+
+impl KeystoreConfig {
+    /// Returns the path for the keystore.
+    #[allow(dead_code)]
+    pub fn path(&self) -> Option<&Path> {
+        match self {
+            Self::Path { path, .. } => Some(path),
+            Self::InMemory => None,
+        }
+    }
+}
diff --git a/shell/src/keystore.rs b/shell/src/keystore.rs
new file mode 100644
index 000000000..4470aba73
--- /dev/null
+++ b/shell/src/keystore.rs
@@ -0,0 +1,34 @@
+use color_eyre::Result;
+use sc_keystore::LocalKeystore;
+use sp_keystore::KeystorePtr;
+use std::sync::Arc;
+
+use crate::config::KeystoreConfig;
+
+/// Construct a local keystore shareable container
+pub struct KeystoreContainer(Arc<LocalKeystore>);
+
+impl KeystoreContainer {
+    /// Construct KeystoreContainer
+    pub fn new(config: &KeystoreConfig) -> Result<Self> {
+        let keystore = Arc::new(match config {
+            KeystoreConfig::Path { path, password } => {
+                LocalKeystore::open(path.clone(), password.clone())?
+            }
+            KeystoreConfig::InMemory => LocalKeystore::in_memory(),
+        });
+
+        Ok(Self(keystore))
+    }
+
+    /// Returns a shared reference to a dynamic `Keystore` trait implementation.
+    #[allow(dead_code)]
+    pub fn keystore(&self) -> KeystorePtr {
+        self.0.clone()
+    }
+
+    /// Returns a shared reference to the local keystore .
+    pub fn local_keystore(&self) -> Arc<LocalKeystore> {
+        self.0.clone()
+    }
+}
diff --git a/shell/src/main.rs b/shell/src/main.rs
new file mode 100644
index 000000000..7b4eb201d
--- /dev/null
+++ b/shell/src/main.rs
@@ -0,0 +1,107 @@
+use color_eyre::Result;
+use libp2p::Multiaddr;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use structopt::StructOpt;
+
+mod config;
+mod keystore;
+mod network;
+mod protocols;
+mod shell;
+mod tangle;
+
+#[derive(Debug, StructOpt)]
+#[structopt(
+    name = "Gadget",
+    about = "An MPC executor that connects to the Tangle network to perform work"
+)]
+struct Opt {
+    /// Input file
+    #[structopt(parse(from_os_str), short = "c", long = "config")]
+    config: PathBuf,
+    #[structopt(long, short = "v", parse(from_occurrences))]
+    verbose: i32,
+    #[structopt(long)]
+    pretty: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+struct TomlConfig {
+    bind_ip: String,
+    bind_port: u16,
+    bootnodes: Vec<String>,
+    /// The node key in hex format
+    node_key: String,
+    keystore_path: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    color_eyre::install()?;
+    let opt = Opt::from_args();
+    setup_logger(&opt, "gadget_shell")?;
+    let config_contents = std::fs::read_to_string(opt.config)?;
+    let config: TomlConfig = toml::from_str(&config_contents)?;
+    /*
+    // Hardcoded paths for now.
+    let keystore_path = std::path::PathBuf::from(format!(
+        "{}../../target/tangle/chains/local_testnet/keystore",
+        env!("CARGO_MANIFEST_DIR")
+    ));*/
+    let keystore_path = std::path::PathBuf::from(config.keystore_path);
+
+    let mut bootnodes = vec![];
+    for bootnode in config.bootnodes.iter() {
+        let addr: Multiaddr = bootnode.parse()?;
+        bootnodes.push(addr);
+    }
+
+    let decoded_node_key = hex::decode(config.node_key)
+        .map_err(|e| color_eyre::eyre::eyre!("Failed to parse node key: {e}"))?;
+
+    shell::run_forever(config::ShellConfig {
+        keystore: config::KeystoreConfig::Path {
+            path: keystore_path,
+            password: None,
+        },
+        subxt: config::SubxtConfig {
+            endpoint: url::Url::parse("ws://127.0.0.1:9944")?,
+        },
+        bind_ip: config.bind_ip,
+        bind_port: config.bind_port,
+        bootnodes,
+        node_key: <[u8; 32]>::try_from(decoded_node_key.as_slice())
+            .map_err(|e| color_eyre::eyre::eyre!("Failed to parse node key: {e}"))?,
+    })
+    .await?;
+    Ok(())
+}
+
+/// Sets up the logger for the shell, based on the verbosity level passed in.
+fn setup_logger(opt: &Opt, filter: &str) -> Result<()> {
+    use tracing::Level;
+    let log_level = match opt.verbose {
+        0 => Level::ERROR,
+        1 => Level::WARN,
+        2 => Level::INFO,
+        3 => Level::DEBUG,
+        _ => Level::TRACE,
+    };
+    let env_filter = tracing_subscriber::EnvFilter::from_default_env()
+        .add_directive(format!("{filter}={log_level}").parse()?)
+        .add_directive(format!("gadget={log_level}").parse()?);
+    let logger = tracing_subscriber::fmt()
+        .with_target(false)
+        .with_level(true)
+        .with_line_number(false)
+        .without_time()
+        .with_max_level(log_level)
+        .with_env_filter(env_filter);
+    if opt.pretty {
+        logger.pretty().init();
+    } else {
+        logger.compact().init();
+    }
+    Ok(())
+}
diff --git a/shell/src/network.rs b/shell/src/network.rs
new file mode 100644
index 000000000..93472488e
--- /dev/null
+++ b/shell/src/network.rs
@@ -0,0 +1,1134 @@
+use crate::config::ShellConfig;
+use crate::shell::{AGENT_VERSION, CLIENT_VERSION};
+use async_trait::async_trait;
+use futures::stream::StreamExt;
+use gadget_common::prelude::{DebugLogger, Network, WorkManager};
+use gadget_core::job_manager::WorkManagerInterface;
+use libp2p::gossipsub::{IdentTopic, TopicHash};
+use libp2p::kad::store::MemoryStore;
+use libp2p::swarm::dial_opts::DialOpts;
+use libp2p::{
+    gossipsub, mdns, request_response, swarm::NetworkBehaviour, swarm::SwarmEvent, PeerId,
+    StreamProtocol,
+};
+use serde::{Deserialize, Serialize};
+use sp_core::ecdsa;
+use std::collections::HashMap;
+use std::error::Error;
+use std::sync::atomic::AtomicU32;
+use std::sync::Arc;
+use std::time::Duration;
+use tokio::sync::mpsc::UnboundedSender;
+use tokio::sync::{Mutex, RwLock};
+use tokio::task::JoinHandle;
+use tokio::{io, select};
+
+/// Maximum allowed size for a Signed Message.
+pub const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
+
+// We create a custom network behaviour that combines Gossipsub and Mdns.
+#[derive(NetworkBehaviour)]
+struct MyBehaviour {
+    gossipsub: gossipsub::Behaviour,
+    mdns: mdns::tokio::Behaviour,
+    p2p: request_response::cbor::Behaviour<MyBehaviourRequest, MyBehaviourResponse>,
+    identify: libp2p::identify::Behaviour,
+    kadmelia: libp2p::kad::Behaviour<MemoryStore>,
+    dcutr: libp2p::dcutr::Behaviour,
+    relay: libp2p::relay::Behaviour,
+    relay_client: libp2p::relay::client::Behaviour,
+    ping: libp2p::ping::Behaviour,
+}
+
+pub async fn setup_network(
+    identity: libp2p::identity::Keypair,
+    config: &ShellConfig,
+    logger: DebugLogger,
+    networks: Vec<&'static str>,
+    role_key: ecdsa::Public,
+) -> Result<(Vec<GossipHandle>, JoinHandle<()>), Box<dyn Error>> {
+    // Setup both QUIC (UDP) and TCP transports the increase the chances of NAT traversal
+    let mut swarm = libp2p::SwarmBuilder::with_existing_identity(identity)
+        .with_tokio()
+        .with_tcp(
+            libp2p::tcp::Config::default()
+                .port_reuse(true)
+                .nodelay(true), // Allow port reuse for TCP-hole punching
+            libp2p::noise::Config::new,
+            libp2p::yamux::Config::default,
+        )?
+        .with_quic_config(|mut config| {
+            config.handshake_timeout = Duration::from_secs(30);
+            config
+        })
+        .with_dns()?
+        .with_relay_client(libp2p::noise::Config::new, libp2p::yamux::Config::default)?
+        .with_behaviour(|key, relay_client| {
+            // Set a custom gossipsub configuration
+            let gossipsub_config = gossipsub::ConfigBuilder::default()
+                .protocol_id_prefix("/tangle/gadget-shell/meshsub")
+                .max_transmit_size(MAX_MESSAGE_SIZE)
+                .validate_messages()
+                .validation_mode(gossipsub::ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing)
+                .build()
+                .map_err(|msg| io::Error::new(io::ErrorKind::Other, msg))?; // Temporary hack because `build` does not return a proper `std::error::Error`.
+
+            // Setup gossipsub network behaviour for broadcasting
+            let gossipsub = gossipsub::Behaviour::new(
+                gossipsub::MessageAuthenticity::Signed(key.clone()),
+                gossipsub_config,
+            )?;
+
+            // Setup mDNS for peer discovery
+            let mdns =
+                mdns::tokio::Behaviour::new(mdns::Config::default(), key.public().to_peer_id())?;
+
+            // Setup request-response for direct messaging
+            let p2p_config = request_response::Config::default();
+            // StreamProtocols MUST begin with a forward slash
+            let protocols = networks
+                .iter()
+                .map(|n| {
+                    (
+                        StreamProtocol::new(n),
+                        request_response::ProtocolSupport::Full,
+                    )
+                })
+                .collect::<Vec<_>>();
+
+            let p2p = request_response::Behaviour::new(protocols, p2p_config);
+
+            // Setup the identify protocol for peers to exchange information about each other, a requirement for kadmelia DHT
+            let identify = libp2p::identify::Behaviour::new(
+                libp2p::identify::Config::new(CLIENT_VERSION.into(), key.public())
+                    .with_agent_version(AGENT_VERSION.into()),
+            );
+
+            // Setup kadmelia for DHT for peer discovery over a larger network
+            let memory_db = MemoryStore::new(key.public().to_peer_id());
+            let kadmelia = libp2p::kad::Behaviour::new(key.public().to_peer_id(), memory_db);
+
+            // Setup dcutr for upgrading existing connections to use relay against the bootnodes when necessary
+            // This also provided hole-punching capabilities to attempt to seek a direct connection, and fallback to relaying
+            // otherwise.
+            // dcutr = direct connection upgrade through relay
+            let dcutr = libp2p::dcutr::Behaviour::new(key.public().to_peer_id());
+
+            // Setup relay for using the dcutr-upgraded connections to relay messages for other peers when required
+            let relay_config = libp2p::relay::Config::default();
+            let relay = libp2p::relay::Behaviour::new(key.public().to_peer_id(), relay_config);
+
+            // Setup ping for liveness checks between connections
+            let ping = libp2p::ping::Behaviour::new(libp2p::ping::Config::default());
+
+            Ok(MyBehaviour {
+                gossipsub,
+                mdns,
+                p2p,
+                identify,
+                kadmelia,
+                dcutr,
+                relay,
+                relay_client,
+                ping,
+            })
+        })?
+        .with_swarm_config(|c| c.with_idle_connection_timeout(Duration::from_secs(60)))
+        .build();
+
+    // Subscribe to all networks
+    let mut inbound_mapping = Vec::new();
+    let (tx_to_outbound, mut rx_to_outbound) =
+        tokio::sync::mpsc::unbounded_channel::<IntraNodePayload>();
+    let ecdsa_peer_id_to_libp2p_id = Arc::new(RwLock::new(HashMap::new()));
+    let mut handles_ret = vec![];
+    for network in networks {
+        let topic = IdentTopic::new(network);
+        swarm.behaviour_mut().gossipsub.subscribe(&topic)?;
+        let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel();
+        let connected_peers = Arc::new(AtomicU32::new(0));
+        inbound_mapping.push((topic.clone(), inbound_tx, connected_peers.clone()));
+
+        handles_ret.push(GossipHandle {
+            connected_peers,
+            topic,
+            tx_to_outbound: tx_to_outbound.clone(),
+            rx_from_inbound: Arc::new(Mutex::new(inbound_rx)),
+            logger: logger.clone(),
+            ecdsa_peer_id_to_libp2p_id: ecdsa_peer_id_to_libp2p_id.clone(),
+        })
+    }
+
+    swarm
+        .listen_on(format!("/ip4/{}/udp/{}/quic-v1", config.bind_ip, config.bind_port).parse()?)?;
+    swarm.listen_on(format!("/ip4/{}/tcp/{}", config.bind_ip, config.bind_port).parse()?)?;
+
+    // Dial all bootnodes
+    for bootnode in &config.bootnodes {
+        swarm.dial(
+            DialOpts::unknown_peer_id()
+                .address(bootnode.clone())
+                .build(),
+        )?;
+    }
+
+    let worker = async move {
+        let span = tracing::debug_span!("network_worker");
+        let _enter = span.enter();
+        let service = NetworkServiceWithoutSwarm {
+            logger: &logger,
+            inbound_mapping: &inbound_mapping,
+            ecdsa_peer_id_to_libp2p_id,
+            role_key: &role_key,
+            span: tracing::debug_span!(parent: &span, "network_service"),
+        };
+        loop {
+            select! {
+                // Setup outbound channel
+                Some(msg) = rx_to_outbound.recv() => {
+                    service.with_swarm(&mut swarm).handle_intra_node_payload(msg);
+                }
+                event = swarm.select_next_some() => {
+                    service.with_swarm(&mut swarm).handle_swarm_event(event).await;
+                }
+            }
+        }
+    };
+
+    let spawn_handle = tokio::task::spawn(worker);
+    Ok((handles_ret, spawn_handle))
+}
+
+type InboundMapping = (IdentTopic, UnboundedSender<Vec<u8>>, Arc<AtomicU32>);
+
+struct NetworkServiceWithoutSwarm<'a> {
+    logger: &'a DebugLogger,
+    inbound_mapping: &'a [InboundMapping],
+    ecdsa_peer_id_to_libp2p_id: Arc<RwLock<HashMap<ecdsa::Public, PeerId>>>,
+    role_key: &'a ecdsa::Public,
+    span: tracing::Span,
+}
+
+impl<'a> NetworkServiceWithoutSwarm<'a> {
+    fn with_swarm(&'a self, swarm: &'a mut libp2p::Swarm<MyBehaviour>) -> NetworkService<'a> {
+        NetworkService {
+            swarm,
+            logger: self.logger,
+            inbound_mapping: self.inbound_mapping,
+            ecdsa_peer_id_to_libp2p_id: &self.ecdsa_peer_id_to_libp2p_id,
+            role_key: self.role_key,
+            span: &self.span,
+        }
+    }
+}
+
+struct NetworkService<'a> {
+    swarm: &'a mut libp2p::Swarm<MyBehaviour>,
+    logger: &'a DebugLogger,
+    inbound_mapping: &'a [InboundMapping],
+    ecdsa_peer_id_to_libp2p_id: &'a Arc<RwLock<HashMap<ecdsa::Public, PeerId>>>,
+    role_key: &'a ecdsa::Public,
+    span: &'a tracing::Span,
+}
+
+impl<'a> NetworkService<'a> {
+    fn handle_intra_node_payload(&mut self, msg: IntraNodePayload) {
+        let _enter = self.span.enter();
+        match (msg.message_type, msg.payload) {
+            (MessageType::Broadcast, GossipOrRequestResponse::Gossip(payload)) => {
+                let gossip_message = bincode2::serialize(&payload).expect("Should serialize");
+                if let Err(e) = self
+                    .swarm
+                    .behaviour_mut()
+                    .gossipsub
+                    .publish(msg.topic, gossip_message)
+                {
+                    self.logger.error(format!("Publish error: {e:?}"));
+                }
+            }
+
+            (MessageType::P2P(peer_id), GossipOrRequestResponse::Request(req)) => {
+                // Send the outer payload in order to attach the topic to it
+                // "Requests are sent using Behaviour::send_request and the responses
+                // received as Message::Response via Event::Message."
+                self.swarm.behaviour_mut().p2p.send_request(&peer_id, req);
+            }
+            (MessageType::Broadcast, GossipOrRequestResponse::Request(_)) => {
+                self.logger.error("Broadcasting a request is not supported");
+            }
+            (MessageType::Broadcast, GossipOrRequestResponse::Response(_)) => {
+                self.logger
+                    .error("Broadcasting a response is not supported");
+            }
+            (MessageType::P2P(_), GossipOrRequestResponse::Gossip(_)) => {
+                self.logger
+                    .error("P2P message should be a request or response");
+            }
+            (MessageType::P2P(_), GossipOrRequestResponse::Response(_)) => {
+                // TODO: Send the response to the peer.
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_p2p(
+        &mut self,
+        event: request_response::Event<MyBehaviourRequest, MyBehaviourResponse>,
+    ) {
+        use request_response::Event::*;
+        match event {
+            Message { peer, message } => {
+                self.logger
+                    .debug(format!("Received P2P message from: {peer}"));
+                self.handle_p2p_message(peer, message).await;
+            }
+            OutboundFailure {
+                peer,
+                request_id,
+                error,
+            } => {
+                self.logger.error(format!("Failed to send message to peer: {peer} with request_id: {request_id} and error: {error}"));
+            }
+            InboundFailure {
+                peer,
+                request_id,
+                error,
+            } => {
+                self.logger.error(format!("Failed to receive message from peer: {peer} with request_id: {request_id} and error: {error}"));
+            }
+            ResponseSent { peer, request_id } => {
+                self.logger.debug(format!(
+                    "Sent response to peer: {peer} with request_id: {request_id}"
+                ));
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, message))]
+    async fn handle_p2p_message(
+        &mut self,
+        peer: PeerId,
+        message: request_response::Message<MyBehaviourRequest, MyBehaviourResponse>,
+    ) {
+        use request_response::Message::*;
+        match message {
+            Request {
+                request,
+                channel,
+                request_id,
+            } => {
+                self.logger.debug(format!(
+                    "Received request with request_id: {request_id} from peer: {peer}"
+                ));
+                self.handle_p2p_request(peer, request_id, request, channel)
+                    .await;
+            }
+            Response {
+                response,
+                request_id,
+            } => {
+                self.logger.debug(format!(
+                    "Received response from peer: {peer} with request_id: {request_id}"
+                ));
+                self.handle_p2p_response(peer, request_id, response).await;
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, message))]
+    async fn handle_p2p_response(
+        &mut self,
+        peer: PeerId,
+        _request_id: request_response::OutboundRequestId,
+        message: MyBehaviourResponse,
+    ) {
+        use MyBehaviourResponse::*;
+        match message {
+            Handshaked { ecdsa_public_key } => {
+                // Their response to our handshake request.
+                // we should add them to our mapping.
+                // TODO: Add signature verification here.
+                self.ecdsa_peer_id_to_libp2p_id
+                    .write()
+                    .await
+                    .insert(ecdsa_public_key, peer);
+            }
+            MessageHandled => {}
+        }
+    }
+
+    #[tracing::instrument(skip(self, req, channel))]
+    async fn handle_p2p_request(
+        &mut self,
+        peer: PeerId,
+        request_id: request_response::InboundRequestId,
+        req: MyBehaviourRequest,
+        channel: request_response::ResponseChannel<MyBehaviourResponse>,
+    ) {
+        use MyBehaviourRequest::*;
+        let result = match req {
+            Handshake { ecdsa_public_key } => {
+                self.logger
+                    .debug(format!("Received handshake from peer: {peer}"));
+                self.ecdsa_peer_id_to_libp2p_id
+                    .write()
+                    .await
+                    .insert(ecdsa_public_key, peer);
+                self.swarm.behaviour_mut().p2p.send_response(
+                    channel,
+                    MyBehaviourResponse::Handshaked {
+                        ecdsa_public_key: *self.role_key,
+                    },
+                )
+            }
+            Message { topic, raw_payload } => {
+                let topic = IdentTopic::new(topic);
+                if let Some((_, tx, _)) = self
+                    .inbound_mapping
+                    .iter()
+                    .find(|r| r.0.to_string() == topic.to_string())
+                {
+                    if let Err(e) = tx.send(raw_payload) {
+                        self.logger
+                            .error(format!("Failed to send message to worker: {e}"));
+                    }
+                } else {
+                    self.logger
+                        .error(format!("No registered worker for topic: {topic}!"));
+                }
+                self.swarm
+                    .behaviour_mut()
+                    .p2p
+                    .send_response(channel, MyBehaviourResponse::MessageHandled)
+            }
+        };
+        if result.is_err() {
+            self.logger
+                .error(format!("Failed to send response for {request_id}"));
+        }
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_gossip(&mut self, event: gossipsub::Event) {
+        use gossipsub::Event::*;
+        let with_connected_peers = |topic: &TopicHash, f: fn(&Arc<AtomicU32>)| {
+            let maybe_mapping = self
+                .inbound_mapping
+                .iter()
+                .find(|r| r.0.to_string() == topic.to_string());
+            match maybe_mapping {
+                Some((_, _, connected_peers)) => {
+                    f(connected_peers);
+                    true
+                }
+                None => false,
+            }
+        };
+        match event {
+            Message {
+                propagation_source,
+                message_id,
+                message,
+            } => {
+                self.handle_gossip_message(propagation_source, message_id, message)
+                    .await;
+            }
+            Subscribed { peer_id, topic } => {
+                let added = with_connected_peers(&topic, |connected_peers| {
+                    connected_peers.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+                });
+                if added {
+                    self.logger
+                        .trace(format!("{peer_id} subscribed to {topic}",));
+                } else {
+                    self.logger
+                        .error(format!("{peer_id} subscribed to unknown topic: {topic}"));
+                }
+            }
+            Unsubscribed { peer_id, topic } => {
+                let removed = with_connected_peers(&topic, |connected_peers| {
+                    connected_peers.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
+                });
+                if removed {
+                    self.logger
+                        .trace(format!("{peer_id} unsubscribed from {topic}",));
+                } else {
+                    self.logger.error(format!(
+                        "{peer_id} unsubscribed from unknown topic: {topic}"
+                    ));
+                }
+            }
+            GossipsubNotSupported { peer_id } => {
+                self.logger
+                    .trace(format!("{peer_id} does not support gossipsub!"));
+            }
+        }
+    }
+
+    #[tracing::instrument(
+        skip(self, message),
+        fields(
+            %_message_id,
+            %_propagation_source,
+            source = ?message.source
+        )
+    )]
+    async fn handle_gossip_message(
+        &mut self,
+        _propagation_source: PeerId,
+        _message_id: gossipsub::MessageId,
+        message: gossipsub::Message,
+    ) {
+        let Some(origin) = message.source else {
+            self.logger
+                .error("Got message from unknown peer".to_string());
+            return;
+        };
+        self.logger
+            .debug(format!("Got message from peer: {origin}",));
+        match bincode2::deserialize::<GossipMessage>(&message.data) {
+            Ok(GossipMessage { topic, raw_payload }) => {
+                if let Some((_, tx, _)) = self
+                    .inbound_mapping
+                    .iter()
+                    .find(|r| r.0.to_string() == topic)
+                {
+                    if let Err(e) = tx.send(raw_payload) {
+                        self.logger
+                            .error(format!("Failed to send message to worker: {e}"));
+                    }
+                } else {
+                    self.logger
+                        .error(format!("No registered worker for topic: {topic}!"));
+                }
+            }
+            Err(e) => {
+                self.logger
+                    .error(format!("Failed to deserialize message: {e}"));
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_mdns_event(&mut self, event: mdns::Event) {
+        use mdns::Event::*;
+        match event {
+            Discovered(list) => {
+                for (peer_id, multiaddr) in list {
+                    self.logger
+                        .debug(format!("discovered a new peer: {peer_id} on {multiaddr}"));
+                    self.swarm
+                        .behaviour_mut()
+                        .gossipsub
+                        .add_explicit_peer(&peer_id);
+                    if let Err(err) = self.swarm.dial(multiaddr) {
+                        self.logger.error(format!("Failed to dial peer: {err}"));
+                    }
+                }
+            }
+            Expired(list) => {
+                for (peer_id, multiaddr) in list {
+                    self.logger.debug(format!(
+                        "discover peer has expired: {peer_id} with {multiaddr}"
+                    ));
+                    self.swarm
+                        .behaviour_mut()
+                        .gossipsub
+                        .remove_explicit_peer(&peer_id);
+                }
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_identify_event(&mut self, event: libp2p::identify::Event) {
+        use libp2p::identify::Event::*;
+        match event {
+            Received { peer_id, info } => {
+                // TODO: Verify the peer info, for example the protocol version, agent version, etc.
+                let info_lines = vec![
+                    format!("Protocol Version: {}", info.protocol_version),
+                    format!("Agent Version: {}", info.agent_version),
+                    format!("Supported Protocols: {:?}", info.protocols),
+                ];
+                let info_lines = info_lines.join(", ");
+                self.logger.debug(format!(
+                    "Received identify event from peer: {peer_id} with info: {info_lines}"
+                ));
+                self.swarm.add_external_address(info.observed_addr);
+            }
+            Sent { peer_id } => {
+                self.logger
+                    .trace(format!("Sent identify event to peer: {peer_id}"));
+            }
+            Pushed { peer_id, info } => {
+                let info_lines = vec![
+                    format!("Protocol Version: {}", info.protocol_version),
+                    format!("Agent Version: {}", info.agent_version),
+                    format!("Supported Protocols: {:?}", info.protocols),
+                ];
+                let info_lines = info_lines.join(", ");
+                self.logger.debug(format!(
+                    "Pushed identify event to peer: {peer_id} with info: {info_lines}"
+                ));
+            }
+            Error { peer_id, error } => {
+                self.logger.error(format!(
+                    "Identify error from peer: {peer_id} with error: {error}"
+                ));
+            }
+        }
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_kadmelia_event(&mut self, event: libp2p::kad::Event) {
+        // TODO: Handle kadmelia events
+        self.logger.trace(format!("Kadmelia event: {event:?}"));
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn handle_connection_established(&mut self, peer_id: PeerId, _num_established: u32) {
+        self.logger.debug("Connection established");
+        // TODO: Sign the handshake message.
+        let handshake = MyBehaviourRequest::Handshake {
+            ecdsa_public_key: *self.role_key,
+        };
+        self.swarm
+            .behaviour_mut()
+            .p2p
+            .send_request(&peer_id, handshake);
+        self.swarm
+            .behaviour_mut()
+            .gossipsub
+            .add_explicit_peer(&peer_id);
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn handle_connection_closed(
+        &mut self,
+        peer_id: PeerId,
+        _num_established: u32,
+        _cause: Option<libp2p::swarm::ConnectionError>,
+    ) {
+        self.logger.debug("Connection closed");
+        self.swarm
+            .behaviour_mut()
+            .gossipsub
+            .remove_explicit_peer(&peer_id);
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn handle_incoming_connection(
+        &mut self,
+        _connection_id: libp2p::swarm::ConnectionId,
+        _local_addr: libp2p::Multiaddr,
+        _send_back_addr: libp2p::Multiaddr,
+    ) {
+        self.logger.debug("Incoming connection");
+    }
+
+    #[tracing::instrument(skip(self))]
+    async fn handle_outgoing_connection(
+        &mut self,
+        peer_id: PeerId,
+        _connection_id: libp2p::swarm::ConnectionId,
+    ) {
+        self.logger
+            .debug(format!("Outgoing connection to peer: {peer_id}",));
+    }
+
+    #[tracing::instrument(skip(self, error))]
+    async fn handle_incoming_connection_error(
+        &mut self,
+        _connection_id: libp2p::swarm::ConnectionId,
+        _local_addr: libp2p::Multiaddr,
+        _send_back_addr: libp2p::Multiaddr,
+        error: libp2p::swarm::ListenError,
+    ) {
+        self.logger
+            .error(format!("Incoming connection error: {error}",));
+    }
+
+    #[tracing::instrument(skip(self, error))]
+    async fn handle_outgoing_connection_error(
+        &mut self,
+        _connection_id: libp2p::swarm::ConnectionId,
+        _peer_id: Option<PeerId>,
+        error: libp2p::swarm::DialError,
+    ) {
+        self.logger
+            .error(format!("Outgoing connection error: {error}",));
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_dcutr_event(&mut self, event: libp2p::dcutr::Event) {
+        self.logger.debug(format!("DCUTR event: {event:?}"));
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_relay_event(&mut self, event: libp2p::relay::Event) {
+        self.logger.debug(format!("Relay event: {event:?}"));
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_relay_client_event(&mut self, event: libp2p::relay::client::Event) {
+        self.logger.debug(format!("Relay client event: {event:?}"));
+    }
+
+    #[tracing::instrument(skip(self, event))]
+    async fn handle_ping_event(&mut self, event: libp2p::ping::Event) {
+        self.logger.debug(format!("Ping event: {event:?}"));
+    }
+
+    async fn handle_swarm_event(&mut self, event: SwarmEvent<MyBehaviourEvent>) {
+        use MyBehaviourEvent::*;
+        use SwarmEvent::*;
+        let _enter = self.span.enter();
+        match event {
+            Behaviour(P2p(event)) => {
+                self.handle_p2p(event).await;
+            }
+            Behaviour(Gossipsub(event)) => {
+                self.handle_gossip(event).await;
+            }
+            Behaviour(Mdns(event)) => {
+                self.handle_mdns_event(event).await;
+            }
+            Behaviour(Identify(event)) => {
+                self.handle_identify_event(event).await;
+            }
+            Behaviour(Kadmelia(event)) => {
+                self.logger.trace(format!("Kadmelia event: {event:?}"));
+            }
+            Behaviour(Dcutr(event)) => {
+                self.handle_dcutr_event(event).await;
+            }
+            Behaviour(Relay(event)) => {
+                self.handle_relay_event(event).await;
+            }
+            Behaviour(RelayClient(event)) => {
+                self.handle_relay_client_event(event).await;
+            }
+            Behaviour(Ping(event)) => {
+                self.handle_ping_event(event).await;
+            }
+
+            NewListenAddr {
+                address,
+                listener_id,
+            } => {
+                self.logger
+                    .debug(format!("{listener_id} has a new address: {address}"));
+            }
+            ConnectionEstablished {
+                peer_id,
+                num_established,
+                ..
+            } => {
+                self.handle_connection_established(peer_id, num_established.get())
+                    .await;
+            }
+            ConnectionClosed {
+                peer_id,
+                num_established,
+                cause,
+                ..
+            } => {
+                self.handle_connection_closed(peer_id, num_established, cause)
+                    .await;
+            }
+            IncomingConnection {
+                connection_id,
+                local_addr,
+                send_back_addr,
+            } => {
+                self.handle_incoming_connection(connection_id, local_addr, send_back_addr)
+                    .await;
+            }
+            IncomingConnectionError {
+                connection_id,
+                local_addr,
+                send_back_addr,
+                error,
+            } => {
+                self.handle_incoming_connection_error(
+                    connection_id,
+                    local_addr,
+                    send_back_addr,
+                    error,
+                )
+                .await;
+            }
+            OutgoingConnectionError {
+                connection_id,
+                peer_id,
+                error,
+            } => {
+                self.handle_outgoing_connection_error(connection_id, peer_id, error)
+                    .await;
+            }
+            ExpiredListenAddr {
+                listener_id,
+                address,
+            } => {
+                self.logger
+                    .trace(format!("{listener_id} has an expired address: {address}"));
+            }
+            ListenerClosed {
+                listener_id,
+                addresses,
+                reason,
+            } => {
+                self.logger.trace(format!(
+                    "{listener_id} on {addresses:?} has been closed: {reason:?}"
+                ));
+            }
+            ListenerError { listener_id, error } => {
+                self.logger
+                    .error(format!("{listener_id} has an error: {error}"));
+            }
+            Dialing {
+                peer_id,
+                connection_id,
+            } => {
+                self.logger.debug(format!(
+                    "Dialing peer: {peer_id:?} with connection_id: {connection_id}"
+                ));
+            }
+            NewExternalAddrCandidate { address } => {
+                self.logger
+                    .trace(format!("New external address candidate: {address}"));
+            }
+            ExternalAddrConfirmed { address } => {
+                self.logger
+                    .trace(format!("External address confirmed: {address}"));
+            }
+            ExternalAddrExpired { address } => {
+                self.logger
+                    .trace(format!("External address expired: {address}"));
+            }
+            NewExternalAddrOfPeer { peer_id, address } => {
+                self.logger.trace(format!(
+                    "New external address of peer: {peer_id} with address: {address}"
+                ));
+            }
+            unknown => {
+                self.logger
+                    .warn(format!("Unknown swarm event: {unknown:?}"));
+            }
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct GossipHandle {
+    topic: IdentTopic,
+    tx_to_outbound: UnboundedSender<IntraNodePayload>,
+    rx_from_inbound: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>>>,
+    logger: DebugLogger,
+    connected_peers: Arc<AtomicU32>,
+    ecdsa_peer_id_to_libp2p_id: Arc<RwLock<HashMap<ecdsa::Public, PeerId>>>,
+}
+
+impl GossipHandle {
+    pub fn connected_peers(&self) -> usize {
+        self.connected_peers
+            .load(std::sync::atomic::Ordering::Relaxed) as usize
+    }
+
+    pub fn topic(&self) -> IdentTopic {
+        self.topic.clone()
+    }
+}
+
+pub struct IntraNodePayload {
+    topic: IdentTopic,
+    payload: GossipOrRequestResponse,
+    message_type: MessageType,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum GossipOrRequestResponse {
+    Gossip(GossipMessage),
+    Request(MyBehaviourRequest),
+    Response(MyBehaviourResponse),
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct GossipMessage {
+    topic: String,
+    raw_payload: Vec<u8>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum MyBehaviourRequest {
+    Handshake { ecdsa_public_key: ecdsa::Public },
+    Message { topic: String, raw_payload: Vec<u8> },
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum MyBehaviourResponse {
+    Handshaked { ecdsa_public_key: ecdsa::Public },
+    MessageHandled,
+}
+
+enum MessageType {
+    Broadcast,
+    P2P(PeerId),
+}
+
+#[async_trait]
+impl Network for GossipHandle {
+    async fn next_message(&self) -> Option<<WorkManager as WorkManagerInterface>::ProtocolMessage> {
+        let mut lock = self
+            .rx_from_inbound
+            .try_lock()
+            .expect("There should be only a single caller for `next_message`");
+
+        let message = lock.recv().await?;
+        match bincode2::deserialize(&message) {
+            Ok(message) => Some(message),
+            Err(e) => {
+                self.logger
+                    .error(format!("Failed to deserialize message: {e}"));
+                drop(lock);
+                self.next_message().await
+            }
+        }
+    }
+
+    async fn send_message(
+        &self,
+        message: <WorkManager as WorkManagerInterface>::ProtocolMessage,
+    ) -> Result<(), gadget_common::Error> {
+        let message_type = if let Some(to) = message.to_network_id {
+            let libp2p_id = self
+                .ecdsa_peer_id_to_libp2p_id
+                .read()
+                .await
+                .get(&to)
+                .cloned()
+                .ok_or_else(|| gadget_common::Error::NetworkError {
+                    err: format!(
+                        "No libp2p ID found for ecdsa public key: {to}. No handshake happened?"
+                    ),
+                })?;
+
+            MessageType::P2P(libp2p_id)
+        } else {
+            MessageType::Broadcast
+        };
+
+        let payload_inner = match message_type {
+            MessageType::Broadcast => GossipOrRequestResponse::Gossip(GossipMessage {
+                topic: self.topic.to_string(),
+                raw_payload: bincode2::serialize(&message).expect("Should serialize"),
+            }),
+            MessageType::P2P(_) => GossipOrRequestResponse::Request(MyBehaviourRequest::Message {
+                topic: self.topic.to_string(),
+                raw_payload: bincode2::serialize(&message).expect("Should serialize"),
+            }),
+        };
+
+        let payload = IntraNodePayload {
+            topic: self.topic.clone(),
+            payload: payload_inner,
+            message_type,
+        };
+
+        self.tx_to_outbound
+            .send(payload)
+            .map_err(|e| gadget_common::Error::NetworkError {
+                err: format!("Failed to send intra-node payload: {e}"),
+            })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::config::{KeystoreConfig, ShellConfig, SubxtConfig};
+    use crate::network::setup_network;
+    use crate::shell::wait_for_connection_to_bootnodes;
+    use gadget_common::prelude::{DebugLogger, GadgetProtocolMessage, Network, WorkManager};
+    use gadget_core::job_manager::WorkManagerInterface;
+    use sp_core::ecdsa;
+
+    #[tokio::test]
+    async fn test_gossip_network() {
+        color_eyre::install().unwrap();
+        test_utils::setup_log();
+        const N_PEERS: usize = 3;
+        let networks = vec!["/test-network"];
+        let mut all_handles = Vec::new();
+        for x in 0..N_PEERS {
+            let identity = libp2p::identity::Keypair::generate_ed25519();
+
+            let logger = DebugLogger {
+                peer_id: identity.public().to_peer_id().to_string(),
+            };
+
+            let (bind_port, bootnodes) = if x == 0 {
+                (
+                    30555,
+                    vec![
+                        "/ip4/0.0.0.0/tcp/30556".parse().unwrap(),
+                        "/ip4/0.0.0.0/tcp/30557".parse().unwrap(),
+                    ],
+                )
+            } else if x == 1 {
+                (
+                    30556,
+                    vec![
+                        "/ip4/0.0.0.0/tcp/30555".parse().unwrap(),
+                        "/ip4/0.0.0.0/tcp/30557".parse().unwrap(),
+                    ],
+                )
+            } else if x == 2 {
+                (
+                    30557,
+                    vec![
+                        "/ip4/0.0.0.0/tcp/30555".parse().unwrap(),
+                        "/ip4/0.0.0.0/tcp/30556".parse().unwrap(),
+                    ],
+                )
+            } else {
+                panic!("Invalid peer index");
+            };
+
+            let shell_config = ShellConfig {
+                keystore: KeystoreConfig::InMemory,
+                subxt: SubxtConfig {
+                    endpoint: url::Url::from_directory_path("/").unwrap(),
+                },
+                bind_ip: "0.0.0.0".to_string(),
+                bind_port,
+                bootnodes,
+                node_key: [0u8; 32],
+            };
+
+            let role_key = get_dummy_role_key_from_index(x);
+
+            let (handles, _) = setup_network(
+                identity,
+                &shell_config,
+                logger.clone(),
+                networks.clone(),
+                role_key,
+            )
+            .await
+            .unwrap();
+            all_handles.push((handles, shell_config, logger));
+        }
+
+        for (handles, shell_config, logger) in &all_handles {
+            wait_for_connection_to_bootnodes(shell_config, handles, logger)
+                .await
+                .unwrap();
+        }
+
+        /*
+           We must test the following:
+           * Broadcast send
+           * Broadcast receive
+           * P2P send
+           * P2P receive
+        */
+
+        // Now, send broadcast messages through each topic
+        for _network in &networks {
+            for (handles, _, _) in &all_handles {
+                for handle in handles {
+                    handle
+                        .send_message(dummy_message_broadcast(b"Hello, world".to_vec()))
+                        .await
+                        .unwrap();
+                }
+            }
+        }
+
+        // Next, receive these broadcasted messages
+        for _network in &networks {
+            for (handles, _, logger) in &all_handles {
+                logger.debug("Waiting to receive broadcast messages ...");
+                for handle in handles {
+                    let message = handle.next_message().await.unwrap();
+                    assert_eq!(message.payload, b"Hello, world");
+                    assert_eq!(message.to_network_id, None);
+                }
+            }
+        }
+
+        let send_idxs = [0, 1, 2];
+        // Next, send P2P messages: everybody sends a message to everybody
+        for _network in networks.iter() {
+            for (handles, _, _) in all_handles.iter() {
+                for (my_idx, handle) in handles.iter().enumerate() {
+                    let send_idxs = send_idxs
+                        .iter()
+                        .filter(|&&idx| idx != my_idx)
+                        .cloned()
+                        .collect::<Vec<_>>();
+                    for i in send_idxs {
+                        handle
+                            .send_message(dummy_message_p2p(b"Hello, world".to_vec(), i))
+                            .await
+                            .unwrap();
+                    }
+                }
+            }
+        }
+
+        // Finally, receive P2P messages: everybody should receive a message from everybody else
+        for _network in networks.iter() {
+            for (handles, _, logger) in all_handles.iter() {
+                logger.debug("Waiting to receive P2P messages ...");
+                for (my_idx, handle) in handles.iter().enumerate() {
+                    // Each party should receive two messages
+                    for _ in 0..2 {
+                        let message = handle.next_message().await.unwrap();
+                        assert_eq!(message.payload, b"Hello, world");
+                        assert_eq!(
+                            message.to_network_id,
+                            Some(get_dummy_role_key_from_index(my_idx))
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    fn dummy_message_broadcast(
+        input: Vec<u8>,
+    ) -> <WorkManager as WorkManagerInterface>::ProtocolMessage {
+        dummy_message_inner(input, None)
+    }
+
+    fn dummy_message_p2p(
+        input: Vec<u8>,
+        to_idx: usize,
+    ) -> <WorkManager as WorkManagerInterface>::ProtocolMessage {
+        let dummy_role_key = get_dummy_role_key_from_index(to_idx);
+        dummy_message_inner(input, Some(dummy_role_key))
+    }
+
+    fn dummy_message_inner(
+        input: Vec<u8>,
+        to_network_id: Option<ecdsa::Public>,
+    ) -> <WorkManager as WorkManagerInterface>::ProtocolMessage {
+        GadgetProtocolMessage {
+            associated_block_id: 0,
+            associated_session_id: 0,
+            associated_retry_id: 0,
+            task_hash: [0u8; 32],
+            from: 0,
+            to: None,
+            payload: input,
+            from_network_id: None,
+            to_network_id,
+        }
+    }
+
+    fn get_dummy_role_key_from_index(index: usize) -> ecdsa::Public {
+        ecdsa::Public::from_raw([index as u8; 33])
+    }
+}
diff --git a/shell/src/protocols/dfns.rs b/shell/src/protocols/dfns.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/shell/src/protocols/dfns.rs
@@ -0,0 +1,1 @@
+
diff --git a/shell/src/protocols/mod.rs b/shell/src/protocols/mod.rs
new file mode 100644
index 000000000..1a581ac3f
--- /dev/null
+++ b/shell/src/protocols/mod.rs
@@ -0,0 +1,1 @@
+pub mod dfns;
diff --git a/shell/src/shell.rs b/shell/src/shell.rs
new file mode 100644
index 000000000..c08676fd9
--- /dev/null
+++ b/shell/src/shell.rs
@@ -0,0 +1,208 @@
+use std::sync::Arc;
+use std::time::Duration;
+
+use crate::{
+    keystore::KeystoreContainer,
+    tangle::{TangleConfig, TangleRuntime},
+};
+use color_eyre::eyre::OptionExt;
+use gadget_common::{
+    client::{PairSigner, SubxtPalletSubmitter},
+    config::{ClientWithApi, DebugLogger, PrometheusConfig},
+    full_protocol::NodeInput,
+    keystore::{ECDSAKeyStore, InMemoryBackend},
+};
+use sp_core::{ecdsa, ed25519, sr25519, ByteArray, Pair};
+use sp_keystore::Keystore;
+
+use crate::config::ShellConfig;
+use crate::network::GossipHandle;
+use crate::tangle::crypto;
+use dfns_cggmp21_protocol::constants::{
+    DFNS_CGGMP21_KEYGEN_PROTOCOL_NAME, DFNS_CGGMP21_KEYREFRESH_PROTOCOL_NAME,
+    DFNS_CGGMP21_KEYROTATE_PROTOCOL_NAME, DFNS_CGGMP21_SIGNING_PROTOCOL_NAME,
+};
+use gadget_common::keystore::KeystoreBackend;
+use tangle_subxt::subxt;
+
+/// The version of the shell
+pub const AGENT_VERSION: &str = "tangle/gadget-shell/1.0.0";
+pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+/// Start the shell and run it forever
+#[tracing::instrument(skip(config))]
+pub async fn run_forever(config: ShellConfig) -> color_eyre::Result<()> {
+    let (role_key, acco_key) = load_keys_from_keystore(&config.keystore)?;
+    let network_key = ed25519::Pair::from_seed(&config.node_key);
+    let subxt_client =
+        subxt::OnlineClient::<subxt::PolkadotConfig>::from_url(&config.subxt.endpoint).await?;
+    let logger = DebugLogger::default();
+    let pair_signer = PairSigner::new(acco_key.clone());
+    let pallet_tx_submitter =
+        SubxtPalletSubmitter::with_client(subxt_client.clone(), pair_signer, logger.clone());
+    let wrapped_keystore = ECDSAKeyStore::new(InMemoryBackend::new(), role_key.clone());
+
+    let libp2p_key = libp2p::identity::Keypair::ed25519_from_bytes(network_key.to_raw_vec())
+        .map_err(|e| color_eyre::eyre::eyre!("Failed to create libp2p keypair: {e}"))?;
+
+    let (networks, network_task) = crate::network::setup_network(
+        libp2p_key,
+        &config,
+        logger.clone(),
+        vec![
+            DFNS_CGGMP21_KEYGEN_PROTOCOL_NAME,
+            DFNS_CGGMP21_SIGNING_PROTOCOL_NAME,
+            DFNS_CGGMP21_KEYREFRESH_PROTOCOL_NAME,
+            DFNS_CGGMP21_KEYROTATE_PROTOCOL_NAME,
+        ],
+        role_key.public(),
+    )
+    .await
+    .map_err(|e| color_eyre::eyre::eyre!("Failed to setup network: {e}"))?;
+
+    logger.debug("Successfully initialized network, now waiting for bootnodes to connect ...");
+    wait_for_connection_to_bootnodes(&config, &networks, &logger).await?;
+
+    let pallet_tx = Arc::new(pallet_tx_submitter);
+
+    let dfns_cggmp21_protocol = start_protocol(
+        vec![
+            TangleRuntime::new(subxt_client.clone()),
+            TangleRuntime::new(subxt_client.clone()),
+            TangleRuntime::new(subxt_client.clone()),
+            TangleRuntime::new(subxt_client.clone()),
+        ],
+        networks,
+        acco_key.public(),
+        logger,
+        pallet_tx,
+        wrapped_keystore,
+    );
+
+    let ctrl_c = tokio::signal::ctrl_c();
+
+    tokio::select! {
+        _ = ctrl_c => {
+            tracing::info!("Received Ctrl-C, shutting down");
+        }
+        dfns = dfns_cggmp21_protocol => {
+            if let Err(e) = dfns {
+                tracing::error!(?e, "DFNS-CGGMP21 protocol failed");
+            }
+        }
+
+        _ = network_task => {
+            tracing::error!("Network task unexpectedly shutdown")
+        }
+    }
+
+    Ok(())
+}
+
+pub async fn start_protocol<C, KBE>(
+    clients: Vec<C>,
+    networks: Vec<GossipHandle>,
+    account_id: sr25519::Public,
+    logger: DebugLogger,
+    pallet_tx: Arc<SubxtPalletSubmitter<TangleConfig, PairSigner<TangleConfig>>>,
+    keystore: ECDSAKeyStore<KBE>,
+) -> color_eyre::Result<()>
+where
+    C: ClientWithApi + 'static,
+    KBE: KeystoreBackend,
+{
+    let node_input = NodeInput {
+        clients,
+        account_id,
+        logger,
+        pallet_tx,
+        keystore,
+        node_index: 0,
+        additional_params: (),
+        prometheus_config: PrometheusConfig::Disabled,
+        networks,
+    };
+
+    // Wait for the protocol to finish
+    dfns_cggmp21_protocol::setup_node(node_input).await;
+
+    Err(color_eyre::eyre::eyre!("Protocol finished unexpectedly"))
+}
+
+pub fn load_keys_from_keystore(
+    keystore_config: &crate::config::KeystoreConfig,
+) -> color_eyre::Result<(ecdsa::Pair, sr25519::Pair)> {
+    let keystore_container = KeystoreContainer::new(keystore_config)?;
+    let keystore = keystore_container.local_keystore();
+    tracing::debug!("Loaded keystore from path");
+    let ecdsa_keys = keystore.ecdsa_public_keys(crypto::role::KEY_TYPE);
+    let sr25519_keys = keystore.sr25519_public_keys(crypto::acco::KEY_TYPE);
+
+    if ecdsa_keys.len() != 1 {
+        color_eyre::eyre::bail!(
+            "`role`: Expected exactly one key in ECDSA keystore, found {}",
+            ecdsa_keys.len()
+        );
+    }
+
+    if sr25519_keys.len() != 1 {
+        color_eyre::eyre::bail!(
+            "`acco`: Expected exactly one key in SR25519 keystore, found {}",
+            sr25519_keys.len()
+        );
+    }
+
+    let role_public_key = crypto::role::Public::from_slice(&ecdsa_keys[0].0)
+        .map_err(|_| color_eyre::eyre::eyre!("Failed to parse public key from keystore"))?;
+    let account_public_key = crypto::acco::Public::from_slice(&sr25519_keys[0].0)
+        .map_err(|_| color_eyre::eyre::eyre!("Failed to parse public key from keystore"))?;
+    let role_key = keystore
+        .key_pair::<crypto::role::Pair>(&role_public_key)?
+        .ok_or_eyre("Failed to load key `role` from keystore")?
+        .into_inner();
+    let acco_key = keystore
+        .key_pair::<crypto::acco::Pair>(&account_public_key)?
+        .ok_or_eyre("Failed to load key `acco` from keystore")?
+        .into_inner();
+
+    tracing::debug!(%role_public_key, "Loaded key from keystore");
+    tracing::debug!(%account_public_key, "Loaded key from keystore");
+    Ok((role_key, acco_key))
+}
+
+pub async fn wait_for_connection_to_bootnodes(
+    config: &ShellConfig,
+    handles: &[GossipHandle],
+    logger: &DebugLogger,
+) -> color_eyre::Result<()> {
+    let n_required = config.bootnodes.len();
+    let n_networks = handles.len();
+    logger.debug(format!(
+        "Waiting for {n_required} peers to show up across {n_networks} networks"
+    ));
+
+    let mut tasks = tokio::task::JoinSet::new();
+
+    // For each network, we start a task that checks if we have enough peers connected
+    // and then we wait for all of them to finish.
+
+    let wait_for_peers = |handle: GossipHandle, n_required, logger: DebugLogger| async move {
+        'inner: loop {
+            let n_connected = handle.connected_peers();
+            if n_connected >= n_required {
+                break 'inner;
+            }
+            let topic = handle.topic();
+            logger.debug(format!("`{topic}`: We currently have {n_connected}/{n_required} peers connected to network"));
+            tokio::time::sleep(Duration::from_millis(1000)).await;
+        }
+    };
+
+    for handle in handles.iter() {
+        tasks.spawn(wait_for_peers(handle.clone(), n_required, logger.clone()));
+    }
+    // Wait for all tasks to finish
+    while (tasks.join_next().await).is_some() {}
+
+    Ok(())
+}
diff --git a/shell/src/tangle.rs b/shell/src/tangle.rs
new file mode 100644
index 000000000..6380ee620
--- /dev/null
+++ b/shell/src/tangle.rs
@@ -0,0 +1,250 @@
+use std::sync::Arc;
+use std::time::Duration;
+
+use color_eyre::Result;
+use gadget_common::config::ClientWithApi;
+use gadget_common::locks::TokioMutexExt;
+use gadget_core::gadget::substrate::{Client, FinalityNotification};
+use tangle_subxt::subxt::blocks::{Block, BlockRef};
+use tangle_subxt::subxt::ext::futures::TryFutureExt;
+use tangle_subxt::subxt::utils::AccountId32;
+use tangle_subxt::subxt::{self, PolkadotConfig};
+use tangle_subxt::tangle_runtime::api;
+use tangle_subxt::tangle_runtime::api::runtime_types::tangle_testnet_runtime::MaxAdditionalParamsLen;
+use tangle_subxt::tangle_runtime::api::runtime_types::{
+    tangle_primitives::jobs::{PhaseResult, RpcResponseJobsData},
+    tangle_primitives::roles::RoleType,
+    tangle_testnet_runtime::{
+        MaxDataLen, MaxKeyLen, MaxParticipants, MaxProofLen, MaxSignatureLen, MaxSubmissionLen,
+    },
+};
+
+pub type TangleConfig = subxt::PolkadotConfig;
+type TangleClient = subxt::OnlineClient<TangleConfig>;
+type TangleBlock = Block<TangleConfig, TangleClient>;
+type TangleBlockStream = subxt::backend::StreamOfResults<TangleBlock>;
+
+pub mod crypto {
+    use sp_application_crypto::{app_crypto, ecdsa, sr25519};
+    pub mod acco {
+        use super::*;
+        pub use sp_core::crypto::key_types::ACCOUNT as KEY_TYPE;
+        app_crypto!(sr25519, KEY_TYPE);
+    }
+
+    pub mod role {
+        use super::*;
+        /// Key type for ROLE keys
+        pub const KEY_TYPE: sp_application_crypto::KeyTypeId =
+            sp_application_crypto::KeyTypeId(*b"role");
+
+        app_crypto!(ecdsa, KEY_TYPE);
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct TangleRuntime {
+    client: subxt::OnlineClient<PolkadotConfig>,
+    finality_notification_stream: Arc<tokio::sync::Mutex<Option<TangleBlockStream>>>,
+    latest_finality_notification: Arc<tokio::sync::Mutex<Option<FinalityNotification>>>,
+}
+
+impl TangleRuntime {
+    /// Create a new TangleRuntime instance.
+    pub fn new(client: subxt::OnlineClient<PolkadotConfig>) -> Self {
+        Self {
+            client,
+            finality_notification_stream: Arc::new(tokio::sync::Mutex::new(None)),
+            latest_finality_notification: Arc::new(tokio::sync::Mutex::new(None)),
+        }
+    }
+
+    /// Initialize the TangleRuntime instance by listening for finality notifications.
+    /// This method must be called before using the instance.
+    async fn initialize(&self) -> Result<()> {
+        let finality_notification_stream = self.client.blocks().subscribe_finalized().await?;
+        *self.finality_notification_stream.lock().await = Some(finality_notification_stream);
+        Ok(())
+    }
+
+    fn runtime_api(
+        &self,
+        at: [u8; 32],
+    ) -> subxt::runtime_api::RuntimeApi<TangleConfig, TangleClient> {
+        let block_ref = BlockRef::from_hash(sp_core::hash::H256::from_slice(&at));
+        self.client.runtime_api().at(block_ref)
+    }
+}
+
+#[async_trait::async_trait]
+impl Client for TangleRuntime {
+    async fn get_next_finality_notification(&self) -> Option<FinalityNotification> {
+        let mut lock = self
+            .finality_notification_stream
+            .try_lock_timeout(Duration::from_millis(500))
+            .await
+            .ok()?;
+        match lock.as_mut() {
+            Some(stream) => {
+                let block = stream.next().await?.ok()?;
+                let notification = FinalityNotification {
+                    number: block.number().into(),
+                    hash: block.hash().into(),
+                };
+                let mut lock2 = self
+                    .latest_finality_notification
+                    .lock_timeout(Duration::from_millis(500))
+                    .await;
+                *lock2 = Some(notification.clone());
+                Some(notification)
+            }
+            None => {
+                drop(lock);
+                tracing::debug!("Finality notification stream is not initialized. Initializing...");
+                self.initialize().await.ok()?;
+                // Next time, the stream should be initialized.
+                self.get_next_finality_notification().await
+            }
+        }
+    }
+
+    async fn get_latest_finality_notification(&self) -> Option<FinalityNotification> {
+        let lock = self
+            .latest_finality_notification
+            .try_lock_timeout(Duration::from_millis(500))
+            .await
+            .ok()?;
+        match &*lock {
+            Some(notification) => Some(notification.clone()),
+            None => {
+                drop(lock);
+                tracing::debug!("Latest finality notification is not available. Fetching...");
+                self.get_next_finality_notification().await
+            }
+        }
+    }
+}
+
+#[async_trait::async_trait]
+impl ClientWithApi for TangleRuntime {
+    async fn query_jobs_by_validator(
+        &self,
+        at: [u8; 32],
+        validator: AccountId32,
+    ) -> core::result::Result<
+        Option<
+            Vec<
+                RpcResponseJobsData<
+                    AccountId32,
+                    u64,
+                    MaxParticipants,
+                    MaxSubmissionLen,
+                    MaxAdditionalParamsLen,
+                >,
+            >,
+        >,
+        gadget_common::Error,
+    > {
+        let q = api::apis().jobs_api().query_jobs_by_validator(validator);
+        self.runtime_api(at)
+            .call(q)
+            .map_err(|e| gadget_common::Error::ClientError { err: e.to_string() })
+            .await
+    }
+
+    async fn query_job_by_id(
+        &self,
+        at: [u8; 32],
+        role_type: RoleType,
+        job_id: u64,
+    ) -> core::result::Result<
+        Option<
+            RpcResponseJobsData<
+                AccountId32,
+                u64,
+                MaxParticipants,
+                MaxSubmissionLen,
+                MaxAdditionalParamsLen,
+            >,
+        >,
+        gadget_common::Error,
+    > {
+        let q = api::apis().jobs_api().query_job_by_id(role_type, job_id);
+        self.runtime_api(at)
+            .call(q)
+            .map_err(|e| gadget_common::Error::ClientError { err: e.to_string() })
+            .await
+    }
+
+    async fn query_job_result(
+        &self,
+        at: [u8; 32],
+        role_type: RoleType,
+        job_id: u64,
+    ) -> core::result::Result<
+        Option<
+            PhaseResult<
+                AccountId32,
+                u64,
+                MaxParticipants,
+                MaxKeyLen,
+                MaxDataLen,
+                MaxSignatureLen,
+                MaxSubmissionLen,
+                MaxProofLen,
+                MaxAdditionalParamsLen,
+            >,
+        >,
+        gadget_common::Error,
+    > {
+        let q = api::apis().jobs_api().query_job_result(role_type, job_id);
+        self.runtime_api(at)
+            .call(q)
+            .map_err(|e| gadget_common::Error::ClientError { err: e.to_string() })
+            .await
+    }
+
+    async fn query_next_job_id(
+        &self,
+        at: [u8; 32],
+    ) -> core::result::Result<u64, gadget_common::Error> {
+        let q = api::apis().jobs_api().query_next_job_id();
+        self.runtime_api(at)
+            .call(q)
+            .map_err(|e| gadget_common::Error::ClientError { err: e.to_string() })
+            .await
+    }
+
+    async fn query_restaker_role_key(
+        &self,
+        at: [u8; 32],
+        address: AccountId32,
+    ) -> core::result::Result<Option<Vec<u8>>, gadget_common::Error> {
+        let q = api::apis().jobs_api().query_restaker_role_key(address);
+        self.runtime_api(at)
+            .call(q)
+            .map_err(|e| gadget_common::Error::ClientError { err: e.to_string() })
+            .await
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use color_eyre::{eyre::OptionExt, Result};
+
+    #[ignore = "requires a running node"]
+    #[tokio::test]
+    async fn client() -> Result<()> {
+        let subxt_client = subxt::OnlineClient::new().await?;
+        let runtime = TangleRuntime::new(subxt_client);
+
+        let notification = runtime
+            .get_next_finality_notification()
+            .await
+            .ok_or_eyre("Finality notification not found")?;
+        let job_id = runtime.query_next_job_id(notification.hash).await?;
+        eprintln!("Next job id: {}", job_id);
+        Ok(())
+    }
+}


Test patch:
diff --git a/libp2p/src/protocol/notifications/service/tests.rs b/libp2p/src/protocol/notifications/service/tests.rs
new file mode 100644
index 000000000..011a238a6
--- /dev/null
+++ b/libp2p/src/protocol/notifications/service/tests.rs
@@ -0,0 +1,954 @@
+// 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 super::*;
+use crate::protocol::notifications::handler::{
+    NotificationsSinkMessage, ASYNC_NOTIFICATIONS_BUFFER_SIZE,
+};
+
+use std::future::Future;
+
+#[tokio::test]
+async fn validate_and_accept_substream() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (handle, _stream) = proto.split();
+
+    let peer_id = PeerId::random();
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+}
+
+#[tokio::test]
+async fn substream_opened() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, _, _) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+
+    let peer_id = PeerId::random();
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+}
+
+#[tokio::test]
+async fn send_sync_notification() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    notif.send_sync_notification(&peer_id, vec![1, 3, 3, 8]);
+    assert_eq!(
+        sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 8]
+        })
+    );
+}
+
+#[tokio::test]
+async fn send_async_notification() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    notif
+        .send_async_notification(&peer_id, vec![1, 3, 3, 9])
+        .await
+        .unwrap();
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 9]
+        })
+    );
+}
+
+#[tokio::test]
+async fn send_sync_notification_to_non_existent_peer() {
+    let (proto, notif) = notification_service("/proto/1".into());
+    let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
+    let (_handle, _stream) = proto.split();
+    let peer = PeerId::random();
+
+    // as per the original implementation, the call doesn't fail
+    notif.send_sync_notification(&peer, vec![1, 3, 3, 7])
+}
+
+#[tokio::test]
+async fn send_async_notification_to_non_existent_peer() {
+    let (proto, notif) = notification_service("/proto/1".into());
+    let (_sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
+    let (_handle, _stream) = proto.split();
+    let peer = PeerId::random();
+
+    if let Err(error::Error::PeerDoesntExist(peer_id)) =
+        notif.send_async_notification(&peer, vec![1, 3, 3, 7]).await
+    {
+        assert_eq!(peer, peer_id);
+    } else {
+        panic!("invalid error received from `send_async_notification()`");
+    }
+}
+
+#[tokio::test]
+async fn receive_notification() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, _, _sync_rx) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // notification is received
+    handle
+        .report_notification_received(peer_id, vec![1, 3, 3, 8])
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
+        notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(notification, vec![1, 3, 3, 8]);
+    } else {
+        panic!("invalid event received");
+    }
+}
+
+#[tokio::test]
+async fn backpressure_works() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, mut async_rx, _) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // fill the message buffer with messages
+    for i in 0..=ASYNC_NOTIFICATIONS_BUFFER_SIZE {
+        assert!(
+            futures::poll!(notif.send_async_notification(&peer_id, vec![1, 3, 3, i as u8]))
+                .is_ready()
+        );
+    }
+
+    // try to send one more message and verify that the call blocks
+    assert!(futures::poll!(notif.send_async_notification(&peer_id, vec![1, 3, 3, 9])).is_pending());
+
+    // release one slot from the buffer for new message
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 0]
+        })
+    );
+
+    // verify that a message can be sent
+    assert!(futures::poll!(notif.send_async_notification(&peer_id, vec![1, 3, 3, 9])).is_ready());
+}
+
+#[tokio::test]
+async fn peer_disconnects_then_sync_notification_is_sent() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, _, sync_rx) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // report that a substream has been closed but don't poll `notif` to receive this
+    // information
+    handle.report_substream_closed(peer_id).unwrap();
+    drop(sync_rx);
+
+    // as per documentation, error is not reported but the notification is silently dropped
+    notif.send_sync_notification(&peer_id, vec![1, 3, 3, 7]);
+}
+
+#[tokio::test]
+async fn peer_disconnects_then_async_notification_is_sent() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, async_rx, _) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // report that a substream has been closed but don't poll `notif` to receive this
+    // information
+    handle.report_substream_closed(peer_id).unwrap();
+    drop(async_rx);
+
+    // as per documentation, error is not reported but the notification is silently dropped
+    if let Err(error::Error::ConnectionClosed) = notif
+        .send_async_notification(&peer_id, vec![1, 3, 3, 7])
+        .await
+    {
+    } else {
+        panic!("invalid state after calling `send_async_notificatio()` on closed connection")
+    }
+}
+
+#[tokio::test]
+async fn cloned_service_opening_substream_works() {
+    let (proto, mut notif1) = notification_service("/proto/1".into());
+    let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
+    let (handle, _stream) = proto.split();
+    let mut notif2 = notif1.clone().unwrap();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(mut result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    // verify that `notif1` gets the event
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif1.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+
+    // verify that because only one listener has thus far send their result, the result is
+    // pending
+    assert!(result_rx.try_recv().is_err());
+
+    // verify that `notif2` also gets the event
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif2.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+}
+
+#[tokio::test]
+async fn cloned_service_one_service_rejects_substream() {
+    let (proto, notif1) = notification_service("/proto/1".into());
+    let mut notif1 = Box::new(notif1) as Box<dyn NotificationService>;
+    let (_sink, _async_rx, _) = NotificationsSink::new(PeerId::random());
+    let (handle, _stream) = proto.split();
+    let mut notif2 = notif1.clone().unwrap();
+    let mut notif3 = notif2.clone().unwrap();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(mut result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    for notif in [&mut notif1, &mut notif2] {
+        if let Some(NotificationEvent::ValidateInboundSubstream {
+            peer,
+            handshake,
+            result_tx,
+        }) = notif.next_event().await
+        {
+            assert_eq!(peer_id, peer);
+            assert_eq!(handshake, vec![1, 3, 3, 7]);
+            result_tx.send(ValidationResult::Accept).unwrap();
+        } else {
+            panic!("invalid event received");
+        }
+    }
+
+    // `notif3` has not yet sent their validation result
+    assert!(result_rx.try_recv().is_err());
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif3.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Reject).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Reject);
+}
+
+#[tokio::test]
+async fn cloned_service_opening_substream_sending_and_receiving_notifications_work() {
+    let (proto, notif1) = notification_service("/proto/1".into());
+    let (sink, _, mut sync_rx) = NotificationsSink::new(PeerId::random());
+    let mut notif1 = Box::new(notif1) as Box<dyn NotificationService>;
+    let (mut handle, _stream) = proto.split();
+    let mut notif2 = notif1.clone().unwrap();
+    let mut notif3 = notif1.clone().unwrap();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    for notif in [&mut notif1, &mut notif2, &mut notif3] {
+        // accept the inbound substream for all services
+        if let Some(NotificationEvent::ValidateInboundSubstream {
+            peer,
+            handshake,
+            result_tx,
+        }) = notif.next_event().await
+        {
+            assert_eq!(peer_id, peer);
+            assert_eq!(handshake, vec![1, 3, 3, 7]);
+            result_tx.send(ValidationResult::Accept).unwrap();
+        } else {
+            panic!("invalid event received");
+        }
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that then notification stream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    for notif in [&mut notif1, &mut notif2, &mut notif3] {
+        if let Some(NotificationEvent::NotificationStreamOpened {
+            peer,
+            negotiated_fallback,
+            handshake,
+            direction,
+        }) = notif.next_event().await
+        {
+            assert_eq!(peer_id, peer);
+            assert_eq!(negotiated_fallback, None);
+            assert_eq!(handshake, vec![1, 3, 3, 7]);
+            assert_eq!(direction, Direction::Inbound);
+        } else {
+            panic!("invalid event received");
+        }
+    }
+    // receive a notification from peer and verify all services receive it
+    handle
+        .report_notification_received(peer_id, vec![1, 3, 3, 8])
+        .unwrap();
+
+    for notif in [&mut notif1, &mut notif2, &mut notif3] {
+        if let Some(NotificationEvent::NotificationReceived { peer, notification }) =
+            notif.next_event().await
+        {
+            assert_eq!(peer_id, peer);
+            assert_eq!(notification, vec![1, 3, 3, 8]);
+        } else {
+            panic!("invalid event received");
+        }
+    }
+
+    for (i, notif) in [&mut notif1, &mut notif2, &mut notif3].iter().enumerate() {
+        // send notification from each service and verify peer receives it
+        notif.send_sync_notification(&peer_id, vec![1, 3, 3, i as u8]);
+        assert_eq!(
+            sync_rx.next().await,
+            Some(NotificationsSinkMessage::Notification {
+                message: vec![1, 3, 3, i as u8]
+            })
+        );
+    }
+
+    // close the substream for peer and verify all services receive the event
+    handle.report_substream_closed(peer_id).unwrap();
+
+    for notif in [&mut notif1, &mut notif2, &mut notif3] {
+        if let Some(NotificationEvent::NotificationStreamClosed { peer }) = notif.next_event().await
+        {
+            assert_eq!(peer_id, peer);
+        } else {
+            panic!("invalid event received");
+        }
+    }
+}
+
+#[tokio::test]
+async fn sending_notifications_using_notifications_sink_works() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // get a copy of the notification sink and send a synchronous notification using.
+    let sink = notif.message_sink(&peer_id).unwrap();
+    sink.send_sync_notification(vec![1, 3, 3, 6]);
+
+    // send an asynchronous notification using the acquired notifications sink.
+    sink.send_async_notification(vec![1, 3, 3, 7])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 6]
+        }),
+    );
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 7]
+        }),
+    );
+
+    // send notifications using the stored notification sink as well.
+    notif.send_sync_notification(&peer_id, vec![1, 3, 3, 8]);
+    notif
+        .send_async_notification(&peer_id, vec![1, 3, 3, 9])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 8]
+        }),
+    );
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 9]
+        }),
+    );
+}
+
+#[test]
+fn try_to_get_notifications_sink_for_non_existent_peer() {
+    let (_proto, notif) = notification_service("/proto/1".into());
+    assert!(notif.message_sink(&PeerId::random()).is_none());
+}
+
+#[tokio::test]
+async fn notification_sink_replaced() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (sink, mut async_rx, mut sync_rx) = NotificationsSink::new(PeerId::random());
+    let (mut handle, _stream) = proto.split();
+    let peer_id = PeerId::random();
+
+    // validate inbound substream
+    let ValidationCallResult::WaitForValidation(result_rx) = handle
+        .report_incoming_substream(peer_id, vec![1, 3, 3, 7])
+        .unwrap()
+    else {
+        panic!("peerset not enabled");
+    };
+
+    if let Some(NotificationEvent::ValidateInboundSubstream {
+        peer,
+        handshake,
+        result_tx,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        result_tx.send(ValidationResult::Accept).unwrap();
+    } else {
+        panic!("invalid event received");
+    }
+    assert_eq!(result_rx.await.unwrap(), ValidationResult::Accept);
+
+    // report that a substream has been opened
+    handle
+        .report_substream_opened(peer_id, Direction::Inbound, vec![1, 3, 3, 7], None, sink)
+        .unwrap();
+
+    if let Some(NotificationEvent::NotificationStreamOpened {
+        peer,
+        negotiated_fallback,
+        handshake,
+        direction,
+    }) = notif.next_event().await
+    {
+        assert_eq!(peer_id, peer);
+        assert_eq!(negotiated_fallback, None);
+        assert_eq!(handshake, vec![1, 3, 3, 7]);
+        assert_eq!(direction, Direction::Inbound);
+    } else {
+        panic!("invalid event received");
+    }
+
+    // get a copy of the notification sink and send a synchronous notification using.
+    let sink = notif.message_sink(&peer_id).unwrap();
+    sink.send_sync_notification(vec![1, 3, 3, 6]);
+
+    // send an asynchronous notification using the acquired notifications sink.
+    sink.send_async_notification(vec![1, 3, 3, 7])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 6]
+        }),
+    );
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 7]
+        }),
+    );
+
+    // send notifications using the stored notification sink as well.
+    notif.send_sync_notification(&peer_id, vec![1, 3, 3, 8]);
+    notif
+        .send_async_notification(&peer_id, vec![1, 3, 3, 9])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 8]
+        }),
+    );
+    assert_eq!(
+        async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 9]
+        }),
+    );
+
+    // the initial connection was closed and `Notifications` switched to secondary connection
+    // and emitted `CustomProtocolReplaced` which informs the local `NotificationService` that
+    // the notification sink was replaced.
+    let (new_sink, mut new_async_rx, mut new_sync_rx) = NotificationsSink::new(PeerId::random());
+    handle
+        .report_notification_sink_replaced(peer_id, new_sink)
+        .unwrap();
+
+    // drop the old sinks and poll `notif` once to register the sink replacement
+    drop(sync_rx);
+    drop(async_rx);
+
+    futures::future::poll_fn(|cx| {
+        let _ = std::pin::Pin::new(&mut notif.next_event()).poll(cx);
+        std::task::Poll::Ready(())
+    })
+    .await;
+
+    // verify that using the `NotificationService` API automatically results in using the correct
+    // sink
+    notif.send_sync_notification(&peer_id, vec![1, 3, 3, 8]);
+    notif
+        .send_async_notification(&peer_id, vec![1, 3, 3, 9])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        new_sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 8]
+        }),
+    );
+    assert_eq!(
+        new_async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 9]
+        }),
+    );
+
+    // now send two notifications using the acquired message sink and verify that
+    // it's also updated
+    sink.send_sync_notification(vec![1, 3, 3, 6]);
+
+    // send an asynchronous notification using the acquired notifications sink.
+    sink.send_async_notification(vec![1, 3, 3, 7])
+        .await
+        .unwrap();
+
+    assert_eq!(
+        new_sync_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 6]
+        }),
+    );
+    assert_eq!(
+        new_async_rx.next().await,
+        Some(NotificationsSinkMessage::Notification {
+            message: vec![1, 3, 3, 7]
+        }),
+    );
+}
+
+#[tokio::test]
+async fn set_handshake() {
+    let (proto, mut notif) = notification_service("/proto/1".into());
+    let (_handle, mut stream) = proto.split();
+
+    assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
+
+    match stream.next().await {
+        Some(NotificationCommand::SetHandshake(handshake)) => {
+            assert_eq!(handshake, vec![1, 3, 3, 7]);
+        }
+        _ => panic!("invalid event received"),
+    }
+
+    for _ in 0..COMMAND_QUEUE_SIZE {
+        assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_ok());
+    }
+
+    assert!(notif.try_set_handshake(vec![1, 3, 3, 7]).is_err());
+}
diff --git a/libp2p/src/protocol/notifications/tests.rs b/libp2p/src/protocol/notifications/tests.rs
new file mode 100644
index 000000000..6a34627f9
--- /dev/null
+++ b/libp2p/src/protocol/notifications/tests.rs
@@ -0,0 +1,404 @@
+// 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/>.
+
+#![cfg(test)]
+
+use crate::{
+    peer_store::PeerStore,
+    protocol::notifications::{Notifications, NotificationsOut, ProtocolConfig},
+    protocol_controller::{ProtoSetConfig, ProtocolController, SetId},
+    service::traits::{NotificationEvent, ValidationResult},
+    NotificationService,
+};
+
+use futures::{future::BoxFuture, prelude::*};
+use libp2p::{
+    core::{transport::MemoryTransport, upgrade, Endpoint},
+    identity, noise,
+    swarm::{
+        behaviour::FromSwarm, ConnectionDenied, ConnectionId, Executor, NetworkBehaviour,
+        PollParameters, Swarm, SwarmBuilder, SwarmEvent, THandler, THandlerInEvent,
+        THandlerOutEvent, ToSwarm,
+    },
+    yamux, Multiaddr, PeerId, Transport,
+};
+use sc_utils::mpsc::tracing_unbounded;
+use std::{
+    iter,
+    pin::Pin,
+    task::{Context, Poll},
+    time::Duration,
+};
+
+struct TokioExecutor(tokio::runtime::Runtime);
+impl Executor for TokioExecutor {
+    fn exec(&self, f: Pin<Box<dyn Future<Output = ()> + Send>>) {
+        let res = self.0.spawn(f);
+        drop(res);
+    }
+}
+
+/// 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>) {
+    let mut out = Vec::with_capacity(2);
+
+    let keypairs: Vec<_> = (0..2)
+        .map(|_| identity::Keypair::generate_ed25519())
+        .collect();
+    let addrs: Vec<Multiaddr> = (0..2)
+        .map(|_| {
+            format!("/memory/{}", rand::random::<u64>())
+                .parse()
+                .unwrap()
+        })
+        .collect();
+
+    for index in 0..2 {
+        let keypair = keypairs[index].clone();
+
+        let transport = MemoryTransport::new()
+            .upgrade(upgrade::Version::V1)
+            .authenticate(noise::Config::new(&keypair).unwrap())
+            .multiplex(yamux::Config::default())
+            .timeout(Duration::from_secs(20))
+            .boxed();
+
+        let (protocol_handle_pair, notif_service) =
+            crate::protocol::notifications::service::notification_service("/foo".into());
+        let mut notif_service = Box::new(notif_service) as Box<dyn NotificationService>;
+        let peer_store = PeerStore::new(if index == 0 {
+            keypairs
+                .iter()
+                .skip(1)
+                .map(|keypair| keypair.public().to_peer_id())
+                .collect()
+        } else {
+            vec![]
+        });
+
+        let (to_notifications, from_controller) =
+            tracing_unbounded("test_protocol_controller_to_notifications", 10_000);
+
+        let (controller_handle, controller) = ProtocolController::new(
+            SetId::from(0),
+            ProtoSetConfig {
+                in_peers: 25,
+                out_peers: 25,
+                reserved_nodes: Default::default(),
+                reserved_only: false,
+            },
+            to_notifications,
+            Box::new(peer_store.handle()),
+        );
+
+        let (notif_handle, command_stream) = protocol_handle_pair.split();
+        let behaviour = CustomProtoWithAddr {
+            inner: Notifications::new(
+                vec![controller_handle],
+                from_controller,
+                None,
+                iter::once((
+                    ProtocolConfig {
+                        name: "/foo".into(),
+                        fallback_names: Vec::new(),
+                        handshake: Vec::new(),
+                        max_notification_size: 1024 * 1024,
+                    },
+                    notif_handle,
+                    command_stream,
+                )),
+            ),
+            peer_store_future: peer_store.run().boxed(),
+            protocol_controller_future: controller.run().boxed(),
+            addrs: addrs
+                .iter()
+                .enumerate()
+                .filter_map(|(n, a)| {
+                    if n != index {
+                        Some((keypairs[n].public().to_peer_id(), a.clone()))
+                    } else {
+                        None
+                    }
+                })
+                .collect(),
+        };
+
+        let runtime = tokio::runtime::Runtime::new().unwrap();
+        runtime.spawn(async move {
+            loop {
+                if let NotificationEvent::ValidateInboundSubstream { result_tx, .. } =
+                    notif_service.next_event().await.unwrap()
+                {
+                    result_tx.send(ValidationResult::Accept).unwrap();
+                }
+            }
+        });
+
+        let mut swarm = SwarmBuilder::with_executor(
+            transport,
+            behaviour,
+            keypairs[index].public().to_peer_id(),
+            TokioExecutor(runtime),
+        )
+        .build();
+        swarm.listen_on(addrs[index].clone()).unwrap();
+        out.push(swarm);
+    }
+
+    // Final output
+    let mut out_iter = out.into_iter();
+    let first = out_iter.next().unwrap();
+    let second = out_iter.next().unwrap();
+    (first, second)
+}
+
+/// Wraps around the `CustomBehaviour` network behaviour, and adds hardcoded node addresses to it.
+struct CustomProtoWithAddr {
+    inner: Notifications,
+    peer_store_future: BoxFuture<'static, ()>,
+    protocol_controller_future: BoxFuture<'static, ()>,
+    addrs: Vec<(PeerId, Multiaddr)>,
+}
+
+impl std::ops::Deref for CustomProtoWithAddr {
+    type Target = Notifications;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl std::ops::DerefMut for CustomProtoWithAddr {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.inner
+    }
+}
+
+impl NetworkBehaviour for CustomProtoWithAddr {
+    type ConnectionHandler = <Notifications as NetworkBehaviour>::ConnectionHandler;
+    type OutEvent = <Notifications as NetworkBehaviour>::OutEvent;
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        self.inner
+            .handle_pending_inbound_connection(connection_id, local_addr, remote_addr)
+    }
+
+    fn handle_pending_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        maybe_peer: Option<PeerId>,
+        addresses: &[Multiaddr],
+        effective_role: Endpoint,
+    ) -> Result<Vec<Multiaddr>, ConnectionDenied> {
+        let mut list = self.inner.handle_pending_outbound_connection(
+            connection_id,
+            maybe_peer,
+            addresses,
+            effective_role,
+        )?;
+        if let Some(peer_id) = maybe_peer {
+            for (p, a) in self.addrs.iter() {
+                if *p == peer_id {
+                    list.push(a.clone());
+                }
+            }
+        }
+        Ok(list)
+    }
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.inner.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &Multiaddr,
+        role_override: Endpoint,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.inner
+            .handle_established_outbound_connection(connection_id, peer, addr, role_override)
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<Self::ConnectionHandler>) {
+        self.inner.on_swarm_event(event);
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        self.inner
+            .on_connection_handler_event(peer_id, connection_id, event);
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut Context,
+        params: &mut impl PollParameters,
+    ) -> Poll<ToSwarm<Self::OutEvent, THandlerInEvent<Self>>> {
+        let _ = self.peer_store_future.poll_unpin(cx);
+        let _ = self.protocol_controller_future.poll_unpin(cx);
+        self.inner.poll(cx, params)
+    }
+}
+
+#[test]
+#[allow(clippy::nonminimal_bool)]
+fn reconnect_after_disconnect() {
+    // We connect two nodes together, then force a disconnect (through the API of the `Service`),
+    // check that the disconnect worked, and finally check whether they successfully reconnect.
+
+    let (mut service1, mut service2) = build_nodes();
+
+    // For this test, the services can be in the following states.
+    #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+    enum ServiceState {
+        NotConnected,
+        FirstConnec,
+        Disconnected,
+        ConnectedAgain,
+    }
+    let mut service1_state = ServiceState::NotConnected;
+    let mut service2_state = ServiceState::NotConnected;
+
+    futures::executor::block_on(async move {
+        loop {
+            // Grab next event from services.
+            let event = {
+                let s1 = service1.select_next_some();
+                let s2 = service2.select_next_some();
+                futures::pin_mut!(s1, s2);
+                match future::select(s1, s2).await {
+                    future::Either::Left((ev, _)) => future::Either::Left(ev),
+                    future::Either::Right((ev, _)) => future::Either::Right(ev),
+                }
+            };
+
+            match event {
+                future::Either::Left(SwarmEvent::Behaviour(
+                    NotificationsOut::CustomProtocolOpen { .. },
+                )) => match service1_state {
+                    ServiceState::NotConnected => {
+                        service1_state = ServiceState::FirstConnec;
+                        if service2_state == ServiceState::FirstConnec {
+                            service1
+                                .behaviour_mut()
+                                .disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
+                        }
+                    }
+                    ServiceState::Disconnected => service1_state = ServiceState::ConnectedAgain,
+                    ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
+                },
+                future::Either::Left(SwarmEvent::Behaviour(
+                    NotificationsOut::CustomProtocolClosed { .. },
+                )) => match service1_state {
+                    ServiceState::FirstConnec => service1_state = ServiceState::Disconnected,
+                    ServiceState::ConnectedAgain
+                    | ServiceState::NotConnected
+                    | ServiceState::Disconnected => panic!(),
+                },
+                future::Either::Right(SwarmEvent::Behaviour(
+                    NotificationsOut::CustomProtocolOpen { .. },
+                )) => match service2_state {
+                    ServiceState::NotConnected => {
+                        service2_state = ServiceState::FirstConnec;
+                        if service1_state == ServiceState::FirstConnec {
+                            service1
+                                .behaviour_mut()
+                                .disconnect_peer(Swarm::local_peer_id(&service2), SetId::from(0));
+                        }
+                    }
+                    ServiceState::Disconnected => service2_state = ServiceState::ConnectedAgain,
+                    ServiceState::FirstConnec | ServiceState::ConnectedAgain => panic!(),
+                },
+                future::Either::Right(SwarmEvent::Behaviour(
+                    NotificationsOut::CustomProtocolClosed { .. },
+                )) => match service2_state {
+                    ServiceState::FirstConnec => service2_state = ServiceState::Disconnected,
+                    ServiceState::ConnectedAgain
+                    | ServiceState::NotConnected
+                    | ServiceState::Disconnected => panic!(),
+                },
+                _ => {}
+            }
+
+            // Due to the bug in `Notifications`, the disconnected node does not always detect that
+            // it was disconnected. The closed inbound substream is tolerated by design, and the
+            // closed outbound substream is not detected until something is sent into it.
+            // See [PR #13396](https://github.com/paritytech/substrate/pull/13396).
+            // This happens if the disconnecting node reconnects to it fast enough.
+            // In this case the disconnected node does not transit via `ServiceState::NotConnected`
+            // and stays in `ServiceState::FirstConnec`.
+            // TODO: update this once the fix is finally merged.
+            if service1_state == ServiceState::ConnectedAgain
+                && service2_state == ServiceState::ConnectedAgain
+                || service1_state == ServiceState::ConnectedAgain
+                    && service2_state == ServiceState::FirstConnec
+                || service1_state == ServiceState::FirstConnec
+                    && service2_state == ServiceState::ConnectedAgain
+            {
+                break;
+            }
+        }
+
+        // Now that the two services have disconnected and reconnected, wait for 3 seconds and
+        // check whether they're still connected.
+        let mut delay = futures_timer::Delay::new(Duration::from_secs(3));
+
+        loop {
+            // Grab next event from services.
+            let event = {
+                let s1 = service1.select_next_some();
+                let s2 = service2.select_next_some();
+                futures::pin_mut!(s1, s2);
+                match future::select(future::select(s1, s2), &mut delay).await {
+                    future::Either::Right(_) => break, // success
+                    future::Either::Left((future::Either::Left((ev, _)), _)) => ev,
+                    future::Either::Left((future::Either::Right((ev, _)), _)) => ev,
+                }
+            };
+
+            match event {
+                SwarmEvent::Behaviour(NotificationsOut::CustomProtocolOpen { .. })
+                | SwarmEvent::Behaviour(NotificationsOut::CustomProtocolClosed { .. }) => panic!(),
+                _ => {}
+            }
+        }
+    });
+}
diff --git a/libp2p/sync/src/strategy/chain_sync/test.rs b/libp2p/sync/src/strategy/chain_sync/test.rs
new file mode 100644
index 000000000..f76bf6e01
--- /dev/null
+++ b/libp2p/sync/src/strategy/chain_sync/test.rs
@@ -0,0 +1,1045 @@
+// 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/>.
+
+//! Tests of [`ChainSync`].
+
+use super::*;
+use futures::executor::block_on;
+use sc_block_builder::BlockBuilderBuilder;
+use sc_network_common::sync::message::{BlockAnnounce, BlockData, BlockState, FromBlock};
+use sp_blockchain::HeaderBackend;
+use substrate_test_runtime_client::{
+    runtime::{Block, Hash, Header},
+    BlockBuilderExt, ClientBlockImportExt, ClientExt, DefaultTestClientBuilderExt, TestClient,
+    TestClientBuilder, TestClientBuilderExt,
+};
+
+#[test]
+fn processes_empty_response_on_justification_request_for_unknown_block() {
+    // if we ask for a justification for a given block to a peer that doesn't know that block
+    // (different from not having a justification), the peer will reply with an empty response.
+    // internally we should process the response as the justification not being available.
+
+    let client = Arc::new(TestClientBuilder::new().build());
+    let peer_id = PeerId::random();
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
+
+    let (a1_hash, a1_number) = {
+        let a1 = BlockBuilderBuilder::new(&*client)
+            .on_parent_block(client.chain_info().best_hash)
+            .with_parent_block_number(client.chain_info().best_number)
+            .build()
+            .unwrap()
+            .build()
+            .unwrap()
+            .block;
+        (a1.hash(), *a1.header.number())
+    };
+
+    // add a new peer with the same best block
+    sync.add_peer(peer_id, a1_hash, a1_number);
+
+    // and request a justification for the block
+    sync.request_justification(&a1_hash, a1_number);
+
+    // the justification request should be scheduled to that peer
+    assert!(sync
+        .justification_requests()
+        .iter()
+        .any(|(who, request)| { *who == peer_id && request.from == FromBlock::Hash(a1_hash) }));
+
+    // there are no extra pending requests
+    assert_eq!(sync.extra_justifications.pending_requests().count(), 0);
+
+    // there's one in-flight extra request to the expected peer
+    assert!(sync
+        .extra_justifications
+        .active_requests()
+        .any(|(who, (hash, number))| {
+            *who == peer_id && *hash == a1_hash && *number == a1_number
+        }));
+
+    // if the peer replies with an empty response (i.e. it doesn't know the block),
+    // the active request should be cleared.
+    sync.on_block_justification(
+        peer_id,
+        BlockResponse::<Block> {
+            id: 0,
+            blocks: vec![],
+        },
+    )
+    .unwrap();
+
+    // there should be no in-flight requests
+    assert_eq!(sync.extra_justifications.active_requests().count(), 0);
+
+    // and the request should now be pending again, waiting for reschedule
+    assert!(sync
+        .extra_justifications
+        .pending_requests()
+        .any(|(hash, number)| { *hash == a1_hash && *number == a1_number }));
+}
+
+#[test]
+fn restart_doesnt_affect_peers_downloading_finality_data() {
+    let mut client = Arc::new(TestClientBuilder::new().build());
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
+
+    let peer_id1 = PeerId::random();
+    let peer_id2 = PeerId::random();
+    let peer_id3 = PeerId::random();
+
+    let mut new_blocks = |n| {
+        for _ in 0..n {
+            let block = BlockBuilderBuilder::new(&*client)
+                .on_parent_block(client.chain_info().best_hash)
+                .with_parent_block_number(client.chain_info().best_number)
+                .build()
+                .unwrap()
+                .build()
+                .unwrap()
+                .block;
+            block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
+        }
+
+        let info = client.info();
+        (info.best_hash, info.best_number)
+    };
+
+    let (b1_hash, b1_number) = new_blocks(50);
+
+    // add 2 peers at blocks that we don't have locally
+    sync.add_peer(peer_id1, Hash::random(), 42);
+    sync.add_peer(peer_id2, Hash::random(), 10);
+
+    // we wil send block requests to these peers
+    // for these blocks we don't know about
+    assert!(sync
+        .block_requests()
+        .into_iter()
+        .all(|(p, _)| { p == peer_id1 || p == peer_id2 }));
+
+    // add a new peer at a known block
+    sync.add_peer(peer_id3, b1_hash, b1_number);
+
+    // we request a justification for a block we have locally
+    sync.request_justification(&b1_hash, b1_number);
+
+    // the justification request should be scheduled to the
+    // new peer which is at the given block
+    assert!(sync.justification_requests().iter().any(|(p, r)| {
+        *p == peer_id3
+            && r.fields == BlockAttributes::JUSTIFICATION
+            && r.from == FromBlock::Hash(b1_hash)
+    }));
+
+    assert_eq!(
+        sync.peers.get(&peer_id3).unwrap().state,
+        PeerSyncState::DownloadingJustification(b1_hash),
+    );
+
+    // clear old actions
+    let _ = sync.take_actions();
+
+    // we restart the sync state
+    sync.restart();
+    let actions = sync.take_actions().collect::<Vec<_>>();
+
+    // which should make us send out block requests to the first two peers
+    assert_eq!(actions.len(), 2);
+    assert!(actions.iter().all(|action| match action {
+        ChainSyncAction::SendBlockRequest { peer_id, .. } =>
+            peer_id == &peer_id1 || peer_id == &peer_id2,
+        _ => false,
+    }));
+
+    // peer 3 should be unaffected it was downloading finality data
+    assert_eq!(
+        sync.peers.get(&peer_id3).unwrap().state,
+        PeerSyncState::DownloadingJustification(b1_hash),
+    );
+
+    // Set common block to something that we don't have (e.g. failed import)
+    sync.peers.get_mut(&peer_id3).unwrap().common_number = 100;
+    sync.restart();
+    assert_eq!(sync.peers.get(&peer_id3).unwrap().common_number, 50);
+}
+
+/// Send a block annoucnement for the given `header`.
+fn send_block_announce(header: Header, peer_id: PeerId, sync: &mut ChainSync<Block, TestClient>) {
+    let announce = BlockAnnounce {
+        header: header.clone(),
+        state: Some(BlockState::Best),
+        data: Some(Vec::new()),
+    };
+
+    let _ = sync.on_validated_block_announce(true, peer_id, &announce);
+}
+
+/// Create a block response from the given `blocks`.
+fn create_block_response(blocks: Vec<Block>) -> BlockResponse<Block> {
+    BlockResponse::<Block> {
+        id: 0,
+        blocks: blocks
+            .into_iter()
+            .map(|b| BlockData::<Block> {
+                hash: b.hash(),
+                header: Some(b.header().clone()),
+                body: Some(b.deconstruct().1),
+                indexed_body: None,
+                receipt: None,
+                message_queue: None,
+                justification: None,
+                justifications: None,
+            })
+            .collect(),
+    }
+}
+
+/// Get a block request from `sync` and check that is matches the expected request.
+fn get_block_request(
+    sync: &mut ChainSync<Block, TestClient>,
+    from: FromBlock<Hash, u64>,
+    max: u32,
+    peer: &PeerId,
+) -> BlockRequest<Block> {
+    let requests = sync.block_requests();
+
+    log::trace!(target: LOG_TARGET, "Requests: {requests:?}");
+
+    assert_eq!(1, requests.len());
+    assert_eq!(*peer, requests[0].0);
+
+    let request = requests[0].1.clone();
+
+    assert_eq!(from, request.from);
+    assert_eq!(Some(max), request.max);
+    request
+}
+
+/// Build and import a new best block.
+fn build_block(client: &mut Arc<TestClient>, at: Option<Hash>, fork: bool) -> Block {
+    let at = at.unwrap_or_else(|| client.info().best_hash);
+
+    let mut block_builder = BlockBuilderBuilder::new(&**client)
+        .on_parent_block(at)
+        .fetch_parent_block_number(&**client)
+        .unwrap()
+        .build()
+        .unwrap();
+
+    if fork {
+        block_builder
+            .push_storage_change(vec![1, 2, 3], Some(vec![4, 5, 6]))
+            .unwrap();
+    }
+
+    let block = block_builder.build().unwrap().block;
+
+    block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
+    block
+}
+
+fn unwrap_from_block_number(from: FromBlock<Hash, u64>) -> u64 {
+    if let FromBlock::Number(from) = from {
+        from
+    } else {
+        panic!("Expected a number!");
+    }
+}
+
+/// A regression test for a behavior we have seen on a live network.
+///
+/// The scenario is that the node is doing a full resync and is connected to some node that is
+/// doing a major sync as well. This other node that is doing a major sync will finish before
+/// our node and send a block announcement message, but we don't have seen any block
+/// announcement from this node in its sync process. Meaning our common number didn't change. It
+/// is now expected that we start an ancestor search to find the common number.
+#[test]
+fn do_ancestor_search_when_common_block_to_best_qeued_gap_is_to_big() {
+    sp_tracing::try_init_simple();
+
+    let blocks = {
+        let mut client = Arc::new(TestClientBuilder::new().build());
+        (0..MAX_DOWNLOAD_AHEAD * 2)
+            .map(|_| build_block(&mut client, None, false))
+            .collect::<Vec<_>>()
+    };
+
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let info = client.info();
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
+
+    let peer_id1 = PeerId::random();
+    let peer_id2 = PeerId::random();
+
+    let best_block = blocks.last().unwrap().clone();
+    let max_blocks_to_request = sync.max_blocks_per_request;
+    // Connect the node we will sync from
+    sync.add_peer(peer_id1, best_block.hash(), *best_block.header().number());
+    sync.add_peer(peer_id2, info.best_hash, 0);
+
+    let mut best_block_num = 0;
+    while best_block_num < MAX_DOWNLOAD_AHEAD {
+        let request = get_block_request(
+            &mut sync,
+            FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
+            max_blocks_to_request,
+            &peer_id1,
+        );
+
+        let from = unwrap_from_block_number(request.from.clone());
+
+        let mut resp_blocks = blocks[best_block_num as usize..from as usize].to_vec();
+        resp_blocks.reverse();
+
+        let response = create_block_response(resp_blocks.clone());
+
+        // Clear old actions to not deal with them
+        let _ = sync.take_actions();
+
+        sync.on_block_data(&peer_id1, Some(request), response)
+            .unwrap();
+
+        let actions = sync.take_actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            &actions[0],
+            ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == max_blocks_to_request as usize,
+        ));
+
+        best_block_num += max_blocks_to_request;
+
+        sync.on_blocks_processed(
+            max_blocks_to_request as usize,
+            max_blocks_to_request as usize,
+            resp_blocks
+                .iter()
+                .rev()
+                .map(|b| {
+                    (
+                        Ok(BlockImportStatus::ImportedUnknown(
+                            *b.header().number(),
+                            Default::default(),
+                            Some(peer_id1),
+                        )),
+                        b.hash(),
+                    )
+                })
+                .collect(),
+        );
+
+        resp_blocks
+            .into_iter()
+            .rev()
+            .for_each(|b| block_on(client.import_as_final(BlockOrigin::Own, b)).unwrap());
+    }
+
+    // "Wait" for the queue to clear
+    sync.queue_blocks.clear();
+
+    // Let peer2 announce that it finished syncing
+    send_block_announce(best_block.header().clone(), peer_id2, &mut sync);
+
+    let (peer1_req, peer2_req) =
+        sync.block_requests()
+            .into_iter()
+            .fold((None, None), |res, req| {
+                if req.0 == peer_id1 {
+                    (Some(req.1), res.1)
+                } else if req.0 == peer_id2 {
+                    (res.0, Some(req.1))
+                } else {
+                    panic!("Unexpected req: {:?}", req)
+                }
+            });
+
+    // We should now do an ancestor search to find the correct common block.
+    let peer2_req = peer2_req.unwrap();
+    assert_eq!(Some(1), peer2_req.max);
+    assert_eq!(FromBlock::Number(best_block_num as u64), peer2_req.from);
+
+    let response = create_block_response(vec![blocks[(best_block_num - 1) as usize].clone()]);
+
+    // Clear old actions to not deal with them
+    let _ = sync.take_actions();
+
+    sync.on_block_data(&peer_id2, Some(peer2_req), response)
+        .unwrap();
+
+    let actions = sync.take_actions().collect::<Vec<_>>();
+    assert!(actions.is_empty());
+
+    let peer1_from = unwrap_from_block_number(peer1_req.unwrap().from);
+
+    // As we are on the same chain, we should directly continue with requesting blocks from
+    // peer 2 as well.
+    get_block_request(
+        &mut sync,
+        FromBlock::Number(peer1_from + max_blocks_to_request as u64),
+        max_blocks_to_request,
+        &peer_id2,
+    );
+}
+
+/// A test that ensures that we can sync a huge fork.
+///
+/// The following scenario:
+/// A peer connects to us and we both have the common block 512. The last finalized is 2048.
+/// Our best block is 4096. The peer send us a block announcement with 4097 from a fork.
+///
+/// We will first do an ancestor search to find the common block. After that we start to sync
+/// the fork and finish it ;)
+#[test]
+fn can_sync_huge_fork() {
+    sp_tracing::try_init_simple();
+
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let blocks = (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 4)
+        .map(|_| build_block(&mut client, None, false))
+        .collect::<Vec<_>>();
+
+    let fork_blocks = {
+        let mut client = Arc::new(TestClientBuilder::new().build());
+        let fork_blocks = blocks[..MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2]
+            .iter()
+            .inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap())
+            .cloned()
+            .collect::<Vec<_>>();
+
+        fork_blocks
+            .into_iter()
+            .chain(
+                (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 2 + 1)
+                    .map(|_| build_block(&mut client, None, true)),
+            )
+            .collect::<Vec<_>>()
+    };
+
+    let info = client.info();
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
+
+    let finalized_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2 - 1].clone();
+    let just = (*b"TEST", Vec::new());
+    client
+        .finalize_block(finalized_block.hash(), Some(just))
+        .unwrap();
+    sync.update_chain_info(&info.best_hash, info.best_number);
+
+    let peer_id1 = PeerId::random();
+
+    let common_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize / 2].clone();
+    // Connect the node we will sync from
+    sync.add_peer(
+        peer_id1,
+        common_block.hash(),
+        *common_block.header().number(),
+    );
+
+    send_block_announce(
+        fork_blocks.last().unwrap().header().clone(),
+        peer_id1,
+        &mut sync,
+    );
+
+    let mut request =
+        get_block_request(&mut sync, FromBlock::Number(info.best_number), 1, &peer_id1);
+
+    // Discard old actions we are not interested in
+    let _ = sync.take_actions();
+
+    // Do the ancestor search
+    loop {
+        let block = &fork_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1];
+        let response = create_block_response(vec![block.clone()]);
+
+        sync.on_block_data(&peer_id1, Some(request), response)
+            .unwrap();
+
+        let actions = sync.take_actions().collect::<Vec<_>>();
+
+        request = if actions.is_empty() {
+            // We found the ancenstor
+            break;
+        } else {
+            assert_eq!(actions.len(), 1);
+            match &actions[0] {
+                ChainSyncAction::SendBlockRequest {
+                    peer_id: _,
+                    request,
+                } => request.clone(),
+                action => panic!("Unexpected action: {action:?}"),
+            }
+        };
+
+        log::trace!(target: LOG_TARGET, "Request: {request:?}");
+    }
+
+    // Now request and import the fork.
+    let mut best_block_num = *finalized_block.header().number() as u32;
+    let max_blocks_to_request = sync.max_blocks_per_request;
+    while best_block_num < *fork_blocks.last().unwrap().header().number() as u32 - 1 {
+        let request = get_block_request(
+            &mut sync,
+            FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
+            max_blocks_to_request,
+            &peer_id1,
+        );
+
+        let from = unwrap_from_block_number(request.from.clone());
+
+        let mut resp_blocks = fork_blocks[best_block_num as usize..from as usize].to_vec();
+        resp_blocks.reverse();
+
+        let response = create_block_response(resp_blocks.clone());
+
+        sync.on_block_data(&peer_id1, Some(request), response)
+            .unwrap();
+
+        let actions = sync.take_actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            &actions[0],
+            ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == sync.max_blocks_per_request as usize
+        ));
+
+        best_block_num += sync.max_blocks_per_request;
+
+        sync.on_blocks_processed(
+            max_blocks_to_request as usize,
+            max_blocks_to_request as usize,
+            resp_blocks
+                .iter()
+                .rev()
+                .map(|b| {
+                    (
+                        Ok(BlockImportStatus::ImportedUnknown(
+                            *b.header().number(),
+                            Default::default(),
+                            Some(peer_id1),
+                        )),
+                        b.hash(),
+                    )
+                })
+                .collect(),
+        );
+
+        // Discard pending actions
+        let _ = sync.take_actions();
+
+        resp_blocks
+            .into_iter()
+            .rev()
+            .for_each(|b| block_on(client.import(BlockOrigin::Own, b)).unwrap());
+    }
+
+    // Request the tip
+    get_block_request(
+        &mut sync,
+        FromBlock::Hash(fork_blocks.last().unwrap().hash()),
+        1,
+        &peer_id1,
+    );
+}
+
+#[test]
+fn syncs_fork_without_duplicate_requests() {
+    sp_tracing::try_init_simple();
+
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let blocks = (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 4)
+        .map(|_| build_block(&mut client, None, false))
+        .collect::<Vec<_>>();
+
+    let fork_blocks = {
+        let mut client = Arc::new(TestClientBuilder::new().build());
+        let fork_blocks = blocks[..MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2]
+            .iter()
+            .inspect(|b| block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap())
+            .cloned()
+            .collect::<Vec<_>>();
+
+        fork_blocks
+            .into_iter()
+            .chain(
+                (0..MAX_BLOCKS_TO_LOOK_BACKWARDS * 2 + 1)
+                    .map(|_| build_block(&mut client, None, true)),
+            )
+            .collect::<Vec<_>>()
+    };
+
+    let info = client.info();
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
+
+    let finalized_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2 - 1].clone();
+    let just = (*b"TEST", Vec::new());
+    client
+        .finalize_block(finalized_block.hash(), Some(just))
+        .unwrap();
+    sync.update_chain_info(&info.best_hash, info.best_number);
+
+    let peer_id1 = PeerId::random();
+
+    let common_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize / 2].clone();
+    // Connect the node we will sync from
+    sync.add_peer(
+        peer_id1,
+        common_block.hash(),
+        *common_block.header().number(),
+    );
+
+    send_block_announce(
+        fork_blocks.last().unwrap().header().clone(),
+        peer_id1,
+        &mut sync,
+    );
+
+    let mut request =
+        get_block_request(&mut sync, FromBlock::Number(info.best_number), 1, &peer_id1);
+
+    // Discard pending actions
+    let _ = sync.take_actions();
+
+    // Do the ancestor search
+    loop {
+        let block = &fork_blocks[unwrap_from_block_number(request.from.clone()) as usize - 1];
+        let response = create_block_response(vec![block.clone()]);
+
+        sync.on_block_data(&peer_id1, Some(request), response)
+            .unwrap();
+
+        let actions = sync.take_actions().collect::<Vec<_>>();
+
+        request = if actions.is_empty() {
+            // We found the ancenstor
+            break;
+        } else {
+            assert_eq!(actions.len(), 1);
+            match &actions[0] {
+                ChainSyncAction::SendBlockRequest {
+                    peer_id: _,
+                    request,
+                } => request.clone(),
+                action => panic!("Unexpected action: {action:?}"),
+            }
+        };
+
+        log::trace!(target: LOG_TARGET, "Request: {request:?}");
+    }
+
+    // Now request and import the fork.
+    let mut best_block_num = *finalized_block.header().number() as u32;
+    let max_blocks_to_request = sync.max_blocks_per_request;
+
+    let mut request = get_block_request(
+        &mut sync,
+        FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
+        max_blocks_to_request,
+        &peer_id1,
+    );
+    let last_block_num = *fork_blocks.last().unwrap().header().number() as u32 - 1;
+    while best_block_num < last_block_num {
+        let from = unwrap_from_block_number(request.from.clone());
+
+        let mut resp_blocks = fork_blocks[best_block_num as usize..from as usize].to_vec();
+        resp_blocks.reverse();
+
+        let response = create_block_response(resp_blocks.clone());
+
+        // Discard old actions
+        let _ = sync.take_actions();
+
+        sync.on_block_data(&peer_id1, Some(request.clone()), response)
+            .unwrap();
+
+        let actions = sync.take_actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            &actions[0],
+            ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == max_blocks_to_request as usize
+        ));
+
+        best_block_num += max_blocks_to_request;
+
+        if best_block_num < last_block_num {
+            // make sure we're not getting a duplicate request in the time before the blocks are
+            // processed
+            request = get_block_request(
+                &mut sync,
+                FromBlock::Number(max_blocks_to_request as u64 + best_block_num as u64),
+                max_blocks_to_request,
+                &peer_id1,
+            );
+        }
+
+        let mut notify_imported: Vec<_> = resp_blocks
+            .iter()
+            .rev()
+            .map(|b| {
+                (
+                    Ok(BlockImportStatus::ImportedUnknown(
+                        *b.header().number(),
+                        Default::default(),
+                        Some(peer_id1),
+                    )),
+                    b.hash(),
+                )
+            })
+            .collect();
+
+        // The import queue may send notifications in batches of varying size. So we simulate
+        // this here by splitting the batch into 2 notifications.
+        let max_blocks_to_request = sync.max_blocks_per_request;
+        let second_batch = notify_imported.split_off(notify_imported.len() / 2);
+        sync.on_blocks_processed(
+            max_blocks_to_request as usize,
+            max_blocks_to_request as usize,
+            notify_imported,
+        );
+
+        sync.on_blocks_processed(
+            max_blocks_to_request as usize,
+            max_blocks_to_request as usize,
+            second_batch,
+        );
+
+        resp_blocks
+            .into_iter()
+            .rev()
+            .for_each(|b| block_on(client.import(BlockOrigin::Own, b)).unwrap());
+    }
+
+    // Request the tip
+    get_block_request(
+        &mut sync,
+        FromBlock::Hash(fork_blocks.last().unwrap().hash()),
+        1,
+        &peer_id1,
+    );
+}
+
+#[test]
+fn removes_target_fork_on_disconnect() {
+    sp_tracing::try_init_simple();
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let blocks = (0..3)
+        .map(|_| build_block(&mut client, None, false))
+        .collect::<Vec<_>>();
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
+
+    let peer_id1 = PeerId::random();
+    let common_block = blocks[1].clone();
+    // Connect the node we will sync from
+    sync.add_peer(
+        peer_id1,
+        common_block.hash(),
+        *common_block.header().number(),
+    );
+
+    // Create a "new" header and announce it
+    let mut header = blocks[0].header().clone();
+    header.number = 4;
+    send_block_announce(header, peer_id1, &mut sync);
+    assert!(sync.fork_targets.len() == 1);
+
+    sync.remove_peer(&peer_id1);
+    assert!(sync.fork_targets.is_empty());
+}
+
+#[test]
+fn can_import_response_with_missing_blocks() {
+    sp_tracing::try_init_simple();
+    let mut client2 = Arc::new(TestClientBuilder::new().build());
+    let blocks = (0..4)
+        .map(|_| build_block(&mut client2, None, false))
+        .collect::<Vec<_>>();
+
+    let empty_client = Arc::new(TestClientBuilder::new().build());
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, empty_client.clone(), 1, 64, None).unwrap();
+
+    let peer_id1 = PeerId::random();
+    let best_block = blocks[3].clone();
+    sync.add_peer(peer_id1, best_block.hash(), *best_block.header().number());
+
+    sync.peers.get_mut(&peer_id1).unwrap().state = PeerSyncState::Available;
+    sync.peers.get_mut(&peer_id1).unwrap().common_number = 0;
+
+    // Request all missing blocks and respond only with some.
+    let request = get_block_request(&mut sync, FromBlock::Hash(best_block.hash()), 4, &peer_id1);
+    let response = create_block_response(vec![
+        blocks[3].clone(),
+        blocks[2].clone(),
+        blocks[1].clone(),
+    ]);
+    sync.on_block_data(&peer_id1, Some(request.clone()), response)
+        .unwrap();
+    assert_eq!(sync.best_queued_number, 0);
+
+    // Request should only contain the missing block.
+    let request = get_block_request(&mut sync, FromBlock::Number(1), 1, &peer_id1);
+    let response = create_block_response(vec![blocks[0].clone()]);
+    sync.on_block_data(&peer_id1, Some(request), response)
+        .unwrap();
+    assert_eq!(sync.best_queued_number, 4);
+}
+#[test]
+fn ancestor_search_repeat() {
+    let state = AncestorSearchState::<Block>::BinarySearch(1, 3);
+    assert!(handle_ancestor_search_state(&state, 2, true).is_none());
+}
+
+#[test]
+fn sync_restart_removes_block_but_not_justification_requests() {
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 1, 64, None).unwrap();
+
+    let peers = [PeerId::random(), PeerId::random()];
+
+    let mut new_blocks = |n| {
+        for _ in 0..n {
+            let block = BlockBuilderBuilder::new(&*client)
+                .on_parent_block(client.chain_info().best_hash)
+                .with_parent_block_number(client.chain_info().best_number)
+                .build()
+                .unwrap()
+                .build()
+                .unwrap()
+                .block;
+            block_on(client.import(BlockOrigin::Own, block.clone())).unwrap();
+        }
+
+        let info = client.info();
+        (info.best_hash, info.best_number)
+    };
+
+    let (b1_hash, b1_number) = new_blocks(50);
+
+    // add new peer and request blocks from them
+    sync.add_peer(peers[0], Hash::random(), 42);
+
+    // we don't actually perform any requests, just keep track of peers waiting for a response
+    let mut pending_responses = HashSet::new();
+
+    // we wil send block requests to these peers
+    // for these blocks we don't know about
+    for (peer, _request) in sync.block_requests() {
+        // "send" request
+        pending_responses.insert(peer);
+    }
+
+    // add a new peer at a known block
+    sync.add_peer(peers[1], b1_hash, b1_number);
+
+    // we request a justification for a block we have locally
+    sync.request_justification(&b1_hash, b1_number);
+
+    // the justification request should be scheduled to the
+    // new peer which is at the given block
+    let mut requests = sync.justification_requests();
+    assert_eq!(requests.len(), 1);
+    let (peer, _request) = requests.remove(0);
+    // "send" request
+    assert!(pending_responses.insert(peer));
+
+    assert!(!std::matches!(
+        sync.peers.get(&peers[0]).unwrap().state,
+        PeerSyncState::DownloadingJustification(_),
+    ));
+    assert_eq!(
+        sync.peers.get(&peers[1]).unwrap().state,
+        PeerSyncState::DownloadingJustification(b1_hash),
+    );
+    assert_eq!(pending_responses.len(), 2);
+
+    // discard old actions
+    let _ = sync.take_actions();
+
+    // restart sync
+    sync.restart();
+    let actions = sync.take_actions().collect::<Vec<_>>();
+    for action in actions.iter() {
+        match action {
+            ChainSyncAction::CancelBlockRequest { peer_id } => {
+                pending_responses.remove(peer_id);
+            }
+            ChainSyncAction::SendBlockRequest { peer_id, .. } => {
+                // we drop obsolete response, but don't register a new request, it's checked in
+                // the `assert!` below
+                pending_responses.remove(peer_id);
+            }
+            action => panic!("Unexpected action: {action:?}"),
+        }
+    }
+    assert!(actions.iter().any(|action| {
+        match action {
+            ChainSyncAction::SendBlockRequest { peer_id, .. } => peer_id == &peers[0],
+            _ => false,
+        }
+    }));
+
+    assert_eq!(pending_responses.len(), 1);
+    assert!(pending_responses.contains(&peers[1]));
+    assert_eq!(
+        sync.peers.get(&peers[1]).unwrap().state,
+        PeerSyncState::DownloadingJustification(b1_hash),
+    );
+    sync.remove_peer(&peers[1]);
+    pending_responses.remove(&peers[1]);
+    assert_eq!(pending_responses.len(), 0);
+}
+
+/// The test demonstrates https://github.com/paritytech/polkadot-sdk/issues/2094.
+/// TODO: convert it into desired behavior test once the issue is fixed (see inline comments).
+/// The issue: we currently rely on block numbers instead of block hash
+/// to download blocks from peers. As a result, we can end up with blocks
+/// from different forks as shown by the test.
+#[test]
+#[should_panic]
+fn request_across_forks() {
+    sp_tracing::try_init_simple();
+
+    let mut client = Arc::new(TestClientBuilder::new().build());
+    let blocks = (0..100)
+        .map(|_| build_block(&mut client, None, false))
+        .collect::<Vec<_>>();
+
+    let fork_a_blocks = {
+        let mut client = Arc::new(TestClientBuilder::new().build());
+        let mut fork_blocks = blocks[..]
+            .iter()
+            .inspect(|b| {
+                assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_))));
+                block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()
+            })
+            .cloned()
+            .collect::<Vec<_>>();
+        for _ in 0..10 {
+            fork_blocks.push(build_block(&mut client, None, false));
+        }
+        fork_blocks
+    };
+
+    let fork_b_blocks = {
+        let mut client = Arc::new(TestClientBuilder::new().build());
+        let mut fork_blocks = blocks[..]
+            .iter()
+            .inspect(|b| {
+                assert!(matches!(client.block(*b.header.parent_hash()), Ok(Some(_))));
+                block_on(client.import(BlockOrigin::Own, (*b).clone())).unwrap()
+            })
+            .cloned()
+            .collect::<Vec<_>>();
+        for _ in 0..10 {
+            fork_blocks.push(build_block(&mut client, None, true));
+        }
+        fork_blocks
+    };
+
+    let mut sync = ChainSync::new(ChainSyncMode::Full, client.clone(), 5, 64, None).unwrap();
+
+    // Add the peers, all at the common ancestor 100.
+    let common_block = blocks.last().unwrap();
+    let peer_id1 = PeerId::random();
+    sync.add_peer(
+        peer_id1,
+        common_block.hash(),
+        *common_block.header().number(),
+    );
+    let peer_id2 = PeerId::random();
+    sync.add_peer(
+        peer_id2,
+        common_block.hash(),
+        *common_block.header().number(),
+    );
+
+    // Peer 1 announces 107 from fork 1, 100-107 get downloaded.
+    {
+        let block = fork_a_blocks[106].clone();
+        let peer = peer_id1;
+        log::trace!(target: LOG_TARGET, "<1> {peer} announces from fork 1");
+        send_block_announce(block.header().clone(), peer, &mut sync);
+        let request = get_block_request(&mut sync, FromBlock::Hash(block.hash()), 7, &peer);
+        let mut resp_blocks = fork_a_blocks[100_usize..107_usize].to_vec();
+        resp_blocks.reverse();
+        let response = create_block_response(resp_blocks.clone());
+
+        // Drop old actions
+        let _ = sync.take_actions();
+
+        sync.on_block_data(&peer, Some(request), response).unwrap();
+        let actions = sync.take_actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            &actions[0],
+            ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == 7_usize
+        ));
+        assert_eq!(sync.best_queued_number, 107);
+        assert_eq!(sync.best_queued_hash, block.hash());
+        assert!(sync.is_known(block.header.parent_hash()));
+    }
+
+    // Peer 2 also announces 107 from fork 1.
+    {
+        let prev_best_number = sync.best_queued_number;
+        let prev_best_hash = sync.best_queued_hash;
+        let peer = peer_id2;
+        log::trace!(target: LOG_TARGET, "<2> {peer} announces from fork 1");
+        for block in fork_a_blocks.iter().take(107).skip(100) {
+            let block = block.clone();
+            send_block_announce(block.header().clone(), peer, &mut sync);
+            assert!(sync.block_requests().is_empty());
+        }
+        assert_eq!(sync.best_queued_number, prev_best_number);
+        assert_eq!(sync.best_queued_hash, prev_best_hash);
+    }
+
+    // Peer 2 undergoes reorg, announces 108 from fork 2, gets downloaded even though we
+    // don't have the parent from fork 2.
+    {
+        let block = fork_b_blocks[107].clone();
+        let peer = peer_id2;
+        log::trace!(target: LOG_TARGET, "<3> {peer} announces from fork 2");
+        send_block_announce(block.header().clone(), peer, &mut sync);
+        // TODO: when the issue is fixed, this test can be changed to test the
+        // expected behavior instead. The needed changes would be:
+        // 1. Remove the `#[should_panic]` directive
+        // 2. These should be changed to check that sync.block_requests().is_empty(), after the
+        //    block is announced.
+        let request = get_block_request(&mut sync, FromBlock::Hash(block.hash()), 1, &peer);
+        let response = create_block_response(vec![block.clone()]);
+
+        // Drop old actions we are not going to check
+        let _ = sync.take_actions();
+
+        sync.on_block_data(&peer, Some(request), response).unwrap();
+        let actions = sync.take_actions().collect::<Vec<_>>();
+        assert_eq!(actions.len(), 1);
+        assert!(matches!(
+            &actions[0],
+            ChainSyncAction::ImportBlocks{ origin: _, blocks } if blocks.len() == 1_usize
+        ));
+        assert!(sync.is_known(block.header.parent_hash()));
+    }
+}
diff --git a/libp2p/test/Cargo.toml b/libp2p/test/Cargo.toml
new file mode 100644
index 000000000..9e0fef65e
--- /dev/null
+++ b/libp2p/test/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+description = "Integration tests for Substrate network protocol"
+name = "sc-network-test"
+version = "0.8.0"
+license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
+publish = false
+homepage = "https://substrate.io"
+workspace = "../.."
+edition = "2021"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+tokio = "1.22.0"
+async-trait = "0.1.74"
+futures = "0.3.21"
+futures-timer = "3.0.1"
+libp2p = "0.51.4"
+log = "0.4.17"
+parking_lot = "0.12.1"
+rand = "0.8.5"
+sc-block-builder = { workspace = true }
+sc-client-api = { workspace = true }
+sc-consensus = { workspace = true }
+sc-network = { package = "libp2p-gadget", path = ".." }
+sc-network-common = { path = "../common" }
+sc-utils = { workspace = true }
+sc-network-light = { path = "../light" }
+sc-network-sync = { path = "../sync" }
+sc-service = { workspace = true, default-features = false, features = ["test-helpers"] }
+sp-blockchain = { workspace = true }
+sp-consensus = { workspace = true }
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+sp-tracing = { workspace = true }
+substrate-test-runtime = { workspace = true }
+substrate-test-runtime-client = { workspace = true }
diff --git a/libp2p/test/src/block_import.rs b/libp2p/test/src/block_import.rs
new file mode 100644
index 000000000..35795432b
--- /dev/null
+++ b/libp2p/test/src/block_import.rs
@@ -0,0 +1,134 @@
+// 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/>.
+
+//! Testing block import logic.
+
+use super::*;
+use futures::executor::block_on;
+use sc_consensus::{
+	import_single_block, BasicQueue, BlockImportError, BlockImportStatus, ImportedAux,
+	IncomingBlock,
+};
+use sp_consensus::BlockOrigin;
+use substrate_test_runtime_client::{
+	self,
+	prelude::*,
+	runtime::{Block, Hash},
+};
+
+fn prepare_good_block() -> (TestClient, Hash, u64, PeerId, IncomingBlock<Block>) {
+	let mut client = substrate_test_runtime_client::new();
+	let block = BlockBuilderBuilder::new(&client)
+		.on_parent_block(client.chain_info().best_hash)
+		.with_parent_block_number(client.chain_info().best_number)
+		.build()
+		.unwrap()
+		.build()
+		.unwrap()
+		.block;
+	block_on(client.import(BlockOrigin::File, block)).unwrap();
+
+	let (hash, number) = (client.block_hash(1).unwrap().unwrap(), 1);
+	let header = client.header(hash).unwrap();
+	let justifications = client.justifications(hash).unwrap();
+	let peer_id = PeerId::random();
+	(
+		client,
+		hash,
+		number,
+		peer_id,
+		IncomingBlock {
+			hash,
+			header,
+			body: Some(Vec::new()),
+			indexed_body: None,
+			justifications,
+			origin: Some(peer_id),
+			allow_missing_state: false,
+			import_existing: false,
+			state: None,
+			skip_execution: false,
+		},
+	)
+}
+
+#[test]
+fn import_single_good_block_works() {
+	let (_, _hash, number, peer_id, block) = prepare_good_block();
+
+	let mut expected_aux = ImportedAux::default();
+	expected_aux.is_new_best = true;
+
+	match block_on(import_single_block(
+		&mut substrate_test_runtime_client::new(),
+		BlockOrigin::File,
+		block,
+		&mut PassThroughVerifier::new(true),
+	)) {
+		Ok(BlockImportStatus::ImportedUnknown(ref num, ref aux, ref org))
+			if *num == number && *aux == expected_aux && *org == Some(peer_id) => {},
+		r @ _ => panic!("{:?}", r),
+	}
+}
+
+#[test]
+fn import_single_good_known_block_is_ignored() {
+	let (mut client, _hash, number, _, block) = prepare_good_block();
+	match block_on(import_single_block(
+		&mut client,
+		BlockOrigin::File,
+		block,
+		&mut PassThroughVerifier::new(true),
+	)) {
+		Ok(BlockImportStatus::ImportedKnown(ref n, _)) if *n == number => {},
+		_ => panic!(),
+	}
+}
+
+#[test]
+fn import_single_good_block_without_header_fails() {
+	let (_, _, _, peer_id, mut block) = prepare_good_block();
+	block.header = None;
+	match block_on(import_single_block(
+		&mut substrate_test_runtime_client::new(),
+		BlockOrigin::File,
+		block,
+		&mut PassThroughVerifier::new(true),
+	)) {
+		Err(BlockImportError::IncompleteHeader(ref org)) if *org == Some(peer_id) => {},
+		_ => panic!(),
+	}
+}
+
+#[test]
+fn async_import_queue_drops() {
+	let executor = sp_core::testing::TaskExecutor::new();
+	// Perform this test multiple times since it exhibits non-deterministic behavior.
+	for _ in 0..100 {
+		let verifier = PassThroughVerifier::new(true);
+
+		let queue = BasicQueue::new(
+			verifier,
+			Box::new(substrate_test_runtime_client::new()),
+			None,
+			&executor,
+			None,
+		);
+		drop(queue);
+	}
+}
diff --git a/libp2p/test/src/fuzz.rs b/libp2p/test/src/fuzz.rs
new file mode 100644
index 000000000..2e288accd
--- /dev/null
+++ b/libp2p/test/src/fuzz.rs
@@ -0,0 +1,418 @@
+// 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/>.
+
+//! Fuzz test emulates network events and peer connection handling by `ProtocolController`
+//! and `PeerStore` to discover possible inconsistencies in peer management.
+
+use futures::prelude::*;
+use libp2p::PeerId;
+use rand::{
+	distributions::{Distribution, Uniform, WeightedIndex},
+	seq::IteratorRandom,
+};
+use sc_network::{
+	peer_store::{PeerStore, PeerStoreProvider},
+	protocol_controller::{IncomingIndex, Message, ProtoSetConfig, ProtocolController, SetId},
+	ReputationChange,
+};
+use sc_utils::mpsc::tracing_unbounded;
+use std::collections::{HashMap, HashSet};
+
+/// Peer events as observed by `Notifications` / fuzz test.
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+enum Event {
+	/// Either API requested to disconnect from the peer, or the peer dropped.
+	Disconnected,
+	/// Incoming request.
+	Incoming,
+	/// Answer from PSM: accept.
+	PsmAccept,
+	/// Answer from PSM: reject.
+	PsmReject,
+	/// Command from PSM: connect.
+	PsmConnect,
+	/// Command from PSM: drop connection.
+	PsmDrop,
+}
+
+/// Simplified peer state as thought by `Notifications` / fuzz test.
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+enum State {
+	/// Peer is not connected.
+	Disconnected,
+	/// We have an inbound connection, but have not decided yet whether to accept it.
+	Incoming(IncomingIndex),
+	/// Peer is connected via an inbound connection.
+	Inbound,
+	/// Peer is connected via an outbound connection.
+	Outbound,
+}
+
+/// Bare simplified state without incoming index.
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+enum BareState {
+	/// Peer is not connected.
+	Disconnected,
+	/// We have an inbound connection, but have not decided yet whether to accept it.
+	Incoming,
+	/// Peer is connected via an inbound connection.
+	Inbound,
+	/// Peer is connected via an outbound connection.
+	Outbound,
+}
+
+fn discard_incoming_index(state: State) -> BareState {
+	match state {
+		State::Disconnected => BareState::Disconnected,
+		State::Incoming(_) => BareState::Incoming,
+		State::Inbound => BareState::Inbound,
+		State::Outbound => BareState::Outbound,
+	}
+}
+
+#[tokio::test]
+async fn run() {
+	sp_tracing::try_init_simple();
+
+	for _ in 0..50 {
+		test_once().await;
+	}
+}
+
+async fn test_once() {
+	// Allowed events that can be received in a specific state.
+	let allowed_events: HashMap<BareState, HashSet<Event>> = [
+		(
+			BareState::Disconnected,
+			[Event::Incoming, Event::PsmConnect, Event::PsmDrop /* must be ignored */]
+				.into_iter()
+				.collect::<HashSet<_>>(),
+		),
+		(
+			BareState::Incoming,
+			[Event::PsmAccept, Event::PsmReject].into_iter().collect::<HashSet<_>>(),
+		),
+		(
+			BareState::Inbound,
+			[Event::Disconnected, Event::PsmDrop, Event::PsmConnect /* must be ignored */]
+				.into_iter()
+				.collect::<HashSet<_>>(),
+		),
+		(
+			BareState::Outbound,
+			[Event::Disconnected, Event::PsmDrop, Event::PsmConnect /* must be ignored */]
+				.into_iter()
+				.collect::<HashSet<_>>(),
+		),
+	]
+	.into_iter()
+	.collect();
+
+	// PRNG to use.
+	let mut rng = rand::thread_rng();
+
+	// Nodes that the peerset knows about.
+	let mut known_nodes = HashMap::<PeerId, State>::new();
+	// Nodes that we have reserved. Always a subset of `known_nodes`.
+	let mut reserved_nodes = HashSet::<PeerId>::new();
+
+	// Bootnodes for `PeerStore` initialization.
+	let bootnodes = (0..Uniform::new_inclusive(0, 4).sample(&mut rng))
+		.map(|_| {
+			let id = PeerId::random();
+			known_nodes.insert(id, State::Disconnected);
+			id
+		})
+		.collect();
+
+	let peer_store = PeerStore::new(bootnodes);
+	let mut peer_store_handle = peer_store.handle();
+
+	let (to_notifications, mut from_controller) =
+		tracing_unbounded("test_to_notifications", 10_000);
+	let (protocol_handle, protocol_controller) = ProtocolController::new(
+		SetId::from(0),
+		ProtoSetConfig {
+			reserved_nodes: {
+				(0..Uniform::new_inclusive(0, 2).sample(&mut rng))
+					.map(|_| {
+						let id = PeerId::random();
+						known_nodes.insert(id, State::Disconnected);
+						reserved_nodes.insert(id);
+						id
+					})
+					.collect()
+			},
+			in_peers: Uniform::new_inclusive(0, 25).sample(&mut rng),
+			out_peers: Uniform::new_inclusive(0, 25).sample(&mut rng),
+			reserved_only: Uniform::new_inclusive(0, 10).sample(&mut rng) == 0,
+		},
+		to_notifications,
+		Box::new(peer_store_handle.clone()),
+	);
+
+	tokio::spawn(peer_store.run());
+	tokio::spawn(protocol_controller.run());
+
+	// List of nodes the user of `peerset` assumes it's connected to. Always a subset of
+	// `known_nodes`.
+	let mut connected_nodes = HashSet::<PeerId>::new();
+	// List of nodes the user of `peerset` called `incoming` with and that haven't been
+	// accepted or rejected yet.
+	let mut incoming_nodes = HashMap::<IncomingIndex, PeerId>::new();
+	// Next id for incoming connections.
+	let mut next_incoming_id = IncomingIndex(0);
+
+	// The loop below is effectively synchronous, so for `PeerStore` & `ProtocolController`
+	// runners, spawned above, to advance, we use `spawn_blocking`.
+	let _ = tokio::task::spawn_blocking(move || {
+		// PRNG to use in `spawn_blocking` context.
+		let mut rng = rand::thread_rng();
+
+		// Perform a certain number of actions while checking that the state is consistent. If we
+		// reach the end of the loop, the run has succeeded.
+		// Note that with the ACKing and event postponing mechanism in `ProtocolController`
+		// the test time grows quadratically with the number of iterations below.
+		for _ in 0..2500 {
+			// Peer we are working with.
+			let mut current_peer = None;
+			// Current event for state transition validation.
+			let mut current_event = None;
+			// Last peer state for allowed event validation.
+			let mut last_state = None;
+
+			// Each of these weights corresponds to an action that we may perform.
+			let action_weights = [150, 90, 90, 30, 30, 1, 1, 4, 4];
+
+			match WeightedIndex::new(&action_weights).unwrap().sample(&mut rng) {
+				// If we generate 0, try to grab the next message from `ProtocolController`.
+				0 => match from_controller.next().now_or_never() {
+					Some(Some(Message::Connect { peer_id, .. })) => {
+						log::info!("PSM: connecting to peer {}", peer_id);
+
+						let state = known_nodes.get_mut(&peer_id).unwrap();
+						if matches!(*state, State::Incoming(_)) {
+							log::info!(
+								"Awaiting incoming response, ignoring obsolete Connect from PSM for peer {}",
+								peer_id,
+							);
+							continue
+						}
+
+						last_state = Some(*state);
+
+						if *state != State::Inbound {
+							*state = State::Outbound;
+						}
+
+						if !connected_nodes.insert(peer_id) {
+							log::info!("Request to connect to an already connected node {peer_id}");
+						}
+
+						current_peer = Some(peer_id);
+						current_event = Some(Event::PsmConnect);
+					},
+					Some(Some(Message::Drop { peer_id, .. })) => {
+						log::info!("PSM: dropping peer {}", peer_id);
+
+						let state = known_nodes.get_mut(&peer_id).unwrap();
+						if matches!(*state, State::Incoming(_)) {
+							log::info!(
+								"Awaiting incoming response, ignoring obsolete Drop from PSM for peer {}",
+								peer_id,
+							);
+							continue
+						}
+
+						last_state = Some(*state);
+						*state = State::Disconnected;
+
+						if !connected_nodes.remove(&peer_id) {
+							log::info!("Ignoring attempt to drop a disconnected peer {}", peer_id);
+						}
+
+						current_peer = Some(peer_id);
+						current_event = Some(Event::PsmDrop);
+					},
+					Some(Some(Message::Accept(n))) => {
+						log::info!("PSM: accepting index {}", n.0);
+
+						let peer_id = incoming_nodes.remove(&n).unwrap();
+
+						let state = known_nodes.get_mut(&peer_id).unwrap();
+						match *state {
+							State::Incoming(incoming_index) =>
+								if n.0 < incoming_index.0 {
+									log::info!(
+										"Ignoring obsolete Accept for {:?} while awaiting {:?} for peer {}",
+										n, incoming_index, peer_id,
+									);
+									continue
+								} else if n.0 > incoming_index.0 {
+									panic!(
+										"Received {:?} while awaiting {:?} for peer {}",
+										n, incoming_index, peer_id,
+									);
+								},
+							_ => {},
+						}
+
+						last_state = Some(*state);
+						*state = State::Inbound;
+
+						assert!(connected_nodes.insert(peer_id));
+
+						current_peer = Some(peer_id);
+						current_event = Some(Event::PsmAccept);
+					},
+					Some(Some(Message::Reject(n))) => {
+						log::info!("PSM: rejecting index {}", n.0);
+
+						let peer_id = incoming_nodes.remove(&n).unwrap();
+
+						let state = known_nodes.get_mut(&peer_id).unwrap();
+						match *state {
+							State::Incoming(incoming_index) =>
+								if n.0 < incoming_index.0 {
+									log::info!(
+										"Ignoring obsolete Reject for {:?} while awaiting {:?} for peer {}",
+										n, incoming_index, peer_id,
+									);
+									continue
+								} else if n.0 > incoming_index.0 {
+									panic!(
+										"Received {:?} while awaiting {:?} for peer {}",
+										n, incoming_index, peer_id,
+									);
+								},
+							_ => {},
+						}
+
+						last_state = Some(*state);
+						*state = State::Disconnected;
+
+						assert!(!connected_nodes.contains(&peer_id));
+
+						current_peer = Some(peer_id);
+						current_event = Some(Event::PsmReject);
+					},
+					Some(None) => panic!(),
+					None => {},
+				},
+
+				// If we generate 1, discover a new node.
+				1 => {
+					let new_id = PeerId::random();
+					known_nodes.insert(new_id, State::Disconnected);
+					peer_store_handle.add_known_peer(new_id);
+				},
+
+				// If we generate 2, adjust a random reputation.
+				2 =>
+					if let Some(id) = known_nodes.keys().choose(&mut rng) {
+						let val = Uniform::new_inclusive(i32::MIN, i32::MAX).sample(&mut rng);
+						peer_store_handle.report_peer(*id, ReputationChange::new(val, ""));
+					},
+
+				// If we generate 3, disconnect from a random node.
+				3 =>
+					if let Some(id) = connected_nodes.iter().choose(&mut rng).cloned() {
+						log::info!("Disconnected from {}", id);
+						connected_nodes.remove(&id);
+
+						let state = known_nodes.get_mut(&id).unwrap();
+						last_state = Some(*state);
+						*state = State::Disconnected;
+
+						protocol_handle.dropped(id);
+
+						current_peer = Some(id);
+						current_event = Some(Event::Disconnected);
+					},
+
+				// If we generate 4, connect to a random node.
+				4 => {
+					if let Some(id) = known_nodes
+						.keys()
+						.filter(|n| {
+							incoming_nodes.values().all(|m| m != *n) &&
+								!connected_nodes.contains(*n)
+						})
+						.choose(&mut rng)
+						.cloned()
+					{
+						log::info!("Incoming connection from {}, index {}", id, next_incoming_id.0);
+						protocol_handle.incoming_connection(id, next_incoming_id);
+						incoming_nodes.insert(next_incoming_id, id);
+
+						let state = known_nodes.get_mut(&id).unwrap();
+						last_state = Some(*state);
+						*state = State::Incoming(next_incoming_id);
+
+						next_incoming_id.0 += 1;
+
+						current_peer = Some(id);
+						current_event = Some(Event::Incoming);
+					}
+				},
+
+				// 5 and 6 are the reserved-only mode.
+				5 => {
+					log::info!("Set reserved only");
+					protocol_handle.set_reserved_only(true);
+				},
+				6 => {
+					log::info!("Unset reserved only");
+					protocol_handle.set_reserved_only(false);
+				},
+
+				// 7 and 8 are about switching a random node in or out of reserved mode.
+				7 => {
+					if let Some(id) =
+						known_nodes.keys().filter(|n| !reserved_nodes.contains(*n)).choose(&mut rng)
+					{
+						log::info!("Add reserved: {}", id);
+						protocol_handle.add_reserved_peer(*id);
+						reserved_nodes.insert(*id);
+					}
+				},
+				8 =>
+					if let Some(id) = reserved_nodes.iter().choose(&mut rng).cloned() {
+						log::info!("Remove reserved: {}", id);
+						reserved_nodes.remove(&id);
+						protocol_handle.remove_reserved_peer(id);
+					},
+
+				_ => unreachable!(),
+			}
+
+			// Validate state transitions.
+			if let Some(peer_id) = current_peer {
+				let event = current_event.unwrap();
+				let last_state = discard_incoming_index(last_state.unwrap());
+				if !allowed_events.get(&last_state).unwrap().contains(&event) {
+					panic!(
+						"Invalid state transition: {:?} x {:?} for peer {}",
+						last_state, event, peer_id,
+					);
+				}
+			}
+		}
+	})
+	.await;
+}
diff --git a/libp2p/test/src/lib.rs b/libp2p/test/src/lib.rs
new file mode 100644
index 000000000..aeed2985a
--- /dev/null
+++ b/libp2p/test/src/lib.rs
@@ -0,0 +1,1236 @@
+// 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/>.
+#![allow(missing_docs)]
+
+#[cfg(test)]
+mod block_import;
+#[cfg(test)]
+mod fuzz;
+#[cfg(test)]
+mod service;
+#[cfg(test)]
+mod sync;
+
+use std::{
+	collections::HashMap,
+	pin::Pin,
+	sync::Arc,
+	task::{Context as FutureContext, Poll},
+	time::Duration,
+};
+
+use futures::{channel::oneshot, future::BoxFuture, pin_mut, prelude::*};
+use libp2p::{build_multiaddr, PeerId};
+use log::trace;
+use parking_lot::Mutex;
+use sc_block_builder::{BlockBuilder, BlockBuilderBuilder};
+use sc_client_api::{
+	backend::{AuxStore, Backend, Finalizer},
+	BlockBackend, BlockImportNotification, BlockchainEvents, FinalityNotification,
+	FinalityNotifications, ImportNotifications,
+};
+use sc_consensus::{
+	BasicQueue, BlockCheckParams, BlockImport, BlockImportParams, BoxJustificationImport,
+	ForkChoiceStrategy, ImportQueue, ImportResult, JustificationImport, JustificationSyncLink,
+	LongestChain, Verifier,
+};
+use sc_network::{
+	config::{
+		FullNetworkConfiguration, MultiaddrWithPeerId, NetworkConfiguration, NonDefaultSetConfig,
+		NonReservedPeerMode, ProtocolId, Role, SyncMode, TransportConfig,
+	},
+	peer_store::PeerStore,
+	request_responses::ProtocolConfig as RequestResponseConfig,
+	types::ProtocolName,
+	Multiaddr, NetworkBlock, NetworkService, NetworkStateInfo, NetworkSyncForkRequest,
+	NetworkWorker, NotificationService,
+};
+use sc_network_common::role::Roles;
+use sc_network_light::light_client_requests::handler::LightClientRequestHandler;
+use sc_network_sync::{
+	block_request_handler::BlockRequestHandler,
+	service::{network::NetworkServiceProvider, syncing_service::SyncingService},
+	state_request_handler::StateRequestHandler,
+	strategy::warp::{
+		AuthorityList, EncodedProof, SetId, VerificationResult, WarpSyncParams, WarpSyncProvider,
+	},
+	warp_request_handler,
+};
+use sc_service::client::Client;
+use sp_blockchain::{
+	Backend as BlockchainBackend, HeaderBackend, Info as BlockchainInfo, Result as ClientResult,
+};
+use sp_consensus::{
+	block_validation::{BlockAnnounceValidator, DefaultBlockAnnounceValidator},
+	BlockOrigin, Error as ConsensusError, SyncOracle,
+};
+use sp_core::H256;
+use sp_runtime::{
+	codec::{Decode, Encode},
+	generic::BlockId,
+	traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero},
+	Justification, Justifications,
+};
+use substrate_test_runtime_client::AccountKeyring;
+pub use substrate_test_runtime_client::{
+	runtime::{Block, ExtrinsicBuilder, Hash, Header, Transfer},
+	TestClient, TestClientBuilder, TestClientBuilderExt,
+};
+use tokio::time::timeout;
+
+/// A Verifier that accepts all blocks and passes them on with the configured
+/// finality to be imported.
+#[derive(Clone)]
+pub struct PassThroughVerifier {
+	finalized: bool,
+}
+
+impl PassThroughVerifier {
+	/// Create a new instance.
+	///
+	/// Every verified block will use `finalized` for the `BlockImportParams`.
+	pub fn new(finalized: bool) -> Self {
+		Self { finalized }
+	}
+}
+
+/// This `Verifier` accepts all data as valid.
+#[async_trait::async_trait]
+impl<B: BlockT> Verifier<B> for PassThroughVerifier {
+	async fn verify(
+		&mut self,
+		mut block: BlockImportParams<B>,
+	) -> Result<BlockImportParams<B>, String> {
+		if block.fork_choice.is_none() {
+			block.fork_choice = Some(ForkChoiceStrategy::LongestChain);
+		};
+		block.finalized = self.finalized;
+		Ok(block)
+	}
+}
+
+pub type PeersFullClient = Client<
+	substrate_test_runtime_client::Backend,
+	substrate_test_runtime_client::ExecutorDispatch,
+	Block,
+	substrate_test_runtime_client::runtime::RuntimeApi,
+>;
+
+#[derive(Clone)]
+pub struct PeersClient {
+	client: Arc<PeersFullClient>,
+	backend: Arc<substrate_test_runtime_client::Backend>,
+}
+
+impl PeersClient {
+	pub fn as_client(&self) -> Arc<PeersFullClient> {
+		self.client.clone()
+	}
+
+	pub fn as_backend(&self) -> Arc<substrate_test_runtime_client::Backend> {
+		self.backend.clone()
+	}
+
+	pub fn as_block_import(&self) -> BlockImportAdapter<Self> {
+		BlockImportAdapter::new(self.clone())
+	}
+
+	pub fn get_aux(&self, key: &[u8]) -> ClientResult<Option<Vec<u8>>> {
+		self.client.get_aux(key)
+	}
+
+	pub fn info(&self) -> BlockchainInfo<Block> {
+		self.client.info()
+	}
+
+	pub fn header(
+		&self,
+		hash: <Block as BlockT>::Hash,
+	) -> ClientResult<Option<<Block as BlockT>::Header>> {
+		self.client.header(hash)
+	}
+
+	pub fn has_state_at(&self, block: &BlockId<Block>) -> bool {
+		let (number, hash) = match *block {
+			BlockId::Hash(h) => match self.as_client().number(h) {
+				Ok(Some(n)) => (n, h),
+				_ => return false,
+			},
+			BlockId::Number(n) => match self.as_client().hash(n) {
+				Ok(Some(h)) => (n, h),
+				_ => return false,
+			},
+		};
+		self.backend.have_state_at(hash, number)
+	}
+
+	pub fn justifications(
+		&self,
+		hash: <Block as BlockT>::Hash,
+	) -> ClientResult<Option<Justifications>> {
+		self.client.justifications(hash)
+	}
+
+	pub fn finality_notification_stream(&self) -> FinalityNotifications<Block> {
+		self.client.finality_notification_stream()
+	}
+
+	pub fn import_notification_stream(&self) -> ImportNotifications<Block> {
+		self.client.import_notification_stream()
+	}
+
+	pub fn finalize_block(
+		&self,
+		hash: <Block as BlockT>::Hash,
+		justification: Option<Justification>,
+		notify: bool,
+	) -> ClientResult<()> {
+		self.client.finalize_block(hash, justification, notify)
+	}
+}
+
+#[async_trait::async_trait]
+impl BlockImport<Block> for PeersClient {
+	type Error = ConsensusError;
+
+	async fn check_block(
+		&mut self,
+		block: BlockCheckParams<Block>,
+	) -> Result<ImportResult, Self::Error> {
+		self.client.check_block(block).await
+	}
+
+	async fn import_block(
+		&mut self,
+		block: BlockImportParams<Block>,
+	) -> Result<ImportResult, Self::Error> {
+		self.client.import_block(block).await
+	}
+}
+
+pub struct Peer<D, BlockImport> {
+	pub data: D,
+	client: PeersClient,
+	/// We keep a copy of the verifier so that we can invoke it for locally-generated blocks,
+	/// instead of going through the import queue.
+	verifier: VerifierAdapter<Block>,
+	/// We keep a copy of the block_import so that we can invoke it for locally-generated blocks,
+	/// instead of going through the import queue.
+	block_import: BlockImportAdapter<BlockImport>,
+	select_chain: Option<LongestChain<substrate_test_runtime_client::Backend, Block>>,
+	backend: Option<Arc<substrate_test_runtime_client::Backend>>,
+	network: NetworkWorker<Block, <Block as BlockT>::Hash>,
+	sync_service: Arc<SyncingService<Block>>,
+	imported_blocks_stream: Pin<Box<dyn Stream<Item = BlockImportNotification<Block>> + Send>>,
+	finality_notification_stream: Pin<Box<dyn Stream<Item = FinalityNotification<Block>> + Send>>,
+	listen_addr: Multiaddr,
+	notification_services: HashMap<ProtocolName, Box<dyn NotificationService>>,
+}
+
+impl<D, B> Peer<D, B>
+where
+	B: BlockImport<Block, Error = ConsensusError> + Send + Sync,
+{
+	/// Get this peer ID.
+	pub fn id(&self) -> PeerId {
+		self.network.service().local_peer_id()
+	}
+
+	/// Returns true if we're major syncing.
+	pub fn is_major_syncing(&self) -> bool {
+		self.sync_service.is_major_syncing()
+	}
+
+	// Returns a clone of the local SelectChain, only available on full nodes
+	pub fn select_chain(
+		&self,
+	) -> Option<LongestChain<substrate_test_runtime_client::Backend, Block>> {
+		self.select_chain.clone()
+	}
+
+	/// Returns the number of peers we're connected to.
+	pub async fn num_peers(&self) -> usize {
+		self.sync_service.status().await.unwrap().num_connected_peers as usize
+	}
+
+	/// Returns the number of downloaded blocks.
+	pub async fn num_downloaded_blocks(&self) -> usize {
+		self.sync_service.num_downloaded_blocks().await.unwrap()
+	}
+
+	/// Returns true if we have no peer.
+	pub fn is_offline(&self) -> bool {
+		self.sync_service.is_offline()
+	}
+
+	/// Request a justification for the given block.
+	pub fn request_justification(&self, hash: &<Block as BlockT>::Hash, number: NumberFor<Block>) {
+		self.sync_service.request_justification(hash, number);
+	}
+
+	/// Announces an important block on the network.
+	pub fn announce_block(&self, hash: <Block as BlockT>::Hash, data: Option<Vec<u8>>) {
+		self.sync_service.announce_block(hash, data);
+	}
+
+	/// Request explicit fork sync.
+	pub fn set_sync_fork_request(
+		&self,
+		peers: Vec<PeerId>,
+		hash: <Block as BlockT>::Hash,
+		number: NumberFor<Block>,
+	) {
+		self.sync_service.set_sync_fork_request(peers, hash, number);
+	}
+
+	/// Add blocks to the peer -- edit the block before adding
+	pub fn generate_blocks<F>(
+		&mut self,
+		count: usize,
+		origin: BlockOrigin,
+		edit_block: F,
+	) -> Vec<H256>
+	where
+		F: FnMut(BlockBuilder<Block, PeersFullClient>) -> Block,
+	{
+		let best_hash = self.client.info().best_hash;
+		self.generate_blocks_at(
+			BlockId::Hash(best_hash),
+			count,
+			origin,
+			edit_block,
+			false,
+			true,
+			true,
+			ForkChoiceStrategy::LongestChain,
+		)
+	}
+
+	/// Add blocks to the peer -- edit the block before adding and use custom fork choice rule.
+	pub fn generate_blocks_with_fork_choice<F>(
+		&mut self,
+		count: usize,
+		origin: BlockOrigin,
+		edit_block: F,
+		fork_choice: ForkChoiceStrategy,
+	) -> Vec<H256>
+	where
+		F: FnMut(BlockBuilder<Block, PeersFullClient>) -> Block,
+	{
+		let best_hash = self.client.info().best_hash;
+		self.generate_blocks_at(
+			BlockId::Hash(best_hash),
+			count,
+			origin,
+			edit_block,
+			false,
+			true,
+			true,
+			fork_choice,
+		)
+	}
+
+	/// Add blocks to the peer -- edit the block before adding. The chain will
+	/// start at the given block iD.
+	pub fn generate_blocks_at<F>(
+		&mut self,
+		at: BlockId<Block>,
+		count: usize,
+		origin: BlockOrigin,
+		mut edit_block: F,
+		headers_only: bool,
+		inform_sync_about_new_best_block: bool,
+		announce_block: bool,
+		fork_choice: ForkChoiceStrategy,
+	) -> Vec<H256>
+	where
+		F: FnMut(BlockBuilder<Block, PeersFullClient>) -> Block,
+	{
+		let mut hashes = Vec::with_capacity(count);
+		let full_client = self.client.as_client();
+		let mut at = full_client.block_hash_from_id(&at).unwrap().unwrap();
+		for _ in 0..count {
+			let builder = BlockBuilderBuilder::new(&*full_client)
+				.on_parent_block(at)
+				.fetch_parent_block_number(&*full_client)
+				.unwrap()
+				.build()
+				.unwrap();
+			let block = edit_block(builder);
+			let hash = block.header.hash();
+			trace!(
+				target: "test_network",
+				"Generating {}, (#{}, parent={})",
+				hash,
+				block.header.number,
+				block.header.parent_hash,
+			);
+			let header = block.header.clone();
+			let mut import_block = BlockImportParams::new(origin, header.clone());
+			import_block.body = if headers_only { None } else { Some(block.extrinsics) };
+			import_block.fork_choice = Some(fork_choice);
+			let import_block =
+				futures::executor::block_on(self.verifier.verify(import_block)).unwrap();
+
+			futures::executor::block_on(self.block_import.import_block(import_block))
+				.expect("block_import failed");
+			if announce_block {
+				self.sync_service.announce_block(hash, None);
+			}
+			hashes.push(hash);
+			at = hash;
+		}
+
+		if inform_sync_about_new_best_block {
+			self.sync_service.new_best_block_imported(
+				at,
+				*full_client.header(at).ok().flatten().unwrap().number(),
+			);
+		}
+		hashes
+	}
+
+	/// Push blocks to the peer (simplified: with or without a TX)
+	pub fn push_blocks(&mut self, count: usize, with_tx: bool) -> Vec<H256> {
+		let best_hash = self.client.info().best_hash;
+		self.push_blocks_at(BlockId::Hash(best_hash), count, with_tx)
+	}
+
+	/// Push blocks to the peer (simplified: with or without a TX)
+	pub fn push_headers(&mut self, count: usize) -> Vec<H256> {
+		let best_hash = self.client.info().best_hash;
+		self.generate_tx_blocks_at(BlockId::Hash(best_hash), count, false, true, true, true)
+	}
+
+	/// Push blocks to the peer (simplified: with or without a TX) starting from
+	/// given hash.
+	pub fn push_blocks_at(&mut self, at: BlockId<Block>, count: usize, with_tx: bool) -> Vec<H256> {
+		self.generate_tx_blocks_at(at, count, with_tx, false, true, true)
+	}
+
+	/// Push blocks to the peer (simplified: with or without a TX) starting from
+	/// given hash without informing the sync protocol about the new best block.
+	pub fn push_blocks_at_without_informing_sync(
+		&mut self,
+		at: BlockId<Block>,
+		count: usize,
+		with_tx: bool,
+		announce_block: bool,
+	) -> Vec<H256> {
+		self.generate_tx_blocks_at(at, count, with_tx, false, false, announce_block)
+	}
+
+	/// Push blocks to the peer (simplified: with or without a TX) starting from
+	/// given hash without announcing the block.
+	pub fn push_blocks_at_without_announcing(
+		&mut self,
+		at: BlockId<Block>,
+		count: usize,
+		with_tx: bool,
+	) -> Vec<H256> {
+		self.generate_tx_blocks_at(at, count, with_tx, false, true, false)
+	}
+
+	/// Push blocks/headers to the peer (simplified: with or without a TX) starting from
+	/// given hash.
+	fn generate_tx_blocks_at(
+		&mut self,
+		at: BlockId<Block>,
+		count: usize,
+		with_tx: bool,
+		headers_only: bool,
+		inform_sync_about_new_best_block: bool,
+		announce_block: bool,
+	) -> Vec<H256> {
+		let mut nonce = 0;
+		if with_tx {
+			self.generate_blocks_at(
+				at,
+				count,
+				BlockOrigin::File,
+				|mut builder| {
+					let transfer = Transfer {
+						from: AccountKeyring::Alice.into(),
+						to: AccountKeyring::Alice.into(),
+						amount: 1,
+						nonce,
+					};
+					builder.push(transfer.into_unchecked_extrinsic()).unwrap();
+					nonce += 1;
+					builder.build().unwrap().block
+				},
+				headers_only,
+				inform_sync_about_new_best_block,
+				announce_block,
+				ForkChoiceStrategy::LongestChain,
+			)
+		} else {
+			self.generate_blocks_at(
+				at,
+				count,
+				BlockOrigin::File,
+				|builder| builder.build().unwrap().block,
+				headers_only,
+				inform_sync_about_new_best_block,
+				announce_block,
+				ForkChoiceStrategy::LongestChain,
+			)
+		}
+	}
+
+	/// Get a reference to the client.
+	pub fn client(&self) -> &PeersClient {
+		&self.client
+	}
+
+	/// Get a reference to the network service.
+	pub fn network_service(&self) -> &Arc<NetworkService<Block, <Block as BlockT>::Hash>> {
+		self.network.service()
+	}
+
+	/// Get `SyncingService`.
+	pub fn sync_service(&self) -> &Arc<SyncingService<Block>> {
+		&self.sync_service
+	}
+
+	/// Take notification handle for enabled protocol.
+	pub fn take_notification_service(
+		&mut self,
+		protocol: &ProtocolName,
+	) -> Option<Box<dyn NotificationService>> {
+		self.notification_services.remove(protocol)
+	}
+
+	/// Get a reference to the network worker.
+	pub fn network(&self) -> &NetworkWorker<Block, <Block as BlockT>::Hash> {
+		&self.network
+	}
+
+	/// Test helper to compare the blockchain state of multiple (networked)
+	/// clients.
+	pub fn blockchain_canon_equals(&self, other: &Self) -> bool {
+		if let (Some(mine), Some(others)) = (self.backend.clone(), other.backend.clone()) {
+			mine.blockchain().info().best_hash == others.blockchain().info().best_hash
+		} else {
+			false
+		}
+	}
+
+	/// Count the total number of imported blocks.
+	pub fn blocks_count(&self) -> u64 {
+		self.backend
+			.as_ref()
+			.map(|backend| backend.blockchain().info().best_number)
+			.unwrap_or(0)
+	}
+
+	/// Return a collection of block hashes that failed verification
+	pub fn failed_verifications(&self) -> HashMap<<Block as BlockT>::Hash, String> {
+		self.verifier.failed_verifications.lock().clone()
+	}
+
+	pub fn has_block(&self, hash: H256) -> bool {
+		self.backend
+			.as_ref()
+			.map(|backend| backend.blockchain().header(hash).unwrap().is_some())
+			.unwrap_or(false)
+	}
+
+	pub fn has_body(&self, hash: H256) -> bool {
+		self.backend
+			.as_ref()
+			.map(|backend| backend.blockchain().body(hash).unwrap().is_some())
+			.unwrap_or(false)
+	}
+}
+
+pub trait BlockImportAdapterFull:
+	BlockImport<Block, Error = ConsensusError> + Send + Sync + Clone
+{
+}
+
+impl<T> BlockImportAdapterFull for T where
+	T: BlockImport<Block, Error = ConsensusError> + Send + Sync + Clone
+{
+}
+
+/// Implements `BlockImport` for any `Transaction`. Internally the transaction is
+/// "converted", aka the field is set to `None`.
+///
+/// This is required as the `TestNetFactory` trait does not distinguish between
+/// full and light nodes.
+#[derive(Clone)]
+pub struct BlockImportAdapter<I> {
+	inner: I,
+}
+
+impl<I> BlockImportAdapter<I> {
+	/// Create a new instance of `Self::Full`.
+	pub fn new(inner: I) -> Self {
+		Self { inner }
+	}
+}
+
+#[async_trait::async_trait]
+impl<I> BlockImport<Block> for BlockImportAdapter<I>
+where
+	I: BlockImport<Block, Error = ConsensusError> + Send + Sync,
+{
+	type Error = ConsensusError;
+
+	async fn check_block(
+		&mut self,
+		block: BlockCheckParams<Block>,
+	) -> Result<ImportResult, Self::Error> {
+		self.inner.check_block(block).await
+	}
+
+	async fn import_block(
+		&mut self,
+		block: BlockImportParams<Block>,
+	) -> Result<ImportResult, Self::Error> {
+		self.inner.import_block(block).await
+	}
+}
+
+/// Implements `Verifier` and keeps track of failed verifications.
+struct VerifierAdapter<B: BlockT> {
+	verifier: Arc<futures::lock::Mutex<Box<dyn Verifier<B>>>>,
+	failed_verifications: Arc<Mutex<HashMap<B::Hash, String>>>,
+}
+
+#[async_trait::async_trait]
+impl<B: BlockT> Verifier<B> for VerifierAdapter<B> {
+	async fn verify(
+		&mut self,
+		block: BlockImportParams<B>,
+	) -> Result<BlockImportParams<B>, String> {
+		let hash = block.header.hash();
+		self.verifier.lock().await.verify(block).await.map_err(|e| {
+			self.failed_verifications.lock().insert(hash, e.clone());
+			e
+		})
+	}
+}
+
+impl<B: BlockT> Clone for VerifierAdapter<B> {
+	fn clone(&self) -> Self {
+		Self {
+			verifier: self.verifier.clone(),
+			failed_verifications: self.failed_verifications.clone(),
+		}
+	}
+}
+
+impl<B: BlockT> VerifierAdapter<B> {
+	fn new(verifier: impl Verifier<B> + 'static) -> Self {
+		VerifierAdapter {
+			verifier: Arc::new(futures::lock::Mutex::new(Box::new(verifier))),
+			failed_verifications: Default::default(),
+		}
+	}
+}
+
+struct TestWarpSyncProvider<B: BlockT>(Arc<dyn HeaderBackend<B>>);
+
+impl<B: BlockT> WarpSyncProvider<B> for TestWarpSyncProvider<B> {
+	fn generate(
+		&self,
+		_start: B::Hash,
+	) -> Result<EncodedProof, Box<dyn std::error::Error + Send + Sync>> {
+		let info = self.0.info();
+		let best_header = self.0.header(info.best_hash).unwrap().unwrap();
+		Ok(EncodedProof(best_header.encode()))
+	}
+	fn verify(
+		&self,
+		proof: &EncodedProof,
+		_set_id: SetId,
+		_authorities: AuthorityList,
+	) -> Result<VerificationResult<B>, Box<dyn std::error::Error + Send + Sync>> {
+		let EncodedProof(encoded) = proof;
+		let header = B::Header::decode(&mut encoded.as_slice()).unwrap();
+		Ok(VerificationResult::Complete(0, Default::default(), header))
+	}
+	fn current_authorities(&self) -> AuthorityList {
+		Default::default()
+	}
+}
+
+/// Configuration for a full peer.
+#[derive(Default)]
+pub struct FullPeerConfig {
+	/// Pruning window size.
+	///
+	/// NOTE: only finalized blocks are subject for removal!
+	pub blocks_pruning: Option<u32>,
+	/// Block announce validator.
+	pub block_announce_validator: Option<Box<dyn BlockAnnounceValidator<Block> + Send + Sync>>,
+	/// List of notification protocols that the network must support.
+	pub notifications_protocols: Vec<ProtocolName>,
+	/// List of request-response protocols that the network must support.
+	pub request_response_protocols: Vec<RequestResponseConfig>,
+	/// The indices of the peers the peer should be connected to.
+	///
+	/// If `None`, it will be connected to all other peers.
+	pub connect_to_peers: Option<Vec<usize>>,
+	/// Whether the full peer should have the authority role.
+	pub is_authority: bool,
+	/// Syncing mode
+	pub sync_mode: SyncMode,
+	/// Extra genesis storage.
+	pub extra_storage: Option<sp_core::storage::Storage>,
+	/// Enable transaction indexing.
+	pub storage_chain: bool,
+	/// Optional target block header to sync to
+	pub target_block: Option<<Block as BlockT>::Header>,
+	/// Force genesis even in case of warp & light state sync.
+	pub force_genesis: bool,
+}
+
+#[async_trait::async_trait]
+pub trait TestNetFactory: Default + Sized + Send {
+	type Verifier: 'static + Verifier<Block>;
+	type BlockImport: BlockImport<Block, Error = ConsensusError> + Clone + Send + Sync + 'static;
+	type PeerData: Default + Send;
+
+	/// This one needs to be implemented!
+	fn make_verifier(&self, client: PeersClient, peer_data: &Self::PeerData) -> Self::Verifier;
+
+	/// Get reference to peer.
+	fn peer(&mut self, i: usize) -> &mut Peer<Self::PeerData, Self::BlockImport>;
+	fn peers(&self) -> &Vec<Peer<Self::PeerData, Self::BlockImport>>;
+	fn peers_mut(&mut self) -> &mut Vec<Peer<Self::PeerData, Self::BlockImport>>;
+	fn mut_peers<F: FnOnce(&mut Vec<Peer<Self::PeerData, Self::BlockImport>>)>(
+		&mut self,
+		closure: F,
+	);
+
+	/// Get custom block import handle for fresh client, along with peer data.
+	fn make_block_import(
+		&self,
+		client: PeersClient,
+	) -> (
+		BlockImportAdapter<Self::BlockImport>,
+		Option<BoxJustificationImport<Block>>,
+		Self::PeerData,
+	);
+
+	/// Create new test network with this many peers.
+	fn new(n: usize) -> Self {
+		trace!(target: "test_network", "Creating test network");
+		let mut net = Self::default();
+
+		for i in 0..n {
+			trace!(target: "test_network", "Adding peer {}", i);
+			net.add_full_peer();
+		}
+		net
+	}
+
+	fn add_full_peer(&mut self) {
+		self.add_full_peer_with_config(Default::default())
+	}
+
+	/// Add a full peer.
+	fn add_full_peer_with_config(&mut self, config: FullPeerConfig) {
+		let mut test_client_builder = match (config.blocks_pruning, config.storage_chain) {
+			(Some(blocks_pruning), true) => TestClientBuilder::with_tx_storage(blocks_pruning),
+			(None, true) => TestClientBuilder::with_tx_storage(u32::MAX),
+			(Some(blocks_pruning), false) => TestClientBuilder::with_pruning_window(blocks_pruning),
+			(None, false) => TestClientBuilder::with_default_backend(),
+		};
+		if let Some(storage) = config.extra_storage {
+			let genesis_extra_storage = test_client_builder.genesis_init_mut().extra_storage();
+			*genesis_extra_storage = storage;
+		}
+
+		if !config.force_genesis &&
+			matches!(config.sync_mode, SyncMode::LightState { .. } | SyncMode::Warp)
+		{
+			test_client_builder = test_client_builder.set_no_genesis();
+		}
+		let backend = test_client_builder.backend();
+		let (c, longest_chain) = test_client_builder.build_with_longest_chain();
+		let client = Arc::new(c);
+
+		let (block_import, justification_import, data) = self
+			.make_block_import(PeersClient { client: client.clone(), backend: backend.clone() });
+
+		let verifier = self
+			.make_verifier(PeersClient { client: client.clone(), backend: backend.clone() }, &data);
+		let verifier = VerifierAdapter::new(verifier);
+
+		let import_queue = Box::new(BasicQueue::new(
+			verifier.clone(),
+			Box::new(block_import.clone()),
+			justification_import,
+			&sp_core::testing::TaskExecutor::new(),
+			None,
+		));
+
+		let listen_addr = build_multiaddr![Memory(rand::random::<u64>())];
+
+		let mut network_config =
+			NetworkConfiguration::new("test-node", "test-client", Default::default(), None);
+		network_config.sync_mode = config.sync_mode;
+		network_config.transport = TransportConfig::MemoryOnly;
+		network_config.listen_addresses = vec![listen_addr.clone()];
+		network_config.allow_non_globals_in_dht = true;
+
+		let (notif_configs, notif_handles): (Vec<_>, Vec<_>) = config
+			.notifications_protocols
+			.into_iter()
+			.map(|p| {
+				let (config, handle) = NonDefaultSetConfig::new(
+					p.clone(),
+					Vec::new(),
+					1024 * 1024,
+					None,
+					Default::default(),
+				);
+
+				(config, (p, handle))
+			})
+			.unzip();
+
+		if let Some(connect_to) = config.connect_to_peers {
+			let addrs = connect_to
+				.iter()
+				.map(|v| {
+					let peer_id = self.peer(*v).network_service().local_peer_id();
+					let multiaddr = self.peer(*v).listen_addr.clone();
+					MultiaddrWithPeerId { peer_id, multiaddr }
+				})
+				.collect();
+			network_config.default_peers_set.reserved_nodes = addrs;
+			network_config.default_peers_set.non_reserved_mode = NonReservedPeerMode::Deny;
+		}
+		let mut full_net_config = FullNetworkConfiguration::new(&network_config);
+
+		let protocol_id = ProtocolId::from("test-protocol-name");
+
+		let fork_id = Some(String::from("test-fork-id"));
+
+		let (chain_sync_network_provider, chain_sync_network_handle) =
+			NetworkServiceProvider::new();
+		let mut block_relay_params = BlockRequestHandler::new(
+			chain_sync_network_handle.clone(),
+			&protocol_id,
+			None,
+			client.clone(),
+			50,
+		);
+		self.spawn_task(Box::pin(async move {
+			block_relay_params.server.run().await;
+		}));
+
+		let state_request_protocol_config = {
+			let (handler, protocol_config) =
+				StateRequestHandler::new(&protocol_id, None, client.clone(), 50);
+			self.spawn_task(handler.run().boxed());
+			protocol_config
+		};
+
+		let light_client_request_protocol_config = {
+			let (handler, protocol_config) =
+				LightClientRequestHandler::new(&protocol_id, None, client.clone());
+			self.spawn_task(handler.run().boxed());
+			protocol_config
+		};
+
+		let warp_sync = Arc::new(TestWarpSyncProvider(client.clone()));
+
+		let warp_sync_params = match config.target_block {
+			Some(target_block) => {
+				let (sender, receiver) = oneshot::channel::<<Block as BlockT>::Header>();
+				let _ = sender.send(target_block);
+				WarpSyncParams::WaitForTarget(receiver)
+			},
+			_ => WarpSyncParams::WithProvider(warp_sync.clone()),
+		};
+
+		let warp_protocol_config = {
+			let (handler, protocol_config) = warp_request_handler::RequestHandler::new(
+				protocol_id.clone(),
+				client
+					.block_hash(0u32.into())
+					.ok()
+					.flatten()
+					.expect("Genesis block exists; qed"),
+				None,
+				warp_sync.clone(),
+			);
+			self.spawn_task(handler.run().boxed());
+			protocol_config
+		};
+
+		let peer_store = PeerStore::new(
+			network_config.boot_nodes.iter().map(|bootnode| bootnode.peer_id).collect(),
+		);
+		let peer_store_handle = peer_store.handle();
+		self.spawn_task(peer_store.run().boxed());
+
+		let block_announce_validator = config
+			.block_announce_validator
+			.unwrap_or_else(|| Box::new(DefaultBlockAnnounceValidator));
+
+		let (engine, sync_service, block_announce_config) =
+			sc_network_sync::engine::SyncingEngine::new(
+				Roles::from(if config.is_authority { &Role::Authority } else { &Role::Full }),
+				client.clone(),
+				None,
+				&full_net_config,
+				protocol_id.clone(),
+				&fork_id,
+				block_announce_validator,
+				Some(warp_sync_params),
+				chain_sync_network_handle,
+				import_queue.service(),
+				block_relay_params.downloader,
+				state_request_protocol_config.name.clone(),
+				Some(warp_protocol_config.name.clone()),
+				peer_store_handle.clone(),
+			)
+			.unwrap();
+		let sync_service_import_queue = Box::new(sync_service.clone());
+		let sync_service = Arc::new(sync_service.clone());
+
+		for config in config.request_response_protocols {
+			full_net_config.add_request_response_protocol(config);
+		}
+		for config in [
+			block_relay_params.request_response_config,
+			state_request_protocol_config,
+			light_client_request_protocol_config,
+			warp_protocol_config,
+		] {
+			full_net_config.add_request_response_protocol(config);
+		}
+
+		for config in notif_configs {
+			full_net_config.add_notification_protocol(config);
+		}
+
+		let genesis_hash =
+			client.hash(Zero::zero()).ok().flatten().expect("Genesis block exists; qed");
+		let network = NetworkWorker::new(sc_network::config::Params {
+			role: if config.is_authority { Role::Authority } else { Role::Full },
+			executor: Box::new(|f| {
+				tokio::spawn(f);
+			}),
+			network_config: full_net_config,
+			peer_store: peer_store_handle,
+			genesis_hash,
+			protocol_id,
+			fork_id,
+			metrics_registry: None,
+			block_announce_config,
+		})
+		.unwrap();
+
+		trace!(target: "test_network", "Peer identifier: {}", network.service().local_peer_id());
+
+		let service = network.service().clone();
+		tokio::spawn(async move {
+			chain_sync_network_provider.run(service).await;
+		});
+
+		tokio::spawn(async move {
+			import_queue.run(sync_service_import_queue).await;
+		});
+
+		tokio::spawn(async move {
+			engine.run().await;
+		});
+
+		self.mut_peers(move |peers| {
+			for peer in peers.iter_mut() {
+				peer.network
+					.add_known_address(network.service().local_peer_id(), listen_addr.clone());
+			}
+
+			let imported_blocks_stream = Box::pin(client.import_notification_stream().fuse());
+			let finality_notification_stream =
+				Box::pin(client.finality_notification_stream().fuse());
+
+			peers.push(Peer {
+				data,
+				client: PeersClient { client: client.clone(), backend: backend.clone() },
+				select_chain: Some(longest_chain),
+				backend: Some(backend),
+				imported_blocks_stream,
+				finality_notification_stream,
+				notification_services: HashMap::from_iter(notif_handles.into_iter()),
+				block_import,
+				verifier,
+				network,
+				sync_service,
+				listen_addr,
+			});
+		});
+	}
+
+	/// Used to spawn background tasks, e.g. the block request protocol handler.
+	fn spawn_task(&self, f: BoxFuture<'static, ()>) {
+		tokio::spawn(f);
+	}
+
+	async fn is_in_sync(&mut self) -> bool {
+		let mut highest = None;
+		let peers = self.peers_mut();
+
+		for peer in peers {
+			if peer.sync_service.is_major_syncing() ||
+				peer.sync_service.num_queued_blocks().await.unwrap() != 0
+			{
+				return false
+			}
+			if peer.sync_service.num_sync_requests().await.unwrap() != 0 {
+				return false
+			}
+			match (highest, peer.client.info().best_hash) {
+				(None, b) => highest = Some(b),
+				(Some(ref a), ref b) if a == b => {},
+				(Some(_), _) => return false,
+			}
+		}
+
+		true
+	}
+
+	async fn is_idle(&mut self) -> bool {
+		let peers = self.peers_mut();
+		for peer in peers {
+			if peer.sync_service.num_queued_blocks().await.unwrap() != 0 {
+				return false
+			}
+			if peer.sync_service.num_sync_requests().await.unwrap() != 0 {
+				return false
+			}
+		}
+
+		true
+	}
+
+	/// Blocks the current thread until we are sync'ed.
+	/// Wait until we are sync'ed.
+	///
+	/// (If we've not synced within 10 mins then panic rather than hang.)
+	async fn run_until_sync(&mut self) {
+		timeout(Duration::from_secs(10 * 60), async {
+			loop {
+				futures::future::poll_fn::<(), _>(|cx| {
+					self.poll(cx);
+					Poll::Ready(())
+				})
+				.await;
+
+				if self.is_in_sync().await {
+					break
+				}
+			}
+		})
+		.await
+		.expect("sync didn't happen within 10 mins");
+	}
+
+	/// Run the network until there are no pending packets.
+	///
+	/// Calls `poll_until_idle` repeatedly with the runtime passed as parameter.
+	async fn run_until_idle(&mut self) {
+		loop {
+			futures::future::poll_fn::<(), _>(|cx| {
+				self.poll(cx);
+				Poll::Ready(())
+			})
+			.await;
+
+			if self.is_idle().await {
+				break
+			}
+		}
+	}
+
+	/// Run the network until all peers are connected to each other.
+	async fn run_until_connected(&mut self) {
+		let num_peers = self.peers().len();
+		let sync_services =
+			self.peers().iter().map(|info| info.sync_service.clone()).collect::<Vec<_>>();
+
+		'outer: loop {
+			for sync_service in &sync_services {
+				if sync_service.status().await.unwrap().num_connected_peers as usize !=
+					num_peers - 1
+				{
+					futures::future::poll_fn::<(), _>(|cx| {
+						self.poll(cx);
+						Poll::Ready(())
+					})
+					.await;
+					continue 'outer
+				}
+			}
+
+			break
+		}
+	}
+
+	/// Polls the testnet. Processes all the pending actions.
+	fn poll(&mut self, cx: &mut FutureContext) {
+		self.mut_peers(|peers| {
+			for (i, peer) in peers.iter_mut().enumerate() {
+				trace!(target: "sync", "-- Polling {}: {}", i, peer.id());
+				loop {
+					// The code below is not quite correct, because we are polling a different
+					// instance of the future every time. But as long as
+					// `NetworkWorker::next_action()` contains just streams polling not interleaved
+					// with other `.await`s, dropping the future and recreating it works the same as
+					// polling a single instance.
+					let net_poll_future = peer.network.next_action();
+					pin_mut!(net_poll_future);
+					if let Poll::Pending = net_poll_future.poll(cx) {
+						break
+					}
+				}
+				trace!(target: "sync", "-- Polling complete {}: {}", i, peer.id());
+
+				// We poll `imported_blocks_stream`.
+				while let Poll::Ready(Some(notification)) =
+					peer.imported_blocks_stream.as_mut().poll_next(cx)
+				{
+					peer.sync_service.announce_block(notification.hash, None);
+				}
+
+				// We poll `finality_notification_stream`.
+				while let Poll::Ready(Some(notification)) =
+					peer.finality_notification_stream.as_mut().poll_next(cx)
+				{
+					peer.sync_service.on_block_finalized(notification.hash, notification.header);
+				}
+			}
+		});
+	}
+}
+
+#[derive(Default)]
+pub struct TestNet {
+	peers: Vec<Peer<(), PeersClient>>,
+}
+
+impl TestNetFactory for TestNet {
+	type Verifier = PassThroughVerifier;
+	type PeerData = ();
+	type BlockImport = PeersClient;
+
+	fn make_verifier(&self, _client: PeersClient, _peer_data: &()) -> Self::Verifier {
+		PassThroughVerifier::new(false)
+	}
+
+	fn make_block_import(
+		&self,
+		client: PeersClient,
+	) -> (
+		BlockImportAdapter<Self::BlockImport>,
+		Option<BoxJustificationImport<Block>>,
+		Self::PeerData,
+	) {
+		(client.as_block_import(), None, ())
+	}
+
+	fn peer(&mut self, i: usize) -> &mut Peer<(), Self::BlockImport> {
+		&mut self.peers[i]
+	}
+
+	fn peers(&self) -> &Vec<Peer<(), Self::BlockImport>> {
+		&self.peers
+	}
+
+	fn peers_mut(&mut self) -> &mut Vec<Peer<(), Self::BlockImport>> {
+		&mut self.peers
+	}
+
+	fn mut_peers<F: FnOnce(&mut Vec<Peer<(), Self::BlockImport>>)>(&mut self, closure: F) {
+		closure(&mut self.peers);
+	}
+}
+
+pub struct ForceFinalized(PeersClient);
+
+#[async_trait::async_trait]
+impl JustificationImport<Block> for ForceFinalized {
+	type Error = ConsensusError;
+
+	async fn on_start(&mut self) -> Vec<(H256, NumberFor<Block>)> {
+		Vec::new()
+	}
+
+	async fn import_justification(
+		&mut self,
+		hash: H256,
+		_number: NumberFor<Block>,
+		justification: Justification,
+	) -> Result<(), Self::Error> {
+		self.0
+			.finalize_block(hash, Some(justification), true)
+			.map_err(|_| ConsensusError::InvalidJustification)
+	}
+}
+
+#[derive(Default)]
+pub struct JustificationTestNet(TestNet);
+
+impl TestNetFactory for JustificationTestNet {
+	type Verifier = PassThroughVerifier;
+	type PeerData = ();
+	type BlockImport = PeersClient;
+
+	fn make_verifier(&self, client: PeersClient, peer_data: &()) -> Self::Verifier {
+		self.0.make_verifier(client, peer_data)
+	}
+
+	fn peer(&mut self, i: usize) -> &mut Peer<Self::PeerData, Self::BlockImport> {
+		self.0.peer(i)
+	}
+
+	fn peers(&self) -> &Vec<Peer<Self::PeerData, Self::BlockImport>> {
+		self.0.peers()
+	}
+
+	fn peers_mut(&mut self) -> &mut Vec<Peer<Self::PeerData, Self::BlockImport>> {
+		self.0.peers_mut()
+	}
+
+	fn mut_peers<F: FnOnce(&mut Vec<Peer<Self::PeerData, Self::BlockImport>>)>(
+		&mut self,
+		closure: F,
+	) {
+		self.0.mut_peers(closure)
+	}
+
+	fn make_block_import(
+		&self,
+		client: PeersClient,
+	) -> (
+		BlockImportAdapter<Self::BlockImport>,
+		Option<BoxJustificationImport<Block>>,
+		Self::PeerData,
+	) {
+		(client.as_block_import(), Some(Box::new(ForceFinalized(client))), Default::default())
+	}
+}
diff --git a/libp2p/test/src/service.rs b/libp2p/test/src/service.rs
new file mode 100644
index 000000000..800c0d436
--- /dev/null
+++ b/libp2p/test/src/service.rs
@@ -0,0 +1,818 @@
+// 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 futures::prelude::*;
+use libp2p::{Multiaddr, PeerId};
+
+use sc_consensus::{ImportQueue, Link};
+use sc_network::{
+	config::{self, FullNetworkConfiguration, MultiaddrWithPeerId, ProtocolId, TransportConfig},
+	event::Event,
+	peer_store::PeerStore,
+	service::traits::{NotificationEvent, ValidationResult},
+	NetworkEventStream, NetworkPeers, NetworkService, NetworkStateInfo, NetworkWorker,
+	NotificationService,
+};
+use sc_network_common::role::Roles;
+use sc_network_light::light_client_requests::handler::LightClientRequestHandler;
+use sc_network_sync::{
+	block_request_handler::BlockRequestHandler,
+	engine::SyncingEngine,
+	service::network::{NetworkServiceHandle, NetworkServiceProvider},
+	state_request_handler::StateRequestHandler,
+};
+use sp_blockchain::HeaderBackend;
+use sp_runtime::traits::{Block as BlockT, Zero};
+use substrate_test_runtime_client::{
+	runtime::{Block as TestBlock, Hash as TestHash},
+	TestClientBuilder, TestClientBuilderExt as _,
+};
+
+use std::{sync::Arc, time::Duration};
+
+type TestNetworkWorker = NetworkWorker<TestBlock, TestHash>;
+type TestNetworkService = NetworkService<TestBlock, TestHash>;
+
+const PROTOCOL_NAME: &str = "/foo";
+
+struct TestNetwork {
+	network: TestNetworkWorker,
+}
+
+impl TestNetwork {
+	pub fn new(network: TestNetworkWorker) -> Self {
+		Self { network }
+	}
+
+	pub fn start_network(
+		self,
+	) -> (Arc<TestNetworkService>, (impl Stream<Item = Event> + std::marker::Unpin)) {
+		let worker = self.network;
+		let service = worker.service().clone();
+		let event_stream = service.event_stream("test");
+
+		tokio::spawn(worker.run());
+
+		(service, event_stream)
+	}
+}
+
+struct TestNetworkBuilder {
+	import_queue: Option<Box<dyn ImportQueue<TestBlock>>>,
+	link: Option<Box<dyn Link<TestBlock>>>,
+	client: Option<Arc<substrate_test_runtime_client::TestClient>>,
+	listen_addresses: Vec<Multiaddr>,
+	set_config: Option<config::SetConfig>,
+	chain_sync_network: Option<(NetworkServiceProvider, NetworkServiceHandle)>,
+	notification_protocols: Vec<config::NonDefaultSetConfig>,
+	config: Option<config::NetworkConfiguration>,
+}
+
+impl TestNetworkBuilder {
+	pub fn new() -> Self {
+		Self {
+			import_queue: None,
+			link: None,
+			client: None,
+			listen_addresses: Vec::new(),
+			set_config: None,
+			chain_sync_network: None,
+			notification_protocols: Vec::new(),
+			config: None,
+		}
+	}
+
+	pub fn with_config(mut self, config: config::NetworkConfiguration) -> Self {
+		self.config = Some(config);
+		self
+	}
+
+	pub fn with_notification_protocol(mut self, config: config::NonDefaultSetConfig) -> Self {
+		self.notification_protocols.push(config);
+		self
+	}
+
+	pub fn with_listen_addresses(mut self, addresses: Vec<Multiaddr>) -> Self {
+		self.listen_addresses = addresses;
+		self
+	}
+
+	pub fn with_set_config(mut self, set_config: config::SetConfig) -> Self {
+		self.set_config = Some(set_config);
+		self
+	}
+
+	pub fn build(mut self) -> (TestNetwork, Option<Box<dyn NotificationService>>) {
+		let client = self.client.as_mut().map_or(
+			Arc::new(TestClientBuilder::with_default_backend().build_with_longest_chain().0),
+			|v| v.clone(),
+		);
+
+		let network_config = self.config.unwrap_or(config::NetworkConfiguration {
+			listen_addresses: self.listen_addresses,
+			transport: TransportConfig::MemoryOnly,
+			..config::NetworkConfiguration::new_local()
+		});
+
+		#[derive(Clone)]
+		struct PassThroughVerifier(bool);
+
+		#[async_trait::async_trait]
+		impl<B: BlockT> sc_consensus::Verifier<B> for PassThroughVerifier {
+			async fn verify(
+				&mut self,
+				mut block: sc_consensus::BlockImportParams<B>,
+			) -> Result<sc_consensus::BlockImportParams<B>, String> {
+				block.finalized = self.0;
+				block.fork_choice = Some(sc_consensus::ForkChoiceStrategy::LongestChain);
+				Ok(block)
+			}
+		}
+
+		let mut import_queue =
+			self.import_queue.unwrap_or(Box::new(sc_consensus::BasicQueue::new(
+				PassThroughVerifier(false),
+				Box::new(client.clone()),
+				None,
+				&sp_core::testing::TaskExecutor::new(),
+				None,
+			)));
+
+		let protocol_id = ProtocolId::from("test-protocol-name");
+		let fork_id = Some(String::from("test-fork-id"));
+		let mut full_net_config = FullNetworkConfiguration::new(&network_config);
+
+		let (chain_sync_network_provider, chain_sync_network_handle) =
+			self.chain_sync_network.unwrap_or(NetworkServiceProvider::new());
+		let mut block_relay_params = BlockRequestHandler::new(
+			chain_sync_network_handle.clone(),
+			&protocol_id,
+			None,
+			client.clone(),
+			50,
+		);
+		tokio::spawn(Box::pin(async move {
+			block_relay_params.server.run().await;
+		}));
+
+		let state_request_protocol_config = {
+			let (handler, protocol_config) =
+				StateRequestHandler::new(&protocol_id, None, client.clone(), 50);
+			tokio::spawn(handler.run().boxed());
+			protocol_config
+		};
+
+		let light_client_request_protocol_config = {
+			let (handler, protocol_config) =
+				LightClientRequestHandler::new(&protocol_id, None, client.clone());
+			tokio::spawn(handler.run().boxed());
+			protocol_config
+		};
+
+		let peer_store = PeerStore::new(
+			network_config.boot_nodes.iter().map(|bootnode| bootnode.peer_id).collect(),
+		);
+		let peer_store_handle = peer_store.handle();
+		tokio::spawn(peer_store.run().boxed());
+
+		let (engine, chain_sync_service, block_announce_config) = SyncingEngine::new(
+			Roles::from(&config::Role::Full),
+			client.clone(),
+			None,
+			&full_net_config,
+			protocol_id.clone(),
+			&None,
+			Box::new(sp_consensus::block_validation::DefaultBlockAnnounceValidator),
+			None,
+			chain_sync_network_handle,
+			import_queue.service(),
+			block_relay_params.downloader,
+			state_request_protocol_config.name.clone(),
+			None,
+			peer_store_handle.clone(),
+		)
+		.unwrap();
+		let mut link = self.link.unwrap_or(Box::new(chain_sync_service.clone()));
+
+		let handle = if !self.notification_protocols.is_empty() {
+			for config in self.notification_protocols {
+				full_net_config.add_notification_protocol(config);
+			}
+			None
+		} else {
+			let (config, handle) = config::NonDefaultSetConfig::new(
+				PROTOCOL_NAME.into(),
+				Vec::new(),
+				1024 * 1024,
+				None,
+				self.set_config.unwrap_or_default(),
+			);
+			full_net_config.add_notification_protocol(config);
+			Some(handle)
+		};
+
+		for config in [
+			block_relay_params.request_response_config,
+			state_request_protocol_config,
+			light_client_request_protocol_config,
+		] {
+			full_net_config.add_request_response_protocol(config);
+		}
+
+		let genesis_hash =
+			client.hash(Zero::zero()).ok().flatten().expect("Genesis block exists; qed");
+		let worker = NetworkWorker::<
+			substrate_test_runtime_client::runtime::Block,
+			substrate_test_runtime_client::runtime::Hash,
+		>::new(config::Params::<substrate_test_runtime_client::runtime::Block> {
+			block_announce_config,
+			role: config::Role::Full,
+			executor: Box::new(|f| {
+				tokio::spawn(f);
+			}),
+			genesis_hash,
+			network_config: full_net_config,
+			peer_store: peer_store_handle,
+			protocol_id,
+			fork_id,
+			metrics_registry: None,
+		})
+		.unwrap();
+
+		let service = worker.service().clone();
+		tokio::spawn(async move {
+			let _ = chain_sync_network_provider.run(service).await;
+		});
+		tokio::spawn(async move {
+			loop {
+				futures::future::poll_fn(|cx| {
+					import_queue.poll_actions(cx, &mut *link);
+					std::task::Poll::Ready(())
+				})
+				.await;
+				tokio::time::sleep(std::time::Duration::from_millis(250)).await;
+			}
+		});
+		tokio::spawn(engine.run());
+
+		(TestNetwork::new(worker), handle)
+	}
+}
+
+/// Builds two nodes and their associated events stream.
+/// The nodes are connected together and have the `PROTOCOL_NAME` protocol registered.
+fn build_nodes_one_proto() -> (
+	Arc<TestNetworkService>,
+	Option<Box<dyn NotificationService>>,
+	Arc<TestNetworkService>,
+	Option<Box<dyn NotificationService>>,
+) {
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+
+	let (network1, handle1) = TestNetworkBuilder::new()
+		.with_listen_addresses(vec![listen_addr.clone()])
+		.build();
+	let (node1, _) = network1.start_network();
+
+	let (network2, handle2) = TestNetworkBuilder::new()
+		.with_set_config(config::SetConfig {
+			reserved_nodes: vec![MultiaddrWithPeerId {
+				multiaddr: listen_addr,
+				peer_id: node1.local_peer_id(),
+			}],
+			..Default::default()
+		})
+		.build();
+
+	let (node2, _) = network2.start_network();
+
+	(node1, handle1, node2, handle2)
+}
+
+#[tokio::test]
+async fn notifications_state_consistent() {
+	// Runs two nodes and ensures that events are propagated out of the API in a consistent
+	// correct order, which means no notification received on a closed substream.
+
+	let (node1, handle1, node2, handle2) = build_nodes_one_proto();
+	let (mut handle1, mut handle2) = (handle1.unwrap(), handle2.unwrap());
+
+	// Write some initial notifications that shouldn't get through.
+	for _ in 0..(rand::random::<u8>() % 5) {
+		let _ = handle1.send_sync_notification(&node2.local_peer_id(), b"hello world".to_vec());
+	}
+	for _ in 0..(rand::random::<u8>() % 5) {
+		let _ = handle2.send_sync_notification(&node1.local_peer_id(), b"hello world".to_vec());
+	}
+
+	// True if we have an active substream from node1 to node2.
+	let mut node1_to_node2_open = false;
+	// True if we have an active substream from node2 to node1.
+	let mut node2_to_node1_open = false;
+	// We stop the test after a certain number of iterations.
+	let mut iterations = 0;
+	// Safe guard because we don't want the test to pass if no substream has been open.
+	let mut something_happened = false;
+
+	loop {
+		iterations += 1;
+		if iterations >= 1_000 {
+			assert!(something_happened);
+			break
+		}
+
+		// Start by sending a notification from node1 to node2 and vice-versa. Part of the
+		// test consists in ensuring that notifications get ignored if the stream isn't open.
+		if rand::random::<u8>() % 5 >= 3 {
+			let _ = handle1.send_sync_notification(&node2.local_peer_id(), b"hello world".to_vec());
+		}
+		if rand::random::<u8>() % 5 >= 3 {
+			let _ = handle2.send_sync_notification(&node1.local_peer_id(), b"hello world".to_vec());
+		}
+
+		// Also randomly disconnect the two nodes from time to time.
+		if rand::random::<u8>() % 20 == 0 {
+			node1.disconnect_peer(node2.local_peer_id(), PROTOCOL_NAME.into());
+		}
+		if rand::random::<u8>() % 20 == 0 {
+			node2.disconnect_peer(node1.local_peer_id(), PROTOCOL_NAME.into());
+		}
+
+		// Grab next event from either `events_stream1` or `events_stream2`.
+		let next_event = {
+			let next1 = handle1.next_event();
+			let next2 = handle2.next_event();
+			// We also await on a small timer, otherwise it is possible for the test to wait
+			// forever while nothing at all happens on the network.
+			let continue_test = futures_timer::Delay::new(Duration::from_millis(20));
+			match future::select(future::select(next1, next2), continue_test).await {
+				future::Either::Left((future::Either::Left((Some(ev), _)), _)) =>
+					future::Either::Left(ev),
+				future::Either::Left((future::Either::Right((Some(ev), _)), _)) =>
+					future::Either::Right(ev),
+				future::Either::Right(_) => continue,
+				_ => break,
+			}
+		};
+
+		match next_event {
+			future::Either::Left(NotificationEvent::ValidateInboundSubstream {
+				result_tx, ..
+			}) => {
+				result_tx.send(ValidationResult::Accept).unwrap();
+			},
+			future::Either::Right(NotificationEvent::ValidateInboundSubstream {
+				result_tx,
+				..
+			}) => {
+				result_tx.send(ValidationResult::Accept).unwrap();
+			},
+			future::Either::Left(NotificationEvent::NotificationStreamOpened { peer, .. }) => {
+				something_happened = true;
+				assert!(!node1_to_node2_open);
+				node1_to_node2_open = true;
+				assert_eq!(peer, node2.local_peer_id());
+			},
+			future::Either::Right(NotificationEvent::NotificationStreamOpened { peer, .. }) => {
+				something_happened = true;
+				assert!(!node2_to_node1_open);
+				node2_to_node1_open = true;
+				assert_eq!(peer, node1.local_peer_id());
+			},
+			future::Either::Left(NotificationEvent::NotificationStreamClosed { peer, .. }) => {
+				assert!(node1_to_node2_open);
+				node1_to_node2_open = false;
+				assert_eq!(peer, node2.local_peer_id());
+			},
+			future::Either::Right(NotificationEvent::NotificationStreamClosed { peer, .. }) => {
+				assert!(node2_to_node1_open);
+				node2_to_node1_open = false;
+				assert_eq!(peer, node1.local_peer_id());
+			},
+			future::Either::Left(NotificationEvent::NotificationReceived { peer, .. }) => {
+				assert!(node1_to_node2_open);
+				assert_eq!(peer, node2.local_peer_id());
+				if rand::random::<u8>() % 5 >= 4 {
+					let _ = handle1
+						.send_sync_notification(&node2.local_peer_id(), b"hello world".to_vec());
+				}
+			},
+			future::Either::Right(NotificationEvent::NotificationReceived { peer, .. }) => {
+				assert!(node2_to_node1_open);
+				assert_eq!(peer, node1.local_peer_id());
+				if rand::random::<u8>() % 5 >= 4 {
+					let _ = handle2
+						.send_sync_notification(&node1.local_peer_id(), b"hello world".to_vec());
+				}
+			},
+		};
+	}
+}
+
+#[tokio::test]
+async fn lots_of_incoming_peers_works() {
+	sp_tracing::try_init_simple();
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+
+	let (main_node, handle1) = TestNetworkBuilder::new()
+		.with_listen_addresses(vec![listen_addr.clone()])
+		.with_set_config(config::SetConfig { in_peers: u32::MAX, ..Default::default() })
+		.build();
+	let mut handle1 = handle1.unwrap();
+	let (main_node, _) = main_node.start_network();
+
+	let main_node_peer_id = main_node.local_peer_id();
+
+	tokio::spawn(async move {
+		while let Some(event) = handle1.next_event().await {
+			if let NotificationEvent::ValidateInboundSubstream { result_tx, .. } = event {
+				result_tx.send(ValidationResult::Accept).unwrap();
+			}
+		}
+	});
+
+	// We spawn background tasks and push them in this `Vec`. They will all be waited upon before
+	// this test ends.
+	let mut background_tasks_to_wait = Vec::new();
+
+	for _ in 0..32 {
+		let (dialing_node, handle) = TestNetworkBuilder::new()
+			.with_set_config(config::SetConfig {
+				reserved_nodes: vec![MultiaddrWithPeerId {
+					multiaddr: listen_addr.clone(),
+					peer_id: main_node_peer_id,
+				}],
+				..Default::default()
+			})
+			.build();
+		let mut handle = handle.unwrap();
+		let (_, _) = dialing_node.start_network();
+
+		background_tasks_to_wait.push(tokio::spawn(async move {
+			// Create a dummy timer that will "never" fire, and that will be overwritten when we
+			// actually need the timer. Using an Option would be technically cleaner, but it would
+			// make the code below way more complicated.
+			let mut timer = futures_timer::Delay::new(Duration::from_secs(3600 * 24 * 7)).fuse();
+
+			loop {
+				futures::select! {
+					_ = timer => {
+						// Test succeeds when timer fires.
+						return;
+					}
+					ev = handle.next_event().fuse() => match ev.unwrap() {
+						NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
+							result_tx.send(ValidationResult::Accept).unwrap();
+						}
+						NotificationEvent::NotificationStreamOpened { peer, .. } => {
+							assert_eq!(peer, main_node_peer_id);
+							// Test succeeds after 5 seconds. This timer is here in order to
+							// detect a potential problem after opening.
+							timer = futures_timer::Delay::new(Duration::from_secs(5)).fuse();
+						}
+						_ => {}
+					}
+				}
+			}
+		}));
+	}
+
+	future::join_all(background_tasks_to_wait).await;
+}
+
+#[tokio::test]
+async fn notifications_back_pressure() {
+	// Node 1 floods node 2 with notifications. Random sleeps are done on node 2 to simulate the
+	// node being busy. We make sure that all notifications are received.
+
+	const TOTAL_NOTIFS: usize = 10_000;
+
+	let (_node1, handle1, node2, handle2) = build_nodes_one_proto();
+	let (mut handle1, mut handle2) = (handle1.unwrap(), handle2.unwrap());
+	let node2_id = node2.local_peer_id();
+
+	let receiver = tokio::spawn(async move {
+		let mut received_notifications = 0;
+
+		while received_notifications < TOTAL_NOTIFS {
+			match handle2.next_event().await.unwrap() {
+				NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
+					result_tx.send(ValidationResult::Accept).unwrap();
+				},
+				NotificationEvent::NotificationReceived { notification, .. } => {
+					assert_eq!(
+						notification,
+						format!("hello #{}", received_notifications).into_bytes()
+					);
+					received_notifications += 1;
+				},
+				_ => {},
+			}
+
+			if rand::random::<u8>() < 2 {
+				tokio::time::sleep(Duration::from_millis(rand::random::<u64>() % 750)).await;
+			}
+		}
+	});
+
+	// Wait for the `NotificationStreamOpened`.
+	loop {
+		match handle1.next_event().await.unwrap() {
+			NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
+				result_tx.send(ValidationResult::Accept).unwrap();
+			},
+			NotificationEvent::NotificationStreamOpened { .. } => break,
+			_ => {},
+		};
+	}
+
+	// Sending!
+	for num in 0..TOTAL_NOTIFS {
+		handle1
+			.send_async_notification(&node2_id, format!("hello #{}", num).into_bytes())
+			.await
+			.unwrap();
+	}
+
+	receiver.await.unwrap();
+}
+
+#[tokio::test]
+async fn fallback_name_working() {
+	sp_tracing::try_init_simple();
+	// Node 1 supports the protocols "new" and "old". Node 2 only supports "old". Checks whether
+	// they can connect.
+	const NEW_PROTOCOL_NAME: &str = "/new-shiny-protocol-that-isnt-PROTOCOL_NAME";
+
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+	let (config, mut handle1) = config::NonDefaultSetConfig::new(
+		NEW_PROTOCOL_NAME.into(),
+		vec![PROTOCOL_NAME.into()],
+		1024 * 1024,
+		None,
+		Default::default(),
+	);
+	let (network1, _) = TestNetworkBuilder::new()
+		.with_notification_protocol(config)
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			transport: TransportConfig::MemoryOnly,
+			..config::NetworkConfiguration::new_local()
+		})
+		.build();
+
+	let (node1, _) = network1.start_network();
+
+	let (network2, handle2) = TestNetworkBuilder::new()
+		.with_set_config(config::SetConfig {
+			reserved_nodes: vec![MultiaddrWithPeerId {
+				multiaddr: listen_addr,
+				peer_id: node1.local_peer_id(),
+			}],
+			..Default::default()
+		})
+		.build();
+	let mut handle2 = handle2.unwrap();
+	let _ = network2.start_network();
+
+	let receiver = tokio::spawn(async move {
+		// Wait for the `NotificationStreamOpened`.
+		loop {
+			match handle2.next_event().await.unwrap() {
+				NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
+					result_tx.send(ValidationResult::Accept).unwrap();
+				},
+				NotificationEvent::NotificationStreamOpened { negotiated_fallback, .. } => {
+					assert_eq!(negotiated_fallback, None);
+					break
+				},
+				_ => {},
+			}
+		}
+	});
+
+	// Wait for the `NotificationStreamOpened`.
+	loop {
+		match handle1.next_event().await.unwrap() {
+			NotificationEvent::ValidateInboundSubstream { result_tx, .. } => {
+				result_tx.send(ValidationResult::Accept).unwrap();
+			},
+			NotificationEvent::NotificationStreamOpened { negotiated_fallback, .. } => {
+				assert_eq!(negotiated_fallback, Some(PROTOCOL_NAME.into()));
+				break
+			},
+			_ => {},
+		}
+	}
+
+	receiver.await.unwrap();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_listen_addresses_consistent_with_transport_memory() {
+	let listen_addr = config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)];
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			transport: TransportConfig::MemoryOnly,
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_listen_addresses_consistent_with_transport_not_memory() {
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_boot_node_addresses_consistent_with_transport_memory() {
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+	let boot_node = MultiaddrWithPeerId {
+		multiaddr: config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)],
+		peer_id: PeerId::random(),
+	};
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			transport: TransportConfig::MemoryOnly,
+			boot_nodes: vec![boot_node],
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_boot_node_addresses_consistent_with_transport_not_memory() {
+	let listen_addr = config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)];
+	let boot_node = MultiaddrWithPeerId {
+		multiaddr: config::build_multiaddr![Memory(rand::random::<u64>())],
+		peer_id: PeerId::random(),
+	};
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			boot_nodes: vec![boot_node],
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_reserved_node_addresses_consistent_with_transport_memory() {
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+	let reserved_node = MultiaddrWithPeerId {
+		multiaddr: config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)],
+		peer_id: PeerId::random(),
+	};
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			transport: TransportConfig::MemoryOnly,
+			default_peers_set: config::SetConfig {
+				reserved_nodes: vec![reserved_node],
+				..Default::default()
+			},
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_reserved_node_addresses_consistent_with_transport_not_memory() {
+	let listen_addr = config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)];
+	let reserved_node = MultiaddrWithPeerId {
+		multiaddr: config::build_multiaddr![Memory(rand::random::<u64>())],
+		peer_id: PeerId::random(),
+	};
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			default_peers_set: config::SetConfig {
+				reserved_nodes: vec![reserved_node],
+				..Default::default()
+			},
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_public_addresses_consistent_with_transport_memory() {
+	let listen_addr = config::build_multiaddr![Memory(rand::random::<u64>())];
+	let public_address = config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)];
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			transport: TransportConfig::MemoryOnly,
+			public_addresses: vec![public_address],
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
+
+#[tokio::test]
+#[should_panic(expected = "don't match the transport")]
+async fn ensure_public_addresses_consistent_with_transport_not_memory() {
+	let listen_addr = config::build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(0_u16)];
+	let public_address = config::build_multiaddr![Memory(rand::random::<u64>())];
+
+	let _ = TestNetworkBuilder::new()
+		.with_config(config::NetworkConfiguration {
+			listen_addresses: vec![listen_addr.clone()],
+			public_addresses: vec![public_address],
+			..config::NetworkConfiguration::new(
+				"test-node",
+				"test-client",
+				Default::default(),
+				None,
+			)
+		})
+		.build()
+		.0
+		.start_network();
+}
diff --git a/libp2p/test/src/sync.rs b/libp2p/test/src/sync.rs
new file mode 100644
index 000000000..c025a8262
--- /dev/null
+++ b/libp2p/test/src/sync.rs
@@ -0,0 +1,1350 @@
+// 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 super::*;
+use futures::Future;
+use sp_consensus::{block_validation::Validation, BlockOrigin};
+use sp_runtime::Justifications;
+use substrate_test_runtime::Header;
+
+async fn test_ancestor_search_when_common_is(n: usize) {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	net.peer(0).push_blocks(n, false);
+	net.peer(1).push_blocks(n, false);
+	net.peer(2).push_blocks(n, false);
+
+	net.peer(0).push_blocks(10, true);
+	net.peer(1).push_blocks(100, false);
+	net.peer(2).push_blocks(100, false);
+
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_peers_works() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	while net.peer(0).num_peers().await != 2 &&
+		net.peer(1).num_peers().await != 2 &&
+		net.peer(2).num_peers().await != 2
+	{
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+	}
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_cycle_from_offline_to_syncing_to_offline() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	for peer in 0..3 {
+		// Offline, and not major syncing.
+		assert!(net.peer(peer).is_offline());
+		assert!(!net.peer(peer).is_major_syncing());
+	}
+
+	// Generate blocks.
+	net.peer(2).push_blocks(100, false);
+
+	// Block until all nodes are online and nodes 0 and 1 and major syncing.
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		for peer in 0..3 {
+			// Online
+			if net.peer(peer).is_offline() {
+				return Poll::Pending
+			}
+			if peer < 2 {
+				// Major syncing.
+				if net.peer(peer).blocks_count() < 100 && !net.peer(peer).is_major_syncing() {
+					return Poll::Pending
+				}
+			}
+		}
+		Poll::Ready(())
+	})
+	.await;
+
+	// Block until all nodes are done syncing.
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		for peer in 0..3 {
+			if net.peer(peer).is_major_syncing() {
+				return Poll::Pending
+			}
+		}
+		Poll::Ready(())
+	})
+	.await;
+
+	// Now drop nodes 1 and 2, and check that node 0 is offline.
+	net.peers.remove(2);
+	net.peers.remove(1);
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if !net.peer(0).is_offline() {
+			Poll::Pending
+		} else {
+			Poll::Ready(())
+		}
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncing_node_not_major_syncing_when_disconnected() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	// Generate blocks.
+	net.peer(2).push_blocks(100, false);
+
+	// Check that we're not major syncing when disconnected.
+	assert!(!net.peer(1).is_major_syncing());
+
+	// Check that we switch to major syncing.
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if !net.peer(1).is_major_syncing() {
+			Poll::Pending
+		} else {
+			Poll::Ready(())
+		}
+	})
+	.await;
+
+	// Destroy two nodes, and check that we switch to non-major syncing.
+	net.peers.remove(2);
+	net.peers.remove(0);
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if net.peer(0).is_major_syncing() {
+			Poll::Pending
+		} else {
+			Poll::Ready(())
+		}
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_from_two_peers_works() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	net.peer(1).push_blocks(100, false);
+	net.peer(2).push_blocks(100, false);
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+	assert!(!net.peer(0).is_major_syncing());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_from_two_peers_with_ancestry_search_works() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	net.peer(0).push_blocks(10, true);
+	net.peer(1).push_blocks(100, false);
+	net.peer(2).push_blocks(100, false);
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ancestry_search_works_when_backoff_is_one() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	net.peer(0).push_blocks(1, false);
+	net.peer(1).push_blocks(2, false);
+	net.peer(2).push_blocks(2, false);
+
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ancestry_search_works_when_ancestor_is_genesis() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	net.peer(0).push_blocks(13, true);
+	net.peer(1).push_blocks(100, false);
+	net.peer(2).push_blocks(100, false);
+
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ancestry_search_works_when_common_is_one() {
+	test_ancestor_search_when_common_is(1).await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ancestry_search_works_when_common_is_two() {
+	test_ancestor_search_when_common_is(2).await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ancestry_search_works_when_common_is_hundred() {
+	test_ancestor_search_when_common_is(100).await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_long_chain_works() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+	net.peer(1).push_blocks(500, false);
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_no_common_longer_chain_fails() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	net.peer(0).push_blocks(20, true);
+	net.peer(1).push_blocks(20, false);
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if net.peer(0).is_major_syncing() {
+			Poll::Pending
+		} else {
+			Poll::Ready(())
+		}
+	})
+	.await;
+	let peer1 = &net.peers()[1];
+	assert!(!net.peers()[0].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_justifications() {
+	sp_tracing::try_init_simple();
+	let mut net = JustificationTestNet::new(3);
+	let hashes = net.peer(0).push_blocks(20, false);
+	net.run_until_sync().await;
+
+	let hashof10 = hashes[9];
+	let hashof15 = hashes[14];
+	let hashof20 = hashes[19];
+
+	// there's currently no justification for block #10
+	assert_eq!(net.peer(0).client().justifications(hashof10).unwrap(), None);
+	assert_eq!(net.peer(1).client().justifications(hashof10).unwrap(), None);
+
+	// we finalize block #10, #15 and #20 for peer 0 with a justification
+	let just = (*b"FRNK", Vec::new());
+	net.peer(0).client().finalize_block(hashof10, Some(just.clone()), true).unwrap();
+	net.peer(0).client().finalize_block(hashof15, Some(just.clone()), true).unwrap();
+	net.peer(0).client().finalize_block(hashof20, Some(just.clone()), true).unwrap();
+
+	let hashof10 = net.peer(1).client().as_client().hash(10).unwrap().unwrap();
+	let hashof15 = net.peer(1).client().as_client().hash(15).unwrap().unwrap();
+	let hashof20 = net.peer(1).client().as_client().hash(20).unwrap().unwrap();
+
+	// peer 1 should get the justifications from the network
+	net.peer(1).request_justification(&hashof10, 10);
+	net.peer(1).request_justification(&hashof15, 15);
+	net.peer(1).request_justification(&hashof20, 20);
+
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		for height in (10..21).step_by(5) {
+			if net.peer(0).client().justifications(hashes[height - 1]).unwrap() !=
+				Some(Justifications::from((*b"FRNK", Vec::new())))
+			{
+				return Poll::Pending
+			}
+			if net.peer(1).client().justifications(hashes[height - 1]).unwrap() !=
+				Some(Justifications::from((*b"FRNK", Vec::new())))
+			{
+				return Poll::Pending
+			}
+		}
+
+		Poll::Ready(())
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_justifications_across_forks() {
+	sp_tracing::try_init_simple();
+	let mut net = JustificationTestNet::new(3);
+	// we push 5 blocks
+	net.peer(0).push_blocks(5, false);
+	// and then two forks 5 and 6 blocks long
+	let f1_best = net.peer(0).push_blocks_at(BlockId::Number(5), 5, false).pop().unwrap();
+	let f2_best = net.peer(0).push_blocks_at(BlockId::Number(5), 6, false).pop().unwrap();
+
+	// peer 1 will only see the longer fork. but we'll request justifications
+	// for both and finalize the small fork instead.
+	net.run_until_sync().await;
+
+	let just = (*b"FRNK", Vec::new());
+	net.peer(0).client().finalize_block(f1_best, Some(just), true).unwrap();
+
+	net.peer(1).request_justification(&f1_best, 10);
+	net.peer(1).request_justification(&f2_best, 11);
+
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		if net.peer(0).client().justifications(f1_best).unwrap() ==
+			Some(Justifications::from((*b"FRNK", Vec::new()))) &&
+			net.peer(1).client().justifications(f1_best).unwrap() ==
+				Some(Justifications::from((*b"FRNK", Vec::new())))
+		{
+			Poll::Ready(())
+		} else {
+			Poll::Pending
+		}
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_after_fork_works() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	net.peer(0).push_blocks(30, false);
+	net.peer(1).push_blocks(30, false);
+	net.peer(2).push_blocks(30, false);
+
+	net.peer(0).push_blocks(10, true);
+	net.peer(1).push_blocks(20, false);
+	net.peer(2).push_blocks(20, false);
+
+	net.peer(1).push_blocks(10, true);
+	net.peer(2).push_blocks(1, false);
+
+	// peer 1 has the best chain
+	net.run_until_sync().await;
+	let peer1 = &net.peers()[1];
+	assert!(net.peers()[0].blockchain_canon_equals(peer1));
+	(net.peers()[1].blockchain_canon_equals(peer1));
+	(net.peers()[2].blockchain_canon_equals(peer1));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_all_forks() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(4);
+	net.peer(0).push_blocks(2, false);
+	net.peer(1).push_blocks(2, false);
+
+	let b1 = net.peer(0).push_blocks(2, true).pop().unwrap();
+	let b2 = net.peer(1).push_blocks(4, false).pop().unwrap();
+
+	net.run_until_sync().await;
+	// Check that all peers have all of the branches.
+	assert!(net.peer(0).has_block(b1));
+	assert!(net.peer(0).has_block(b2));
+	assert!(net.peer(1).has_block(b1));
+	assert!(net.peer(1).has_block(b2));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn own_blocks_are_announced() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+	net.run_until_sync().await; // connect'em
+	net.peer(0)
+		.generate_blocks(1, BlockOrigin::Own, |builder| builder.build().unwrap().block);
+
+	net.run_until_sync().await;
+
+	assert_eq!(net.peer(0).client.info().best_number, 1);
+	assert_eq!(net.peer(1).client.info().best_number, 1);
+	let peer0 = &net.peers()[0];
+	assert!(net.peers()[1].blockchain_canon_equals(peer0));
+	(net.peers()[2].blockchain_canon_equals(peer0));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn can_sync_small_non_best_forks() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+	net.peer(0).push_blocks(30, false);
+	net.peer(1).push_blocks(30, false);
+
+	// small fork + reorg on peer 1.
+	net.peer(0).push_blocks_at(BlockId::Number(30), 2, true);
+	let small_hash = net.peer(0).client().info().best_hash;
+	net.peer(0).push_blocks_at(BlockId::Number(30), 10, false);
+	assert_eq!(net.peer(0).client().info().best_number, 40);
+
+	// peer 1 only ever had the long fork.
+	net.peer(1).push_blocks(10, false);
+	assert_eq!(net.peer(1).client().info().best_number, 40);
+
+	assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+	assert!(net.peer(1).client().header(small_hash).unwrap().is_none());
+
+	// poll until the two nodes connect, otherwise announcing the block will not work
+	while net.peer(0).num_peers().await == 0 || net.peer(1).num_peers().await == 0 {
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+	}
+
+	// synchronization: 0 synced to longer chain and 1 didn't sync to small chain.
+
+	assert_eq!(net.peer(0).client().info().best_number, 40);
+
+	assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+	assert!(!net.peer(1).client().header(small_hash).unwrap().is_some());
+
+	net.peer(0).announce_block(small_hash, None);
+
+	// after announcing, peer 1 downloads the block.
+
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+		if net.peer(1).client().header(small_hash).unwrap().is_none() {
+			return Poll::Pending
+		}
+		Poll::Ready(())
+	})
+	.await;
+	net.run_until_sync().await;
+
+	let another_fork = net.peer(0).push_blocks_at(BlockId::Number(35), 2, true).pop().unwrap();
+	net.peer(0).announce_block(another_fork, None);
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if net.peer(1).client().header(another_fork).unwrap().is_none() {
+			return Poll::Pending
+		}
+		Poll::Ready(())
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn can_sync_forks_ahead_of_the_best_chain() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+	net.peer(0).push_blocks(1, false);
+	net.peer(1).push_blocks(1, false);
+
+	net.run_until_connected().await;
+
+	// Peer 0 is on 2-block fork which is announced with is_best=false
+	let fork_hash = net
+		.peer(0)
+		.generate_blocks_with_fork_choice(
+			2,
+			BlockOrigin::Own,
+			|builder| builder.build().unwrap().block,
+			ForkChoiceStrategy::Custom(false),
+		)
+		.pop()
+		.unwrap();
+	// Peer 1 is on 1-block fork
+	net.peer(1).push_blocks(1, false);
+	assert!(net.peer(0).client().header(fork_hash).unwrap().is_some());
+	assert_eq!(net.peer(0).client().info().best_number, 1);
+	assert_eq!(net.peer(1).client().info().best_number, 2);
+
+	// after announcing, peer 1 downloads the block.
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		if net.peer(1).client().header(fork_hash).unwrap().is_none() {
+			return Poll::Pending
+		}
+		Poll::Ready(())
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn can_sync_explicit_forks() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+	net.peer(0).push_blocks(30, false);
+	net.peer(1).push_blocks(30, false);
+
+	// small fork + reorg on peer 1.
+	net.peer(0).push_blocks_at(BlockId::Number(30), 2, true);
+	let small_hash = net.peer(0).client().info().best_hash;
+	let small_number = net.peer(0).client().info().best_number;
+	net.peer(0).push_blocks_at(BlockId::Number(30), 10, false);
+	assert_eq!(net.peer(0).client().info().best_number, 40);
+
+	// peer 1 only ever had the long fork.
+	net.peer(1).push_blocks(10, false);
+	assert_eq!(net.peer(1).client().info().best_number, 40);
+
+	assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+	assert!(net.peer(1).client().header(small_hash).unwrap().is_none());
+
+	// poll until the two nodes connect, otherwise announcing the block will not work
+	while net.peer(0).num_peers().await == 0 || net.peer(1).num_peers().await == 0 {
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+	}
+
+	// synchronization: 0 synced to longer chain and 1 didn't sync to small chain.
+
+	assert_eq!(net.peer(0).client().info().best_number, 40);
+
+	assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+	assert!(!net.peer(1).client().header(small_hash).unwrap().is_some());
+
+	// request explicit sync
+	let first_peer_id = net.peer(0).id();
+	net.peer(1).set_sync_fork_request(vec![first_peer_id], small_hash, small_number);
+
+	// peer 1 downloads the block.
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		assert!(net.peer(0).client().header(small_hash).unwrap().is_some());
+		if net.peer(1).client().header(small_hash).unwrap().is_none() {
+			return Poll::Pending
+		}
+		Poll::Ready(())
+	})
+	.await;
+}
+
+// TODO: for unknown reason, this test is flaky on a multithreaded runtime, so we run it
+//       in a single-threaded mode.
+//       See issue https://github.com/paritytech/substrate/issues/14622.
+#[tokio::test]
+async fn syncs_header_only_forks() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(FullPeerConfig { blocks_pruning: Some(3), ..Default::default() });
+	net.peer(0).push_blocks(2, false);
+	net.peer(1).push_blocks(2, false);
+
+	net.peer(0).push_blocks(2, true);
+	let small_hash = net.peer(0).client().info().best_hash;
+	net.peer(1).push_blocks(4, false);
+
+	// Peer 1 will sync the small fork even though common block state is missing
+	while !net.peer(1).has_block(small_hash) {
+		net.run_until_idle().await;
+	}
+
+	net.run_until_sync().await;
+	assert_eq!(net.peer(0).client().info().best_hash, net.peer(1).client().info().best_hash);
+	assert_ne!(small_hash, net.peer(0).client().info().best_hash);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn does_not_sync_announced_old_best_block() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(3);
+
+	let old_hash = net.peer(0).push_blocks(1, false).pop().unwrap();
+	let old_hash_with_parent = net.peer(0).push_blocks(1, false).pop().unwrap();
+	net.peer(0).push_blocks(18, true);
+	net.peer(1).push_blocks(20, true);
+
+	net.peer(0).announce_block(old_hash, None);
+	futures::future::poll_fn::<(), _>(|cx| {
+		// poll once to import announcement
+		net.poll(cx);
+		Poll::Ready(())
+	})
+	.await;
+	assert!(!net.peer(1).is_major_syncing());
+
+	net.peer(0).announce_block(old_hash_with_parent, None);
+	futures::future::poll_fn::<(), _>(|cx| {
+		// poll once to import announcement
+		net.poll(cx);
+		Poll::Ready(())
+	})
+	.await;
+	assert!(!net.peer(1).is_major_syncing());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn full_sync_requires_block_body() {
+	// Check that we don't sync headers-only in full mode.
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+
+	net.peer(0).push_headers(1);
+	// Wait for nodes to connect
+	while net.peer(0).num_peers().await == 0 || net.peer(1).num_peers().await == 0 {
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+	}
+
+	net.run_until_idle().await;
+	assert_eq!(net.peer(1).client.info().best_number, 0);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn imports_stale_once() {
+	sp_tracing::try_init_simple();
+
+	async fn import_with_announce(net: &mut TestNet, hash: H256) {
+		// Announce twice
+		net.peer(0).announce_block(hash, None);
+		net.peer(0).announce_block(hash, None);
+
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			if net.peer(1).client().header(hash).unwrap().is_some() {
+				Poll::Ready(())
+			} else {
+				Poll::Pending
+			}
+		})
+		.await;
+	}
+
+	// given the network with 2 full nodes
+	let mut net = TestNet::new(2);
+
+	// let them connect to each other
+	net.run_until_sync().await;
+
+	// check that NEW block is imported from announce message
+	let new_hash = net.peer(0).push_blocks(1, false).pop().unwrap();
+	import_with_announce(&mut net, new_hash).await;
+	assert_eq!(net.peer(1).num_downloaded_blocks().await, 1);
+
+	// check that KNOWN STALE block is imported from announce message
+	let known_stale_hash = net.peer(0).push_blocks_at(BlockId::Number(0), 1, true).pop().unwrap();
+	import_with_announce(&mut net, known_stale_hash).await;
+	assert_eq!(net.peer(1).num_downloaded_blocks().await, 2);
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn can_sync_to_peers_with_wrong_common_block() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+
+	net.peer(0).push_blocks(2, true);
+	net.peer(1).push_blocks(2, true);
+	let fork_hash = net.peer(0).push_blocks_at(BlockId::Number(0), 2, false).pop().unwrap();
+	net.peer(1).push_blocks_at(BlockId::Number(0), 2, false);
+	// wait for connection
+	net.run_until_connected().await;
+
+	// both peers re-org to the same fork without notifying each other
+	let just = Some((*b"FRNK", Vec::new()));
+	net.peer(0).client().finalize_block(fork_hash, just.clone(), true).unwrap();
+	net.peer(1).client().finalize_block(fork_hash, just, true).unwrap();
+	let final_hash = net.peer(0).push_blocks(1, false).pop().unwrap();
+
+	net.run_until_sync().await;
+
+	assert!(net.peer(1).has_block(final_hash));
+}
+
+/// Returns `is_new_best = true` for each validated announcement.
+struct NewBestBlockAnnounceValidator;
+
+impl BlockAnnounceValidator<Block> for NewBestBlockAnnounceValidator {
+	fn validate(
+		&mut self,
+		_: &Header,
+		_: &[u8],
+	) -> Pin<Box<dyn Future<Output = Result<Validation, Box<dyn std::error::Error + Send>>> + Send>>
+	{
+		async { Ok(Validation::Success { is_new_best: true }) }.boxed()
+	}
+}
+
+/// Returns `Validation::Failure` for specified block number
+struct FailingBlockAnnounceValidator(u64);
+
+impl BlockAnnounceValidator<Block> for FailingBlockAnnounceValidator {
+	fn validate(
+		&mut self,
+		header: &Header,
+		_: &[u8],
+	) -> Pin<Box<dyn Future<Output = Result<Validation, Box<dyn std::error::Error + Send>>> + Send>>
+	{
+		let number = *header.number();
+		let target_number = self.0;
+		async move {
+			Ok(if number == target_number {
+				Validation::Failure { disconnect: false }
+			} else {
+				Validation::Success { is_new_best: true }
+			})
+		}
+		.boxed()
+	}
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_blocks_when_block_announce_validator_says_it_is_new_best() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(NewBestBlockAnnounceValidator)),
+		..Default::default()
+	});
+
+	net.run_until_connected().await;
+
+	// Add blocks but don't set them as best
+	let block_hash = net
+		.peer(0)
+		.generate_blocks_with_fork_choice(
+			1,
+			BlockOrigin::Own,
+			|builder| builder.build().unwrap().block,
+			ForkChoiceStrategy::Custom(false),
+		)
+		.pop()
+		.unwrap();
+
+	while !net.peer(2).has_block(block_hash) {
+		net.run_until_idle().await;
+	}
+}
+
+/// Waits for some time until the validation is successfull.
+struct DeferredBlockAnnounceValidator;
+
+impl BlockAnnounceValidator<Block> for DeferredBlockAnnounceValidator {
+	fn validate(
+		&mut self,
+		_: &Header,
+		_: &[u8],
+	) -> Pin<Box<dyn Future<Output = Result<Validation, Box<dyn std::error::Error + Send>>> + Send>>
+	{
+		async {
+			futures_timer::Delay::new(std::time::Duration::from_millis(500)).await;
+			Ok(Validation::Success { is_new_best: false })
+		}
+		.boxed()
+	}
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn wait_until_deferred_block_announce_validation_is_ready() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(NewBestBlockAnnounceValidator)),
+		..Default::default()
+	});
+
+	net.run_until_connected().await;
+
+	// Add blocks but don't set them as best
+	let block_hash = net
+		.peer(0)
+		.generate_blocks_with_fork_choice(
+			1,
+			BlockOrigin::Own,
+			|builder| builder.build().unwrap().block,
+			ForkChoiceStrategy::Custom(false),
+		)
+		.pop()
+		.unwrap();
+
+	while !net.peer(1).has_block(block_hash) {
+		net.run_until_idle().await;
+	}
+}
+
+/// When we don't inform the sync protocol about the best block, a node will not sync from us as the
+/// handshake is not does not contain our best block.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_to_tip_requires_that_sync_protocol_is_informed_about_best_block() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(1);
+
+	// Produce some blocks
+	let block_hash = net
+		.peer(0)
+		.push_blocks_at_without_informing_sync(BlockId::Number(0), 3, true, true)
+		.pop()
+		.unwrap();
+
+	// Add a node and wait until they are connected
+	net.add_full_peer_with_config(Default::default());
+	net.run_until_connected().await;
+	net.run_until_idle().await;
+
+	// The peer should not have synced the block.
+	assert!(!net.peer(1).has_block(block_hash));
+
+	// Make sync protocol aware of the best block
+	net.peer(0).sync_service().new_best_block_imported(block_hash, 3);
+	net.run_until_idle().await;
+
+	// Connect another node that should now sync to the tip
+	net.add_full_peer_with_config(FullPeerConfig {
+		connect_to_peers: Some(vec![0]),
+		..Default::default()
+	});
+
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if net.peer(2).has_block(block_hash) {
+			Poll::Ready(())
+		} else {
+			Poll::Pending
+		}
+	})
+	.await;
+
+	// However peer 1 should still not have the block.
+	assert!(!net.peer(1).has_block(block_hash));
+}
+
+/// Ensures that if we as a syncing node sync to the tip while we are connected to another peer
+/// that is currently also doing a major sync.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn sync_to_tip_when_we_sync_together_with_multiple_peers() {
+	sp_tracing::try_init_simple();
+
+	let mut net = TestNet::new(3);
+
+	let block_hash = net
+		.peer(0)
+		.push_blocks_at_without_informing_sync(BlockId::Number(0), 10_000, false, false)
+		.pop()
+		.unwrap();
+
+	net.peer(1)
+		.push_blocks_at_without_informing_sync(BlockId::Number(0), 5_000, false, false);
+
+	net.run_until_connected().await;
+	net.run_until_idle().await;
+
+	assert!(!net.peer(2).has_block(block_hash));
+
+	net.peer(0).sync_service().new_best_block_imported(block_hash, 10_000);
+	net.peer(0).sync_service().announce_block(block_hash, None);
+
+	while !net.peer(2).has_block(block_hash) && !net.peer(1).has_block(block_hash) {
+		net.run_until_idle().await;
+	}
+}
+
+/// Ensures that when we receive a block announcement with some data attached, that we propagate
+/// this data when reannouncing the block.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn block_announce_data_is_propagated() {
+	struct TestBlockAnnounceValidator;
+
+	impl BlockAnnounceValidator<Block> for TestBlockAnnounceValidator {
+		fn validate(
+			&mut self,
+			_: &Header,
+			data: &[u8],
+		) -> Pin<
+			Box<dyn Future<Output = Result<Validation, Box<dyn std::error::Error + Send>>> + Send>,
+		> {
+			let correct = data.get(0) == Some(&137);
+			async move {
+				if correct {
+					Ok(Validation::Success { is_new_best: true })
+				} else {
+					Ok(Validation::Failure { disconnect: false })
+				}
+			}
+			.boxed()
+		}
+	}
+
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(1);
+
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(TestBlockAnnounceValidator)),
+		..Default::default()
+	});
+
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(TestBlockAnnounceValidator)),
+		connect_to_peers: Some(vec![1]),
+		..Default::default()
+	});
+
+	// Wait until peer 1 is connected to both nodes.
+	while net.peer(1).num_peers().await != 2 ||
+		net.peer(0).num_peers().await != 1 ||
+		net.peer(2).num_peers().await != 1
+	{
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+	}
+
+	let block_hash = net
+		.peer(0)
+		.push_blocks_at_without_announcing(BlockId::Number(0), 1, true)
+		.pop()
+		.unwrap();
+	net.peer(0).announce_block(block_hash, Some(vec![137]));
+
+	while !net.peer(1).has_block(block_hash) || !net.peer(2).has_block(block_hash) {
+		net.run_until_idle().await;
+	}
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn continue_to_sync_after_some_block_announcement_verifications_failed() {
+	struct TestBlockAnnounceValidator;
+
+	impl BlockAnnounceValidator<Block> for TestBlockAnnounceValidator {
+		fn validate(
+			&mut self,
+			header: &Header,
+			_: &[u8],
+		) -> Pin<
+			Box<dyn Future<Output = Result<Validation, Box<dyn std::error::Error + Send>>> + Send>,
+		> {
+			let number = *header.number();
+			async move {
+				if number < 100 {
+					Err(Box::<dyn std::error::Error + Send + Sync>::from(String::from("error"))
+						as Box<_>)
+				} else {
+					Ok(Validation::Success { is_new_best: false })
+				}
+			}
+			.boxed()
+		}
+	}
+
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(1);
+
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(TestBlockAnnounceValidator)),
+		..Default::default()
+	});
+
+	net.run_until_connected().await;
+	net.run_until_idle().await;
+
+	let block_hash = net.peer(0).push_blocks(500, true).pop().unwrap();
+
+	net.run_until_sync().await;
+	assert!(net.peer(1).has_block(block_hash));
+}
+
+/// When being spammed by the same request of a peer, we ban this peer. However, we should only ban
+/// this peer if the request was successful. In the case of a justification request for example,
+/// we ask our peers multiple times until we got the requested justification. This test ensures that
+/// asking for the same justification multiple times doesn't ban a peer.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn multiple_requests_are_accepted_as_long_as_they_are_not_fulfilled() {
+	sp_tracing::try_init_simple();
+	let mut net = JustificationTestNet::new(2);
+	let hashes = net.peer(0).push_blocks(10, false);
+	net.run_until_sync().await;
+
+	// there's currently no justification for block #10
+	let hashof10 = hashes[9];
+	assert_eq!(net.peer(0).client().justifications(hashof10).unwrap(), None);
+	assert_eq!(net.peer(1).client().justifications(hashof10).unwrap(), None);
+
+	// Let's assume block 10 was finalized, but we still need the justification from the network.
+	net.peer(1).request_justification(&hashof10, 10);
+
+	// Let's build some more blocks and wait always for the network to have synced them
+	for _ in 0..5 {
+		// We need to sleep 10 seconds as this is the time we wait between sending a new
+		// justification request.
+		tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
+		net.peer(0).push_blocks(1, false);
+		net.run_until_sync().await;
+		assert_eq!(1, net.peer(0).num_peers().await);
+	}
+
+	let hashof10 = hashes[9];
+	// Finalize the 10th block and make the justification available.
+	net.peer(0)
+		.client()
+		.finalize_block(hashof10, Some((*b"FRNK", Vec::new())), true)
+		.unwrap();
+
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+
+		if net.peer(1).client().justifications(hashof10).unwrap() !=
+			Some(Justifications::from((*b"FRNK", Vec::new())))
+		{
+			return Poll::Pending
+		}
+
+		Poll::Ready(())
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_all_forks_from_single_peer() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+	net.peer(0).push_blocks(10, false);
+	net.peer(1).push_blocks(10, false);
+
+	// poll until the two nodes connect, otherwise announcing the block will not work
+	net.run_until_connected().await;
+
+	// Peer 0 produces new blocks and announces.
+	let branch1 = net.peer(0).push_blocks_at(BlockId::Number(10), 2, true).pop().unwrap();
+
+	// Wait till peer 1 starts downloading
+	loop {
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			Poll::Ready(())
+		})
+		.await;
+
+		if net.peer(1).sync_service().best_seen_block().await.unwrap() == Some(12) {
+			break
+		}
+	}
+
+	// Peer 0 produces and announces another fork
+	let branch2 = net.peer(0).push_blocks_at(BlockId::Number(10), 2, false).pop().unwrap();
+
+	net.run_until_sync().await;
+
+	// Peer 1 should have both branches,
+	assert!(net.peer(1).client().header(branch1).unwrap().is_some());
+	assert!(net.peer(1).client().header(branch2).unwrap().is_some());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_after_missing_announcement() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	net.add_full_peer_with_config(Default::default());
+	// Set peer 1 to ignore announcement
+	net.add_full_peer_with_config(FullPeerConfig {
+		block_announce_validator: Some(Box::new(FailingBlockAnnounceValidator(11))),
+		..Default::default()
+	});
+	net.peer(0).push_blocks(10, false);
+	net.peer(1).push_blocks(10, false);
+
+	net.run_until_connected().await;
+
+	// Peer 0 produces a new block and announces. Peer 1 ignores announcement.
+	net.peer(0).push_blocks_at(BlockId::Number(10), 1, false);
+	// Peer 0 produces another block and announces.
+	let final_block = net.peer(0).push_blocks_at(BlockId::Number(11), 1, false).pop().unwrap();
+	net.peer(1).push_blocks_at(BlockId::Number(10), 1, true);
+	net.run_until_sync().await;
+	assert!(net.peer(1).client().header(final_block).unwrap().is_some());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_state() {
+	sp_tracing::try_init_simple();
+	for skip_proofs in &[false, true] {
+		let mut net = TestNet::new(0);
+		let mut genesis_storage: sp_core::storage::Storage = Default::default();
+		genesis_storage.top.insert(b"additional_key".to_vec(), vec![1]);
+		let mut child_data: std::collections::BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
+		for i in 0u8..16 {
+			child_data.insert(vec![i; 5], vec![i; 33]);
+		}
+		let child1 = sp_core::storage::StorageChild {
+			data: child_data.clone(),
+			child_info: sp_core::storage::ChildInfo::new_default(b"child1"),
+		};
+		let child3 = sp_core::storage::StorageChild {
+			data: child_data.clone(),
+			child_info: sp_core::storage::ChildInfo::new_default(b"child3"),
+		};
+		for i in 22u8..33 {
+			child_data.insert(vec![i; 5], vec![i; 33]);
+		}
+		let child2 = sp_core::storage::StorageChild {
+			data: child_data.clone(),
+			child_info: sp_core::storage::ChildInfo::new_default(b"child2"),
+		};
+		genesis_storage
+			.children_default
+			.insert(child1.child_info.storage_key().to_vec(), child1);
+		genesis_storage
+			.children_default
+			.insert(child2.child_info.storage_key().to_vec(), child2);
+		genesis_storage
+			.children_default
+			.insert(child3.child_info.storage_key().to_vec(), child3);
+		let mut config_one = FullPeerConfig::default();
+		config_one.extra_storage = Some(genesis_storage.clone());
+		net.add_full_peer_with_config(config_one);
+		let mut config_two = FullPeerConfig::default();
+		config_two.extra_storage = Some(genesis_storage);
+		config_two.sync_mode =
+			SyncMode::LightState { skip_proofs: *skip_proofs, storage_chain_mode: false };
+		net.add_full_peer_with_config(config_two);
+		let hashes = net.peer(0).push_blocks(64, false);
+		// Wait for peer 1 to sync header chain.
+		net.run_until_sync().await;
+		assert!(!net.peer(1).client().has_state_at(&BlockId::Number(64)));
+
+		let just = (*b"FRNK", Vec::new());
+		let hashof60 = hashes[59];
+		net.peer(1).client().finalize_block(hashof60, Some(just), true).unwrap();
+		// Wait for state sync.
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			if net.peer(1).client.info().finalized_state.is_some() {
+				Poll::Ready(())
+			} else {
+				Poll::Pending
+			}
+		})
+		.await;
+		assert!(!net.peer(1).client().has_state_at(&BlockId::Number(64)));
+		// Wait for the rest of the states to be imported.
+		futures::future::poll_fn::<(), _>(|cx| {
+			net.poll(cx);
+			if net.peer(1).client().has_state_at(&BlockId::Number(64)) {
+				Poll::Ready(())
+			} else {
+				Poll::Pending
+			}
+		})
+		.await;
+	}
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_indexed_blocks() {
+	use sp_runtime::traits::Hash;
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	let mut n: u64 = 0;
+	net.add_full_peer_with_config(FullPeerConfig { storage_chain: true, ..Default::default() });
+	net.add_full_peer_with_config(FullPeerConfig {
+		storage_chain: true,
+		sync_mode: SyncMode::LightState { skip_proofs: false, storage_chain_mode: true },
+		..Default::default()
+	});
+	net.peer(0).generate_blocks_at(
+		BlockId::number(0),
+		64,
+		BlockOrigin::Own,
+		|mut builder| {
+			let ex = ExtrinsicBuilder::new_indexed_call(n.to_le_bytes().to_vec()).nonce(n).build();
+			n += 1;
+			builder.push(ex).unwrap();
+			builder.build().unwrap().block
+		},
+		false,
+		true,
+		true,
+		ForkChoiceStrategy::LongestChain,
+	);
+	let indexed_key = sp_runtime::traits::BlakeTwo256::hash(&42u64.to_le_bytes());
+	assert!(net
+		.peer(0)
+		.client()
+		.as_client()
+		.indexed_transaction(indexed_key)
+		.unwrap()
+		.is_some());
+	assert!(net
+		.peer(1)
+		.client()
+		.as_client()
+		.indexed_transaction(indexed_key)
+		.unwrap()
+		.is_none());
+
+	net.run_until_sync().await;
+	assert!(net
+		.peer(1)
+		.client()
+		.as_client()
+		.indexed_transaction(indexed_key)
+		.unwrap()
+		.is_some());
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn warp_sync() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	// Create 3 synced peers and 1 peer trying to warp sync.
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(FullPeerConfig {
+		sync_mode: SyncMode::Warp,
+		..Default::default()
+	});
+	let gap_end = net.peer(0).push_blocks(63, false).pop().unwrap();
+	let target = net.peer(0).push_blocks(1, false).pop().unwrap();
+	net.peer(1).push_blocks(64, false);
+	net.peer(2).push_blocks(64, false);
+	// Wait for peer 3 to sync state.
+	net.run_until_sync().await;
+	// Make sure it was not a full sync.
+	assert!(!net.peer(3).client().has_state_at(&BlockId::Number(1)));
+	// Make sure warp sync was successful.
+	assert!(net.peer(3).client().has_state_at(&BlockId::Number(64)));
+
+	// Wait for peer 3 to download block history (gap sync).
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		if net.peer(3).has_body(gap_end) && net.peer(3).has_body(target) {
+			Poll::Ready(())
+		} else {
+			Poll::Pending
+		}
+	})
+	.await;
+}
+
+/// If there is a finalized state in the DB, warp sync falls back to full sync.
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn warp_sync_failover_to_full_sync() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	// Create 3 synced peers and 1 peer trying to warp sync.
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(FullPeerConfig {
+		sync_mode: SyncMode::Warp,
+		// We want some finalized state in the DB to make warp sync impossible.
+		force_genesis: true,
+		..Default::default()
+	});
+	net.peer(0).push_blocks(64, false);
+	net.peer(1).push_blocks(64, false);
+	net.peer(2).push_blocks(64, false);
+	// Even though we requested peer 3 to warp sync, it'll fall back to full sync if there is
+	// a finalized state in the DB.
+	assert!(net.peer(3).client().info().finalized_state.is_some());
+	// Wait for peer 3 to sync.
+	net.run_until_sync().await;
+	// Make sure it was a full sync (peer 3 has state for all blocks).
+	(1..65)
+		.into_iter()
+		.for_each(|i| assert!(net.peer(3).client().has_state_at(&BlockId::Number(i as u64))));
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn warp_sync_to_target_block() {
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(0);
+	// Create 3 synced peers and 1 peer trying to warp sync.
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+	net.add_full_peer_with_config(Default::default());
+
+	let blocks = net.peer(0).push_blocks(64, false);
+	let target = blocks[63];
+	net.peer(1).push_blocks(64, false);
+	net.peer(2).push_blocks(64, false);
+
+	let target_block = net.peer(0).client.header(target).unwrap().unwrap();
+
+	net.add_full_peer_with_config(FullPeerConfig {
+		sync_mode: SyncMode::Warp,
+		target_block: Some(target_block),
+		..Default::default()
+	});
+
+	net.run_until_sync().await;
+	assert!(net.peer(3).client().has_state_at(&BlockId::Number(64)));
+
+	// Wait for peer 1 download block history
+	futures::future::poll_fn::<(), _>(|cx| {
+		net.poll(cx);
+		let peer = net.peer(3);
+		if blocks.iter().all(|b| peer.has_body(*b)) {
+			Poll::Ready(())
+		} else {
+			Poll::Pending
+		}
+	})
+	.await;
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn syncs_huge_blocks() {
+	use sp_core::storage::well_known_keys::HEAP_PAGES;
+	use sp_runtime::codec::Encode;
+	use substrate_test_runtime_client::BlockBuilderExt;
+
+	sp_tracing::try_init_simple();
+	let mut net = TestNet::new(2);
+
+	// Increase heap space for bigger blocks.
+	net.peer(0).generate_blocks(1, BlockOrigin::Own, |mut builder| {
+		builder.push_storage_change(HEAP_PAGES.to_vec(), Some(256u64.encode())).unwrap();
+		builder.build().unwrap().block
+	});
+
+	let mut nonce = 0;
+	net.peer(0).generate_blocks(32, BlockOrigin::Own, |mut builder| {
+		// Add 32 extrinsics 32k each = 1MiB total
+		for _ in 0..32u64 {
+			let ex = ExtrinsicBuilder::new_include_data(vec![42u8; 32 * 1024]).nonce(nonce).build();
+			builder.push(ex).unwrap();
+			nonce += 1;
+		}
+		builder.build().unwrap().block
+	});
+
+	net.run_until_sync().await;
+	assert_eq!(net.peer(0).client.info().best_number, 33);
+	assert_eq!(net.peer(1).client.info().best_number, 33);
+}
diff --git a/shell-configs/local-testnet-0.toml b/shell-configs/local-testnet-0.toml
new file mode 100644
index 000000000..649852700
--- /dev/null
+++ b/shell-configs/local-testnet-0.toml
@@ -0,0 +1,6 @@
+bind_ip = "0.0.0.0"
+bind_port = 30555
+bootnodes = []
+# 12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN
+node_key = "0000000000000000000000000000000000000000000000000000000000000000"
+keystore_path = "../tangle/tmp/alice/chains/local_testnet/keystore"
diff --git a/shell-configs/local-testnet-1.toml b/shell-configs/local-testnet-1.toml
new file mode 100644
index 000000000..8b37a526b
--- /dev/null
+++ b/shell-configs/local-testnet-1.toml
@@ -0,0 +1,6 @@
+bind_ip = "0.0.0.0"
+bind_port = 30556
+bootnodes = []
+# 12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp
+node_key = "0000000000000000000000000000000000000000000000000000000000000001"
+keystore_path = "../tangle/tmp/bob/chains/local_testnet/keystore"
diff --git a/shell-configs/local-testnet-2.toml b/shell-configs/local-testnet-2.toml
new file mode 100644
index 000000000..721ad999a
--- /dev/null
+++ b/shell-configs/local-testnet-2.toml
@@ -0,0 +1,6 @@
+bind_ip = "0.0.0.0"
+bind_port = 30557
+bootnodes = []
+# 12D3KooWHdiAxVd8uMQR1hGWXccidmfCwLqcMpGwR6QcTP6QRMuD
+node_key = "0000000000000000000000000000000000000000000000000000000000000002"
+keystore_path = "../tangle/tmp/charlie/chains/local_testnet/keystore"
diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs
index daabc18bd..3a00c72d5 100644
--- a/test-utils/src/lib.rs
+++ b/test-utils/src/lib.rs
@@ -40,6 +40,7 @@ pub async fn wait_for_job_completion(
     mock::MaxSignatureLen,
     mock::MaxSubmissionLen,
     mock::MaxProofLen,
+    mock::MaxAdditionalParamsLen,
 > {
     loop {
         tokio::time::sleep(Duration::from_millis(100)).await;
@@ -158,6 +159,7 @@ macro_rules! generate_signing_and_keygen_tss_tests {
                             ttl: 100,
                             job_type: JobType::DKGTSSPhaseTwo(DKGTSSPhaseTwoJobType {
                                 phase_one_id: keygen_job_id,
+                                derivation_path: None,
                                 submission: Vec::from("Hello, world!").try_into().unwrap(),
                                 role_type: $threshold_sig_ty,
                             }),
diff --git a/test-utils/src/mock.rs b/test-utils/src/mock.rs
index cfd03f2fa..9a2a3776c 100644
--- a/test-utils/src/mock.rs
+++ b/test-utils/src/mock.rs
@@ -122,11 +122,19 @@ impl pallet_timestamp::Config for Runtime {
 
 pub struct JobToFeeHandler;
 
-impl JobToFee<AccountId, BlockNumber, MaxParticipants, MaxSubmissionLen> for JobToFeeHandler {
+impl JobToFee<AccountId, BlockNumber, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>
+    for JobToFeeHandler
+{
     type Balance = Balance;
 
     fn job_to_fee(
-        job: &JobSubmission<AccountId, BlockNumber, MaxParticipants, MaxSubmissionLen>,
+        job: &JobSubmission<
+            AccountId,
+            BlockNumber,
+            MaxParticipants,
+            MaxSubmissionLen,
+            MaxAdditionalParamsLen,
+        >,
     ) -> Balance {
         match job.job_type {
             JobType::DKGTSSPhaseOne(_)
@@ -197,6 +205,7 @@ impl
         MaxDataLen,
         MaxSignatureLen,
         MaxProofLen,
+        MaxAdditionalParamsLen,
     > for MockMPCHandler
 {
     fn verify(
@@ -208,6 +217,7 @@ impl
             MaxDataLen,
             MaxSignatureLen,
             MaxProofLen,
+            MaxAdditionalParamsLen,
         >,
     ) -> DispatchResult {
         match data.result {
@@ -264,6 +274,9 @@ parameter_types! {
     #[derive(Clone, RuntimeDebug, Eq, PartialEq, TypeInfo, Encode, Decode)]
     #[derive(Serialize, Deserialize)]
     pub const MaxRolesPerValidator: u32 = 100;
+    #[derive(Clone, RuntimeDebug, Eq, PartialEq, TypeInfo, Encode, Decode)]
+    #[derive(Serialize, Deserialize)]
+    pub const MaxAdditionalParamsLen: u32 = 64 * MB;
 }
 
 impl pallet_jobs::Config for Runtime {
@@ -283,6 +296,7 @@ impl pallet_jobs::Config for Runtime {
     type PalletId = JobsPalletId;
     type WeightInfo = ();
     type MisbehaviorHandler = MockMisbehaviorHandler;
+    type MaxAdditionalParamsLen = MaxAdditionalParamsLen;
 }
 
 pub struct MockMisbehaviorHandler;
@@ -304,6 +318,7 @@ impl pallet_dkg::Config for Runtime {
     type MaxKeyLen = MaxKeyLen;
     type MaxProofLen = MaxProofLen;
     type WeightInfo = ();
+    type MaxAdditionalParamsLen = MaxAdditionalParamsLen;
 }
 
 impl pallet_zksaas::Config for Runtime {
@@ -319,6 +334,7 @@ impl pallet_zksaas::Config for Runtime {
     type MaxKeyLen = MaxKeyLen;
     type MaxProofLen = MaxProofLen;
     type WeightInfo = ();
+    type MaxAdditionalParamsLen = MaxAdditionalParamsLen;
 }
 
 construct_runtime!(
@@ -334,20 +350,20 @@ construct_runtime!(
 );
 
 sp_api::mock_impl_runtime_apis! {
-    impl pallet_jobs_rpc_runtime_api::JobsApi<Block, AccountId, MaxParticipants, MaxSubmissionLen, MaxKeyLen, MaxDataLen, MaxSignatureLen, MaxProofLen> for Runtime {
-        fn query_jobs_by_validator(&self, validator: AccountId) -> Option<Vec<RpcResponseJobsData<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxSubmissionLen>>> {
+    impl pallet_jobs_rpc_runtime_api::JobsApi<Block, AccountId, MaxParticipants, MaxSubmissionLen, MaxKeyLen, MaxDataLen, MaxSignatureLen, MaxProofLen, MaxAdditionalParamsLen> for Runtime {
+        fn query_jobs_by_validator(&self, validator: AccountId) -> Option<Vec<RpcResponseJobsData<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>>> {
             TEST_EXTERNALITIES.lock().as_ref().unwrap().execute_with(move || {
                 Jobs::query_jobs_by_validator(validator)
             })
         }
 
-        fn query_job_by_id(role_type: RoleType, job_id: JobId) -> Option<RpcResponseJobsData<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxSubmissionLen>> {
+        fn query_job_by_id(role_type: RoleType, job_id: JobId) -> Option<RpcResponseJobsData<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxSubmissionLen, MaxAdditionalParamsLen>> {
             TEST_EXTERNALITIES.lock().as_ref().unwrap().execute_with(move || {
                 Jobs::query_job_by_id(role_type, job_id)
             })
         }
 
-        fn query_job_result(role_type: RoleType, job_id: JobId) -> Option<PhaseResult<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxKeyLen, MaxDataLen, MaxSignatureLen, MaxSubmissionLen, MaxProofLen>> {
+        fn query_job_result(role_type: RoleType, job_id: JobId) -> Option<PhaseResult<AccountId, BlockNumberOf<Block>, MaxParticipants, MaxKeyLen, MaxDataLen, MaxSignatureLen, MaxSubmissionLen, MaxProofLen, MaxAdditionalParamsLen>> {
             TEST_EXTERNALITIES.lock().as_ref().unwrap().execute_with(move || {
                 Jobs::query_job_result(role_type, job_id)
             })
@@ -687,6 +703,7 @@ pub mod mock_wrapper_client {
         MaxDataLen, MaxKeyLen, MaxParticipants, MaxProofLen, MaxSignatureLen, MaxSubmissionLen,
     };
 
+    use gadget_common::prelude::MaxAdditionalParamsLen;
     use gadget_core::gadget::substrate::{self, Client};
     use pallet_jobs_rpc_runtime_api::JobsApi;
     use parity_scale_codec::{Decode, Encode};
@@ -792,6 +809,7 @@ pub mod mock_wrapper_client {
             super::MaxDataLen,
             super::MaxSignatureLen,
             super::MaxProofLen,
+            super::MaxAdditionalParamsLen,
         >,
     {
         async fn query_jobs_by_validator(
@@ -800,7 +818,15 @@ pub mod mock_wrapper_client {
             validator: AccountId32,
         ) -> Result<
             Option<
-                Vec<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>,
+                Vec<
+                    jobs::RpcResponseJobsData<
+                        AccountId32,
+                        u64,
+                        MaxParticipants,
+                        MaxSubmissionLen,
+                        MaxAdditionalParamsLen,
+                    >,
+                >,
             >,
             gadget_common::Error,
         > {
@@ -828,7 +854,15 @@ pub mod mock_wrapper_client {
             role_type: roles::RoleType,
             job_id: u64,
         ) -> Result<
-            Option<jobs::RpcResponseJobsData<AccountId32, u64, MaxParticipants, MaxSubmissionLen>>,
+            Option<
+                jobs::RpcResponseJobsData<
+                    AccountId32,
+                    u64,
+                    MaxParticipants,
+                    MaxSubmissionLen,
+                    MaxAdditionalParamsLen,
+                >,
+            >,
             gadget_common::Error,
         > {
             let at = Decode::decode(&mut Encode::encode(&at).as_slice()).unwrap();
@@ -860,6 +894,7 @@ pub mod mock_wrapper_client {
                     MaxSignatureLen,
                     MaxSubmissionLen,
                     MaxProofLen,
+                    MaxAdditionalParamsLen,
                 >,
             >,
             gadget_common::Error,
@@ -960,6 +995,7 @@ pub mod mock_wrapper_client {
                 MaxSignatureLen,
                 MaxDataLen,
                 MaxProofLen,
+                MaxAdditionalParamsLen,
             >,
         ) -> Result<(), gadget_common::Error> {
             let id = self.id.clone();