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

Issue:
[TASK] Multinode Test Harness w/ Dynamic add/drop support
Our test harness currently works for `N=1` nodes. For testing blueprints that only need 1 node, what we currently have works well and follows a composable design (as opposed to a "black box" design we use to have).

Now, we need to get `N=n` nodes working. The same composability should still apply, and should be reverse-compatible with existing tests to not cause interruption.

Additionally, we should add the ability to add and drop nodes on command.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 32e4d8c76..a305977be 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -167,6 +167,7 @@ jobs:
             gadget-networking,
             gadget-executor,
             gadget-utils-tangle,
+            gadget-tangle-testing-utils,
           ]
     steps:
       - name: checkout code
diff --git a/.gitignore b/.gitignore
index 0e1057e4f..6668e6c2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,6 @@ node_modules
 
 blueprints/**/dependencies/
 blueprint.lock
+blueprint.json
+cache
 target
diff --git a/Cargo.lock b/Cargo.lock
index fae7a8b1d..26f2d13f7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -155,13 +155,13 @@ dependencies = [
 
 [[package]]
 name = "alloy-chains"
-version = "0.1.57"
+version = "0.1.62"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ab9d1367c6ffb90c93fb4a9a4989530aa85112438c6f73a734067255d348469"
+checksum = "1317fde6d2d3cd6082a15144c23230697a5e1a91a27d1facc146715d3b4b2046"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "num_enum",
- "strum 0.26.3",
+ "strum 0.27.1",
 ]
 
 [[package]]
@@ -171,7 +171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f4138dc275554afa6f18c4217262ac9388790b2fc393c2dfe03c51d357abf013"
 dependencies = [
  "alloy-eips",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "alloy-serde",
  "alloy-trie",
@@ -190,7 +190,7 @@ checksum = "0fa04e1882c31288ce1028fdf31b6ea94cfa9eafa2e497f903ded631c8c6a42c"
 dependencies = [
  "alloy-consensus",
  "alloy-eips",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "alloy-serde",
  "serde",
@@ -206,11 +206,11 @@ dependencies = [
  "alloy-json-abi",
  "alloy-network",
  "alloy-network-primitives",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-pubsub",
  "alloy-rpc-types-eth",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "alloy-transport",
  "futures",
  "futures-util",
@@ -219,33 +219,33 @@ dependencies = [
 
 [[package]]
 name = "alloy-core"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "648275bb59110f88cc5fa9a176845e52a554ebfebac2d21220bcda8c9220f797"
+checksum = "482f377cebceed4bb1fb5e7970f0805e2ab123d06701be9351b67ed6341e74aa"
 dependencies = [
  "alloy-dyn-abi",
  "alloy-json-abi",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
 ]
 
 [[package]]
 name = "alloy-dyn-abi"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc9138f4f0912793642d453523c3116bd5d9e11de73b70177aa7cb3e94b98ad2"
+checksum = "555896f0b8578adb522b1453b6e6cc6704c3027bd0af20058befdde992cee8e9"
 dependencies = [
  "alloy-json-abi",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-sol-type-parser",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "const-hex",
  "derive_more 1.0.0",
  "itoa",
  "serde",
  "serde_json",
- "winnow",
+ "winnow 0.7.3",
 ]
 
 [[package]]
@@ -254,7 +254,7 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "serde",
 ]
@@ -265,7 +265,7 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cabf647eb4650c91a9d38cb6f972bb320009e7e9d61765fb688a86f1563b33e8"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "derive_more 1.0.0",
  "k256",
@@ -280,7 +280,7 @@ checksum = "52dd5869ed09e399003e0e0ec6903d981b2a92e74c5d37e6b40890bad2517526"
 dependencies = [
  "alloy-eip2930",
  "alloy-eip7702",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "alloy-serde",
  "c-kzg",
@@ -297,7 +297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e7d2a7fe5c1a9bd6793829ea21a636f30fc2b3f5d2e7418ba86d96e41dd1f460"
 dependencies = [
  "alloy-eips",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-serde",
  "alloy-trie",
  "serde",
@@ -305,11 +305,11 @@ dependencies = [
 
 [[package]]
 name = "alloy-json-abi"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24acd2f5ba97c7a320e67217274bc81fe3c3174b8e6144ec875d9d54e760e278"
+checksum = "4012581681b186ba0882007ed873987cc37f86b1b488fe6b91d5efd0b585dc41"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-sol-type-parser",
  "serde",
  "serde_json",
@@ -321,8 +321,8 @@ version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2008bedb8159a255b46b7c8614516eda06679ea82f620913679afbd8031fea72"
 dependencies = [
- "alloy-primitives 0.8.20",
- "alloy-sol-types 0.8.19",
+ "alloy-primitives 0.8.21",
+ "alloy-sol-types 0.8.21",
  "serde",
  "serde_json",
  "thiserror 2.0.11",
@@ -340,12 +340,12 @@ dependencies = [
  "alloy-eips",
  "alloy-json-rpc",
  "alloy-network-primitives",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rpc-types-any",
  "alloy-rpc-types-eth",
  "alloy-serde",
  "alloy-signer",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "async-trait",
  "auto_impl",
  "futures-utils-wasm",
@@ -362,7 +362,7 @@ checksum = "f31c3c6b71340a1d076831823f09cb6e02de01de5c6630a9631bdb36f947ff80"
 dependencies = [
  "alloy-consensus",
  "alloy-eips",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-serde",
  "serde",
 ]
@@ -374,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4520cd4bc5cec20c32c98e4bc38914c7fb96bf4a712105e44da186a54e65e3ba"
 dependencies = [
  "alloy-genesis",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "k256",
  "rand 0.8.5",
  "serde_json",
@@ -394,7 +394,7 @@ dependencies = [
  "bytes",
  "cfg-if 1.0.0",
  "const-hex",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
  "hex-literal",
  "itoa",
  "proptest",
@@ -406,9 +406,9 @@ dependencies = [
 
 [[package]]
 name = "alloy-primitives"
-version = "0.8.20"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc1360603efdfba91151e623f13a4f4d3dc4af4adc1cbd90bf37c81e84db4c77"
+checksum = "478bedf4d24e71ea48428d1bc278553bd7c6ae07c30ca063beb0b09fe58a9e74"
 dependencies = [
  "alloy-rlp",
  "bytes",
@@ -425,7 +425,7 @@ dependencies = [
  "proptest",
  "rand 0.8.5",
  "ruint",
- "rustc-hash 2.1.0",
+ "rustc-hash 2.1.1",
  "serde",
  "sha3",
  "tiny-keccak",
@@ -443,7 +443,7 @@ dependencies = [
  "alloy-json-rpc",
  "alloy-network",
  "alloy-network-primitives",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-pubsub",
  "alloy-rpc-client",
  "alloy-rpc-types-eth",
@@ -459,7 +459,7 @@ dependencies = [
  "futures-utils-wasm",
  "lru 0.12.5",
  "parking_lot 0.12.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "reqwest 0.12.12",
  "schnellru",
  "serde",
@@ -478,7 +478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2269fd635f7b505f27c63a3cb293148cd02301efce4c8bdd9ff54fbfc4a20e23"
 dependencies = [
  "alloy-json-rpc",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-transport",
  "bimap",
  "futures",
@@ -519,14 +519,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d06a292b37e182e514903ede6e623b9de96420e8109ce300da288a96d88b7e4b"
 dependencies = [
  "alloy-json-rpc",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-pubsub",
  "alloy-transport",
  "alloy-transport-http",
  "alloy-transport-ipc",
  "alloy-transport-ws",
  "futures",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "reqwest 0.12.12",
  "serde",
  "serde_json",
@@ -544,7 +544,7 @@ version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9383845dd924939e7ab0298bbfe231505e20928907d7905aa3bf112287305e06"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rpc-types-engine",
  "alloy-rpc-types-eth",
  "alloy-serde",
@@ -570,7 +570,7 @@ checksum = "4a5f821f30344862a0b6eb9a1c2eb91dfb2ff44c7489f37152a526cdcab79264"
 dependencies = [
  "alloy-consensus",
  "alloy-eips",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "alloy-serde",
  "derive_more 1.0.0",
@@ -588,10 +588,10 @@ dependencies = [
  "alloy-consensus-any",
  "alloy-eips",
  "alloy-network-primitives",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "alloy-serde",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "itertools 0.13.0",
  "serde",
  "serde_json",
@@ -604,7 +604,7 @@ version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae0465c71d4dced7525f408d84873aeebb71faf807d22d74c4a426430ccd9b55"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "serde",
  "serde_json",
 ]
@@ -616,8 +616,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9bfa395ad5cc952c82358d31e4c68b27bf4a89a5456d9b27e226e77dac50e4ff"
 dependencies = [
  "alloy-dyn-abi",
- "alloy-primitives 0.8.20",
- "alloy-sol-types 0.8.19",
+ "alloy-primitives 0.8.21",
+ "alloy-sol-types 0.8.21",
  "async-trait",
  "auto_impl",
  "elliptic-curve",
@@ -633,7 +633,7 @@ checksum = "0eb06810c34427d499863817eb506acf57cb9ded9224b374116cae4e22dbd4e9"
 dependencies = [
  "alloy-consensus",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer",
  "async-trait",
  "aws-sdk-kms",
@@ -651,7 +651,7 @@ checksum = "d629e63fec8802ad53706d46e8eceeeae2b135c6648d0de41669a523bf17df4a"
 dependencies = [
  "alloy-consensus",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer",
  "async-trait",
  "gcloud-sdk",
@@ -670,9 +670,9 @@ dependencies = [
  "alloy-consensus",
  "alloy-dyn-abi",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "async-trait",
  "coins-ledger",
  "futures-util",
@@ -689,7 +689,7 @@ checksum = "fbdc63ce9eda1283fcbaca66ba4a414b841c0e3edbeef9c86a71242fc9e84ccc"
 dependencies = [
  "alloy-consensus",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer",
  "async-trait",
  "coins-bip32 0.12.0",
@@ -718,9 +718,9 @@ dependencies = [
 
 [[package]]
 name = "alloy-sol-macro"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d039d267aa5cbb7732fa6ce1fd9b5e9e29368f580f80ba9d7a8450c794de4b2"
+checksum = "a2708e27f58d747423ae21d31b7a6625159bd8d867470ddd0256f396a68efa11"
 dependencies = [
  "alloy-sol-macro-expander",
  "alloy-sol-macro-input",
@@ -732,9 +732,9 @@ dependencies = [
 
 [[package]]
 name = "alloy-sol-macro-expander"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "620ae5eee30ee7216a38027dec34e0585c55099f827f92f50d11e3d2d3a4a954"
+checksum = "c6b7984d7e085dec382d2c5ef022b533fcdb1fe6129200af30ebf5afddb6a361"
 dependencies = [
  "alloy-json-abi",
  "alloy-sol-macro-input",
@@ -745,15 +745,15 @@ dependencies = [
  "proc-macro2",
  "quote",
  "syn 2.0.98",
- "syn-solidity 0.8.19",
+ "syn-solidity 0.8.21",
  "tiny-keccak",
 ]
 
 [[package]]
 name = "alloy-sol-macro-input"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad9f7d057e00f8c5994e4ff4492b76532c51ead39353aa2ed63f8c50c0f4d52e"
+checksum = "33d6a9fc4ed1a3c70bdb2357bec3924551c1a59f24e5a04a74472c755b37f87d"
 dependencies = [
  "alloy-json-abi",
  "const-hex",
@@ -763,17 +763,17 @@ dependencies = [
  "quote",
  "serde_json",
  "syn 2.0.98",
- "syn-solidity 0.8.19",
+ "syn-solidity 0.8.21",
 ]
 
 [[package]]
 name = "alloy-sol-type-parser"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74e60b084fe1aef8acecda2743ff2d93c18ff3eb67a2d3b12f62582a1e66ef5e"
+checksum = "1b1b3e9a48a6dd7bb052a111c8d93b5afc7956ed5e2cb4177793dc63bb1d2a36"
 dependencies = [
  "serde",
- "winnow",
+ "winnow 0.7.3",
 ]
 
 [[package]]
@@ -790,13 +790,13 @@ dependencies = [
 
 [[package]]
 name = "alloy-sol-types"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1382302752cd751efd275f4d6ef65877ddf61e0e6f5ac84ef4302b79a33a31a"
+checksum = "6044800da35c38118fd4b98e18306bd3b91af5dedeb54c1b768cf1b4fb68f549"
 dependencies = [
  "alloy-json-abi",
- "alloy-primitives 0.8.20",
- "alloy-sol-macro 0.8.19",
+ "alloy-primitives 0.8.21",
+ "alloy-sol-macro 0.8.21",
  "const-hex",
  "serde",
 ]
@@ -848,7 +848,7 @@ dependencies = [
  "bytes",
  "futures",
  "interprocess",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "serde_json",
  "tokio",
  "tokio-util 0.7.13",
@@ -865,7 +865,7 @@ dependencies = [
  "alloy-transport",
  "futures",
  "http 1.2.0",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "serde_json",
  "tokio",
  "tokio-tungstenite 0.24.0",
@@ -875,11 +875,11 @@ dependencies = [
 
 [[package]]
 name = "alloy-trie"
-version = "0.7.8"
+version = "0.7.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6917c79e837aa7b77b7a6dae9f89cbe15313ac161c4d3cfaf8909ef21f3d22d8"
+checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-rlp",
  "arrayvec 0.7.6",
  "derive_more 1.0.0",
@@ -956,9 +956,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.95"
+version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
 
 [[package]]
 name = "anymap2"
@@ -1870,11 +1870,11 @@ dependencies = [
 
 [[package]]
 name = "auth-git2"
-version = "0.5.5"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3810b5af212b013fe7302b12d86616c6c39a48e18f2e4b812a5a9e5710213791"
+checksum = "d55eead120c93036f531829cf9b85830a474e75ce71169680879d28078321ddc"
 dependencies = [
- "dirs",
+ "dirs 6.0.0",
  "git2",
  "terminal-prompt",
 ]
@@ -1898,9 +1898,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 
 [[package]]
 name = "aws-config"
-version = "1.5.15"
+version = "1.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc47e70fc35d054c8fcd296d47a61711f043ac80534a10b4f741904f81e73a90"
+checksum = "50236e4d60fe8458de90a71c0922c761e41755adf091b1b03de1cef537179915"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -1935,9 +1935,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.12.2"
+version = "1.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
+checksum = "4cd755adf9707cf671e31d944a189be3deaaeee11c8bc1d669bb8022ac90fbd0"
 dependencies = [
  "aws-lc-sys",
  "paste",
@@ -1946,9 +1946,9 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.25.0"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491"
+checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43"
 dependencies = [
  "bindgen",
  "cc",
@@ -1960,9 +1960,9 @@ dependencies = [
 
 [[package]]
 name = "aws-runtime"
-version = "1.5.4"
+version = "1.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee7643696e7fdd74c10f9eb42848a87fe469d35eae9c3323f80aa98f350baac"
+checksum = "76dd04d39cc12844c0994f2c9c5a6f5184c22e9188ec1ff723de41910a21dcad"
 dependencies = [
  "aws-credential-types",
  "aws-sigv4",
@@ -1980,14 +1980,14 @@ dependencies = [
  "percent-encoding",
  "pin-project-lite",
  "tracing",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
 name = "aws-sdk-kms"
-version = "1.58.0"
+version = "1.60.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b7a24700ac548025a47a5c579886f5198895bb1eccd8964dfd71cd66c16912"
+checksum = "adc36035f7393a24719069c9a2f52e20972f7ee8472bd788e863968736acc449"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -2007,9 +2007,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sdk-sts"
-version = "1.57.0"
+version = "1.60.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "115fd4fb663817ed595a5ee4f1649d7aacd861d47462323cb37576ce89271b93"
+checksum = "dc1cfe5e16b90421ea031f4c6348b534ef442e76f6bf4a1b2b592c12cc2c6af9"
 dependencies = [
  "aws-credential-types",
  "aws-runtime",
@@ -2030,9 +2030,9 @@ dependencies = [
 
 [[package]]
 name = "aws-sigv4"
-version = "1.2.7"
+version = "1.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "690118821e46967b3c4501d67d7d52dd75106a9c54cf36cefa1985cedbe94e05"
+checksum = "9bfe75fad52793ce6dec0dc3d4b1f388f038b5eb866c8d4d7f3a8e21b5ea5051"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-http",
@@ -2103,9 +2103,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-runtime"
-version = "1.7.7"
+version = "1.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "865f7050bbc7107a6c98a397a9fcd9413690c27fa718446967cf03b2d3ac517e"
+checksum = "d526a12d9ed61fadefda24abe2e682892ba288c2018bcb38b1b4c111d13f6d92"
 dependencies = [
  "aws-smithy-async",
  "aws-smithy-http",
@@ -2147,9 +2147,9 @@ dependencies = [
 
 [[package]]
 name = "aws-smithy-types"
-version = "1.2.12"
+version = "1.2.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a28f6feb647fb5e0d5b50f0472c19a7db9462b74e2fec01bb0b44eedcc834e97"
+checksum = "c7b8a53819e42f10d0821f56da995e1470b199686a1809168db6ca485665f042"
 dependencies = [
  "base64-simd",
  "bytes",
@@ -2182,9 +2182,9 @@ dependencies = [
 
 [[package]]
 name = "aws-types"
-version = "1.3.4"
+version = "1.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0df5a18c4f951c645300d365fec53a61418bcf4650f604f85fe2a665bfaa0c2"
+checksum = "dfbd0a668309ec1f66c0f6bda4840dd6d4796ae26d699ebc266d7cc95c6d040f"
 dependencies = [
  "aws-credential-types",
  "aws-smithy-async",
@@ -2489,9 +2489,9 @@ dependencies = [
 
 [[package]]
 name = "blake2b_simd"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780"
+checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
 dependencies = [
  "arrayref",
  "arrayvec 0.7.6",
@@ -2500,9 +2500,9 @@ dependencies = [
 
 [[package]]
 name = "blake3"
-version = "1.5.5"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e"
+checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937"
 dependencies = [
  "arrayref",
  "arrayvec 0.7.6",
@@ -2544,9 +2544,9 @@ dependencies = [
 
 [[package]]
 name = "blst"
-version = "0.3.13"
+version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874"
+checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d"
 dependencies = [
  "cc",
  "glob",
@@ -2570,7 +2570,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tempfile",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -2600,7 +2600,7 @@ dependencies = [
  "tangle-subxt",
  "thiserror 2.0.11",
  "tokio",
- "toml 0.8.19",
+ "toml 0.8.20",
  "tracing",
  "tracing-subscriber 0.3.19",
 ]
@@ -2629,7 +2629,6 @@ dependencies = [
  "blueprint-build-utils",
  "blueprint-metadata",
  "eigensdk",
- "gadget-client-networking",
  "gadget-clients",
  "gadget-config",
  "gadget-context-derive",
@@ -2641,6 +2640,7 @@ dependencies = [
  "gadget-logging",
  "gadget-macros",
  "gadget-networking",
+ "gadget-networking-round-based-extension",
  "gadget-runners",
  "gadget-std",
  "gadget-stores",
@@ -2664,9 +2664,9 @@ dependencies = [
 
 [[package]]
 name = "bollard"
-version = "0.17.1"
+version = "0.18.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a"
+checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30"
 dependencies = [
  "base64 0.22.1",
  "bollard-stubs",
@@ -2677,15 +2677,15 @@ dependencies = [
  "home",
  "http 1.2.0",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-named-pipe",
  "hyper-rustls 0.27.5",
  "hyper-util",
  "hyperlocal",
  "log",
  "pin-project-lite",
- "rustls 0.23.21",
- "rustls-native-certs 0.7.3",
+ "rustls 0.23.23",
+ "rustls-native-certs 0.8.1",
  "rustls-pemfile 2.2.0",
  "rustls-pki-types",
  "serde",
@@ -2693,7 +2693,7 @@ dependencies = [
  "serde_json",
  "serde_repr",
  "serde_urlencoded",
- "thiserror 1.0.69",
+ "thiserror 2.0.11",
  "tokio",
  "tokio-util 0.7.13",
  "tower-service",
@@ -2703,9 +2703,9 @@ dependencies = [
 
 [[package]]
 name = "bollard-stubs"
-version = "1.45.0-rc.26.0.1"
+version = "1.47.1-rc.27.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4"
+checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da"
 dependencies = [
  "serde",
  "serde_repr",
@@ -2714,9 +2714,9 @@ dependencies = [
 
 [[package]]
 name = "bounded-collections"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d077619e9c237a5d1875166f5e8033e8f6bff0c96f8caf81e1c2d7738c431bf"
+checksum = "32ed0a820ed50891d36358e997d27741a6142e382242df40ff01c89bcdcc7a2b"
 dependencies = [
  "log",
  "parity-scale-codec",
@@ -2969,9 +2969,9 @@ dependencies = [
 
 [[package]]
 name = "bridge-runtime-common"
-version = "0.18.2"
+version = "0.18.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86cf718057e18ce3e5f2c8e3fc318c38ad71d47ada91dc4b841c0f69c214ef04"
+checksum = "789eb7841c8791991317ec4b6e56c119e5e1c2e480ad293b8502736fd7f64b2e"
 dependencies = [
  "bp-header-chain",
  "bp-messages",
@@ -2979,7 +2979,6 @@ dependencies = [
  "bp-polkadot-core",
  "bp-relayers",
  "bp-runtime",
- "bp-xcm-bridge-hub",
  "frame-support",
  "frame-system",
  "log",
@@ -3031,9 +3030,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.16.0"
+version = "3.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "byte-slice-cast"
@@ -3055,9 +3054,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
 dependencies = [
  "serde",
 ]
@@ -3084,9 +3083,9 @@ dependencies = [
 
 [[package]]
 name = "bzip2-sys"
-version = "0.1.11+1.0.8"
+version = "0.1.12+1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
+checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9"
 dependencies = [
  "cc",
  "libc",
@@ -3157,7 +3156,7 @@ dependencies = [
  "tempfile",
  "thiserror 2.0.11",
  "time",
- "toml 0.8.19",
+ "toml 0.8.20",
  "walkdir",
 ]
 
@@ -3178,7 +3177,7 @@ dependencies = [
  "alloy-dyn-abi",
  "alloy-json-abi",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-rpc-types-eth",
  "alloy-signer-local",
@@ -3215,23 +3214,23 @@ dependencies = [
  "thiserror 2.0.11",
  "tnt-bls",
  "tokio",
- "toml 0.8.19",
+ "toml 0.8.20",
  "tracing",
  "tracing-subscriber 0.3.19",
 ]
 
 [[package]]
 name = "cargo-util-schemas"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26a31f1bb58068aa01b7809533b02c26b1e64a7810ae99131da5af1a4b8e7fc2"
+checksum = "9f905f68f8cb8a8182592d9858a5895360f0a5b08b6901fdb10498fb91829804"
 dependencies = [
  "semver 1.0.25",
  "serde",
  "serde-untagged",
  "serde-value",
  "thiserror 1.0.69",
- "toml 0.8.19",
+ "toml 0.8.20",
  "unicode-xid",
  "url",
 ]
@@ -3271,7 +3270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472"
 dependencies = [
  "serde",
- "toml 0.8.19",
+ "toml 0.8.20",
 ]
 
 [[package]]
@@ -3285,9 +3284,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.2.10"
+version = "1.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
+checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
 dependencies = [
  "jobserver",
  "libc",
@@ -3413,9 +3412,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.27"
+version = "4.5.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
+checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -3433,9 +3432,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.27"
+version = "4.5.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
+checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
 dependencies = [
  "anstream",
  "anstyle",
@@ -3446,9 +3445,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.24"
+version = "4.5.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
@@ -3464,9 +3463,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "cmake"
-version = "0.1.52"
+version = "0.1.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
+checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
 dependencies = [
  "cc",
 ]
@@ -3727,6 +3726,26 @@ dependencies = [
  "tiny-keccak",
 ]
 
+[[package]]
+name = "const_format"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
 [[package]]
 name = "constant_time_eq"
 version = "0.1.5"
@@ -3786,6 +3805,17 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "core_affinity"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622892f5635ce1fc38c8f16dfc938553ed64af482edb5e150bf4caedbfcb2304"
+dependencies = [
+ "libc",
+ "num_cpus",
+ "winapi",
+]
+
 [[package]]
 name = "cpp_demangle"
 version = "0.3.5"
@@ -3797,9 +3827,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 dependencies = [
  "libc",
 ]
@@ -3920,6 +3950,15 @@ dependencies = [
  "chrono",
 ]
 
+[[package]]
+name = "crossbeam"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "crossbeam-channel"
 version = "0.5.14"
@@ -4179,9 +4218,9 @@ dependencies = [
 
 [[package]]
 name = "cumulus-pallet-xcmp-queue"
-version = "0.17.0"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f788bdac9474795ea13ba791b55798fb664b2e3da8c3a7385b480c9af4e6539"
+checksum = "cbc44222c528b88dcc6e921e7a0dc94d66b5895aab9e9d9db8798fc62f7ccd40"
 dependencies = [
  "bounded-collections",
  "bp-xcm-bridge-hub-router",
@@ -4374,9 +4413,9 @@ dependencies = [
 
 [[package]]
 name = "cxx"
-version = "1.0.137"
+version = "1.0.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fc894913dccfed0f84106062c284fa021c3ba70cb1d78797d6f5165d4492e45"
+checksum = "8bc580dceb395cae0efdde0a88f034cfd8a276897e40c693a7b87bed17971d33"
 dependencies = [
  "cc",
  "cxxbridge-cmd",
@@ -4388,9 +4427,9 @@ dependencies = [
 
 [[package]]
 name = "cxx-build"
-version = "1.0.137"
+version = "1.0.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "503b2bfb6b3e8ce7f95d865a67419451832083d3186958290cee6c53e39dfcfe"
+checksum = "49d8c1baedad72a7efda12ad8d7ad687b3e7221dfb304a12443fd69e9de8bb30"
 dependencies = [
  "cc",
  "codespan-reporting",
@@ -4402,9 +4441,9 @@ dependencies = [
 
 [[package]]
 name = "cxxbridge-cmd"
-version = "1.0.137"
+version = "1.0.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0d2cb64a95b4b5a381971482235c4db2e0208302a962acdbe314db03cbbe2fb"
+checksum = "e43afb0e3b2ef293492a31ecd796af902112460d53e5f923f7804f348a769f9c"
 dependencies = [
  "clap",
  "codespan-reporting",
@@ -4415,15 +4454,15 @@ dependencies = [
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.137"
+version = "1.0.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f797b0206463c9c2a68ed605ab28892cca784f1ef066050f4942e3de26ad885"
+checksum = "0257ad2096a2474fe877e9e055ab69603851c3d6b394efcc7e0443899c2492ce"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.137"
+version = "1.0.141"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79010a2093848e65a3e0f7062d3f02fb2ef27f866416dfe436fccfa73d3bb59"
+checksum = "b46cbd7358a46b760609f1cb5093683328e58ca50e594a308716f5403fdc03e5"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4482,15 +4521,15 @@ dependencies = [
 
 [[package]]
 name = "data-encoding"
-version = "2.7.0"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
+checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
 
 [[package]]
 name = "data-encoding-macro"
-version = "0.1.16"
+version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b"
+checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558"
 dependencies = [
  "data-encoding",
  "data-encoding-macro-internal",
@@ -4498,9 +4537,9 @@ dependencies = [
 
 [[package]]
 name = "data-encoding-macro-internal"
-version = "0.1.14"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b"
+checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f"
 dependencies = [
  "data-encoding",
  "syn 2.0.98",
@@ -4576,9 +4615,9 @@ dependencies = [
 
 [[package]]
 name = "derive_more"
-version = "0.99.18"
+version = "0.99.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
+checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f"
 dependencies = [
  "convert_case",
  "proc-macro2",
@@ -4658,7 +4697,16 @@ version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
 dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys 0.5.0",
 ]
 
 [[package]]
@@ -4679,10 +4727,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
 dependencies = [
  "libc",
  "option-ext",
- "redox_users",
+ "redox_users 0.4.6",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.5.0",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "dirs-sys-next"
 version = "0.1.2"
@@ -4690,7 +4750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
 dependencies = [
  "libc",
- "redox_users",
+ "redox_users 0.4.6",
  "winapi",
 ]
 
@@ -4728,7 +4788,7 @@ dependencies = [
  "regex",
  "syn 2.0.98",
  "termcolor",
- "toml 0.8.19",
+ "toml 0.8.20",
  "walkdir",
 ]
 
@@ -4796,9 +4856,9 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35"
 
 [[package]]
 name = "ecdsa"
@@ -4951,7 +5011,7 @@ dependencies = [
  "serde_json",
  "sha2 0.10.8",
  "thiserror 1.0.69",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -5377,9 +5437,9 @@ checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b"
 
 [[package]]
 name = "equivalent"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "erased-serde"
@@ -5545,7 +5605,7 @@ dependencies = [
  "ethers-providers",
  "futures-util",
  "once_cell",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "serde",
  "serde_json",
  "thiserror 1.0.69",
@@ -5571,7 +5631,7 @@ dependencies = [
  "serde",
  "serde_json",
  "syn 2.0.98",
- "toml 0.8.19",
+ "toml 0.8.20",
  "walkdir",
 ]
 
@@ -5685,7 +5745,7 @@ dependencies = [
  "instant",
  "jsonwebtoken 8.3.0",
  "once_cell",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "reqwest 0.11.27",
  "serde",
  "serde_json",
@@ -5728,7 +5788,7 @@ checksum = "66244a771d9163282646dbeffe0e6eca4dda4146b6498644e678ac6089b11edd"
 dependencies = [
  "cfg-if 1.0.0",
  "const-hex",
- "dirs",
+ "dirs 5.0.1",
  "dunce",
  "ethers-core",
  "glob",
@@ -5833,6 +5893,17 @@ dependencies = [
  "bytes",
 ]
 
+[[package]]
+name = "fastrlp"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4"
+dependencies = [
+ "arrayvec 0.7.6",
+ "auto_impl",
+ "bytes",
+]
+
 [[package]]
 name = "ff"
 version = "0.13.0"
@@ -5883,9 +5954,9 @@ dependencies = [
 
 [[package]]
 name = "finality-grandpa"
-version = "0.16.2"
+version = "0.16.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36530797b9bf31cd4ff126dcfee8170f86b00cfdcea3269d73133cc0415945c3"
+checksum = "b4f8f43dc520133541781ec03a8cab158ae8b7f7169cdf22e9050aa6cf0fbdfc"
 dependencies = [
  "either",
  "futures",
@@ -5904,7 +5975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2384245d85162258a14b43567a9ee3598f5ae746a1581fb5d3d2cb780f0dbf95"
 dependencies = [
  "futures-timer",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
 ]
 
 [[package]]
@@ -5932,7 +6003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.8.3",
+ "miniz_oxide 0.8.4",
 ]
 
 [[package]]
@@ -6157,13 +6228,14 @@ dependencies = [
 
 [[package]]
 name = "frame-support-procedural"
-version = "30.0.4"
+version = "30.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e8f9b6bc1517a6fcbf0b2377e5c8c6d39f5bb7862b191a59a9992081d63972d"
+checksum = "8da784d943f2a945be923ab081a7c0837355b38045c50945d7ec1a138e2f3c52"
 dependencies = [
  "Inflector",
  "cfg-expr",
  "derive-syn-parse",
+ "docify",
  "expander",
  "frame-support-procedural-tools",
  "itertools 0.11.0",
@@ -6412,7 +6484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb"
 dependencies = [
  "futures-io",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-pki-types",
 ]
 
@@ -6476,7 +6548,7 @@ name = "gadget-anvil-testing-utils"
 version = "0.1.0"
 dependencies = [
  "alloy-contract",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-rpc-types",
  "alloy-rpc-types-eth",
@@ -6558,7 +6630,7 @@ version = "0.1.0"
 dependencies = [
  "alloy-contract",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-pubsub",
  "alloy-transport",
@@ -6582,7 +6654,7 @@ dependencies = [
  "alloy-consensus",
  "alloy-json-rpc",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-pubsub",
  "alloy-rlp",
@@ -6608,22 +6680,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "gadget-client-networking"
-version = "0.1.0"
-dependencies = [
- "gadget-client-core",
- "gadget-config",
- "gadget-crypto",
- "gadget-logging",
- "gadget-networking",
- "gadget-std",
- "libp2p",
- "serde",
- "serde_json",
- "thiserror 2.0.11",
-]
-
 [[package]]
 name = "gadget-client-tangle"
 version = "0.1.0"
@@ -6653,7 +6709,6 @@ dependencies = [
  "gadget-client-core",
  "gadget-client-eigenlayer",
  "gadget-client-evm",
- "gadget-client-networking",
  "gadget-client-tangle",
  "gadget-std",
  "thiserror 2.0.11",
@@ -6663,7 +6718,7 @@ dependencies = [
 name = "gadget-config"
 version = "0.1.0"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "clap",
  "gadget-config",
  "gadget-keystore",
@@ -6822,7 +6877,7 @@ dependencies = [
 name = "gadget-crypto-k256"
 version = "0.1.0"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer-local",
  "gadget-crypto-core",
  "gadget-std",
@@ -6868,7 +6923,7 @@ dependencies = [
 name = "gadget-crypto-tangle-pair-signer"
 version = "0.1.0"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer-local",
  "gadget-crypto-core",
  "gadget-crypto-sp-core",
@@ -6890,7 +6945,7 @@ dependencies = [
  "alloy-json-abi",
  "alloy-network",
  "alloy-node-bindings",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-pubsub",
  "alloy-rpc-client",
@@ -6898,7 +6953,7 @@ dependencies = [
  "alloy-rpc-types-eth",
  "alloy-signer",
  "alloy-signer-local",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "alloy-transport",
  "alloy-transport-http",
  "serde",
@@ -6908,7 +6963,7 @@ dependencies = [
 name = "gadget-eigenlayer-testing-utils"
 version = "0.1.0"
 dependencies = [
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-rpc-types",
  "alloy-rpc-types-eth",
@@ -6949,6 +7004,7 @@ name = "gadget-event-listeners-core"
 version = "0.1.0"
 dependencies = [
  "async-trait",
+ "auto_impl",
  "futures",
  "gadget-logging",
  "gadget-std",
@@ -6977,7 +7033,7 @@ dependencies = [
  "alloy-network",
  "alloy-provider",
  "alloy-rpc-types",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "alloy-transport",
  "async-trait",
  "gadget-event-listeners-core",
@@ -6986,7 +7042,7 @@ dependencies = [
  "gadget-stores",
  "thiserror 2.0.11",
  "tokio",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -7031,7 +7087,7 @@ name = "gadget-keystore"
 version = "0.1.0"
 dependencies = [
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-signer",
  "alloy-signer-aws",
  "alloy-signer-gcp",
@@ -7116,13 +7172,19 @@ dependencies = [
 name = "gadget-networking"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "async-trait",
  "auto_impl",
  "bincode",
+ "blake3",
+ "crossbeam-channel",
  "dashmap",
+ "fastrand",
  "futures",
  "gadget-crypto",
+ "gadget-crypto-core",
  "gadget-logging",
+ "gadget-networking",
  "gadget-std",
  "hex",
  "itertools 0.14.0",
@@ -7131,9 +7193,35 @@ dependencies = [
  "libp2p",
  "lru-mem",
  "parking_lot 0.12.3",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.11",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "tracing-subscriber 0.3.19",
+]
+
+[[package]]
+name = "gadget-networking-round-based-extension"
+version = "0.1.0"
+dependencies = [
+ "crossbeam",
+ "crossbeam-channel",
+ "dashmap",
+ "futures",
+ "gadget-crypto",
+ "gadget-crypto-core",
+ "gadget-networking",
+ "generic-array",
+ "hex",
+ "libp2p",
+ "rand 0.8.5",
+ "rand_dev",
  "round-based",
  "serde",
  "serde_json",
+ "sha2 0.10.8",
  "thiserror 2.0.11",
  "tokio",
  "tracing",
@@ -7168,7 +7256,7 @@ version = "0.1.0"
 dependencies = [
  "alloy-contract",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-rpc-types",
  "alloy-signer",
@@ -7251,15 +7339,16 @@ name = "gadget-tangle-testing-utils"
 version = "0.1.0"
 dependencies = [
  "alloy-json-rpc",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-rpc-types",
  "alloy-signer-local",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "alloy-transport",
  "async-trait",
  "cargo-tangle",
- "dirs",
+ "dirs 5.0.1",
+ "futures",
  "gadget-blueprint-serde",
  "gadget-client-tangle",
  "gadget-clients",
@@ -7271,6 +7360,7 @@ dependencies = [
  "gadget-keystore",
  "gadget-logging",
  "gadget-macros",
+ "gadget-networking",
  "gadget-runners",
  "gadget-std",
  "reqwest 0.12.12",
@@ -7309,7 +7399,7 @@ version = "0.1.0"
 dependencies = [
  "alloy-contract",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-transport",
  "eigensdk",
@@ -7322,7 +7412,7 @@ name = "gadget-utils-evm"
 version = "0.1.0"
 dependencies = [
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-signer",
  "alloy-signer-local",
@@ -7356,8 +7446,8 @@ dependencies = [
  "bytes",
  "chrono",
  "futures",
- "hyper 1.5.2",
- "jsonwebtoken 9.3.0",
+ "hyper 1.6.0",
+ "jsonwebtoken 9.3.1",
  "once_cell",
  "prost",
  "prost-types",
@@ -7393,6 +7483,7 @@ version = "0.14.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
 dependencies = [
+ "serde",
  "typenum",
  "version_check",
  "zeroize",
@@ -7490,7 +7581,7 @@ dependencies = [
  "gix-utils",
  "itoa",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
@@ -7511,7 +7602,7 @@ dependencies = [
  "smallvec",
  "thiserror 1.0.69",
  "unicode-bom",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
@@ -7614,7 +7705,7 @@ dependencies = [
  "itoa",
  "smallvec",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
@@ -7648,7 +7739,7 @@ dependencies = [
  "gix-validate",
  "memmap2",
  "thiserror 1.0.69",
- "winnow",
+ "winnow 0.6.26",
 ]
 
 [[package]]
@@ -7733,7 +7824,7 @@ dependencies = [
  "gloo-utils",
  "http 1.2.0",
  "js-sys",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "serde",
  "serde_json",
  "thiserror 1.0.69",
@@ -7799,9 +7890,9 @@ dependencies = [
 
 [[package]]
 name = "h2"
-version = "0.4.7"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
+checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
 dependencies = [
  "atomic-waker",
  "bytes",
@@ -8126,9 +8217,9 @@ dependencies = [
 
 [[package]]
 name = "httparse"
-version = "1.9.5"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
 
 [[package]]
 name = "httpdate"
@@ -8168,14 +8259,14 @@ dependencies = [
 
 [[package]]
 name = "hyper"
-version = "1.5.2"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
 dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
- "h2 0.4.7",
+ "h2 0.4.8",
  "http 1.2.0",
  "http-body 1.0.1",
  "httparse",
@@ -8194,7 +8285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
 dependencies = [
  "hex",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
  "pin-project-lite",
  "tokio",
@@ -8226,9 +8317,9 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
 dependencies = [
  "futures-util",
  "http 1.2.0",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-native-certs 0.8.1",
  "rustls-pki-types",
  "tokio",
@@ -8242,7 +8333,7 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
 dependencies = [
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
  "pin-project-lite",
  "tokio",
@@ -8270,7 +8361,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
 dependencies = [
  "bytes",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
  "native-tls",
  "tokio",
@@ -8289,7 +8380,7 @@ dependencies = [
  "futures-util",
  "http 1.2.0",
  "http-body 1.0.1",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "pin-project-lite",
  "socket2",
  "tokio",
@@ -8305,7 +8396,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
 dependencies = [
  "hex",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
  "pin-project-lite",
  "tokio",
@@ -8525,7 +8616,7 @@ dependencies = [
  "futures",
  "http 1.2.0",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-util",
  "log",
  "rand 0.8.5",
@@ -8561,9 +8652,9 @@ dependencies = [
 
 [[package]]
 name = "impl-codec"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67aa010c1e3da95bf151bd8b4c059b2ed7e75387cdb969b4f8f2723a43f9941"
+checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14"
 dependencies = [
  "parity-scale-codec",
 ]
@@ -8643,7 +8734,7 @@ dependencies = [
  "alloy-contract",
  "alloy-json-abi",
  "alloy-network",
- "alloy-primitives 0.8.20",
+ "alloy-primitives 0.8.21",
  "alloy-provider",
  "alloy-pubsub",
  "alloy-rpc-client",
@@ -8651,7 +8742,7 @@ dependencies = [
  "alloy-rpc-types-eth",
  "alloy-signer",
  "alloy-signer-local",
- "alloy-sol-types 0.8.19",
+ "alloy-sol-types 0.8.21",
  "alloy-transport",
  "alloy-transport-http",
  "ark-bn254",
@@ -8684,7 +8775,7 @@ dependencies = [
  "tokio",
  "tokio-util 0.7.13",
  "tracing",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -8723,9 +8814,9 @@ checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590"
 
 [[package]]
 name = "indicatif"
-version = "0.17.9"
+version = "0.17.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
+checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
 dependencies = [
  "console",
  "number_prefix",
@@ -8736,9 +8827,9 @@ dependencies = [
 
 [[package]]
 name = "inout"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
 dependencies = [
  "generic-array",
 ]
@@ -8875,9 +8966,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
 [[package]]
 name = "jiff"
-version = "0.1.26"
+version = "0.1.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e58800dee30ca82a1c107c64a0effca7ccb4e6106645c0e3af40d5aaba9f5457"
+checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5"
 dependencies = [
  "jiff-tzdb-platform",
  "log",
@@ -9015,8 +9106,8 @@ dependencies = [
  "gloo-net",
  "http 1.2.0",
  "jsonrpsee-core",
- "pin-project 1.1.8",
- "rustls 0.23.21",
+ "pin-project 1.1.9",
+ "rustls 0.23.23",
  "rustls-pki-types",
  "rustls-platform-verifier",
  "soketto",
@@ -9038,8 +9129,8 @@ dependencies = [
  "futures-timer",
  "futures-util",
  "jsonrpsee-types",
- "pin-project 1.1.8",
- "rustc-hash 2.1.0",
+ "pin-project 1.1.9",
+ "rustc-hash 2.1.1",
  "serde",
  "serde_json",
  "thiserror 1.0.69",
@@ -9115,14 +9206,14 @@ dependencies = [
 
 [[package]]
 name = "jsonwebtoken"
-version = "9.3.0"
+version = "9.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
+checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
 dependencies = [
- "base64 0.21.7",
+ "base64 0.22.1",
  "js-sys",
- "pem 3.0.4",
- "ring 0.17.8",
+ "pem 3.0.5",
+ "ring 0.17.10",
  "serde",
  "serde_json",
  "simple_asn1 0.6.3",
@@ -9293,7 +9384,7 @@ dependencies = [
  "libp2p-upnp",
  "libp2p-yamux",
  "multiaddr",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "rw-stream-sink",
  "thiserror 2.0.11",
 ]
@@ -9361,7 +9452,7 @@ dependencies = [
  "multistream-select",
  "once_cell",
  "parking_lot 0.12.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "quick-protobuf",
  "rand 0.8.5",
  "rw-stream-sink",
@@ -9435,6 +9526,7 @@ dependencies = [
  "quick-protobuf-codec",
  "rand 0.8.5",
  "regex",
+ "serde",
  "sha2 0.10.8",
  "tracing",
  "web-time",
@@ -9473,6 +9565,7 @@ dependencies = [
  "multihash",
  "quick-protobuf",
  "rand 0.8.5",
+ "serde",
  "sha2 0.10.8",
  "thiserror 1.0.69",
  "tracing",
@@ -9498,6 +9591,7 @@ dependencies = [
  "quick-protobuf",
  "quick-protobuf-codec",
  "rand 0.8.5",
+ "serde",
  "sha2 0.10.8",
  "smallvec",
  "thiserror 2.0.11",
@@ -9541,7 +9635,7 @@ dependencies = [
  "libp2p-ping",
  "libp2p-relay",
  "libp2p-swarm",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "prometheus-client",
  "web-time",
 ]
@@ -9600,8 +9694,8 @@ dependencies = [
  "libp2p-tls",
  "quinn",
  "rand 0.8.5",
- "ring 0.17.8",
- "rustls 0.23.21",
+ "ring 0.17.10",
+ "rustls 0.23.23",
  "socket2",
  "thiserror 2.0.11",
  "tokio",
@@ -9713,8 +9807,8 @@ dependencies = [
  "libp2p-core",
  "libp2p-identity",
  "rcgen",
- "ring 0.17.8",
- "rustls 0.23.21",
+ "ring 0.17.10",
+ "rustls 0.23.23",
  "rustls-webpki 0.101.7",
  "thiserror 2.0.11",
  "x509-parser",
@@ -9759,7 +9853,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
 dependencies = [
  "bitflags 2.8.0",
  "libc",
- "redox_syscall 0.5.8",
+ "redox_syscall 0.5.9",
 ]
 
 [[package]]
@@ -9812,9 +9906,9 @@ dependencies = [
 
 [[package]]
 name = "libssh2-sys"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
 dependencies = [
  "cc",
  "libc",
@@ -9880,9 +9974,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "liquid"
-version = "0.26.10"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d394f129df4bc476c828219f621c1a9e42c9d40c31e2849242087cb5ee279448"
+checksum = "2a494c3f9dad3cb7ed16f1c51812cbe4b29493d6c2e5cd1e2b87477263d9534d"
 dependencies = [
  "liquid-core",
  "liquid-derive",
@@ -9892,9 +9986,9 @@ dependencies = [
 
 [[package]]
 name = "liquid-core"
-version = "0.26.10"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7b563798f47f5158238ece76514553c2ce8ca58a20dd59abb21dfdbc3724980"
+checksum = "fc623edee8a618b4543e8e8505584f4847a4e51b805db1af6d9af0a3395d0d57"
 dependencies = [
  "anymap2",
  "itertools 0.14.0",
@@ -9909,9 +10003,9 @@ dependencies = [
 
 [[package]]
 name = "liquid-derive"
-version = "0.26.9"
+version = "0.26.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddde51013c8d2694f8c162c2ceb7abaed91f5175ec1e75a6392e35913e907c5d"
+checksum = "de66c928222984aea59fcaed8ba627f388aaac3c1f57dcb05cc25495ef8faefe"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9920,9 +10014,9 @@ dependencies = [
 
 [[package]]
 name = "liquid-lib"
-version = "0.26.10"
+version = "0.26.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bf4efd9b4a38bf76fac10d8f010ac568251761290a3dbffd4719d41c9f044"
+checksum = "9befeedd61f5995bc128c571db65300aeb50d62e4f0542c88282dbcb5f72372a"
 dependencies = [
  "itertools 0.14.0",
  "liquid-core",
@@ -9950,9 +10044,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.25"
+version = "0.4.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
 
 [[package]]
 name = "loom"
@@ -10167,13 +10261,13 @@ dependencies = [
 
 [[package]]
 name = "metrics-exporter-prometheus"
-version = "0.16.1"
+version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12779523996a67c13c84906a876ac6fe4d07a6e1adb54978378e13f199251a62"
+checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034"
 dependencies = [
  "base64 0.22.1",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-rustls 0.27.5",
  "hyper-util",
  "indexmap 2.7.1",
@@ -10239,9 +10333,9 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
+checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
 dependencies = [
  "adler2",
 ]
@@ -10273,7 +10367,7 @@ dependencies = [
  "smallvec",
  "tagptr",
  "thiserror 1.0.69",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -10332,7 +10426,7 @@ dependencies = [
  "bytes",
  "futures",
  "log",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "smallvec",
  "unsigned-varint 0.7.2",
 ]
@@ -10369,9 +10463,9 @@ checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
 
 [[package]]
 name = "native-tls"
-version = "0.2.12"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
 dependencies = [
  "libc",
  "log",
@@ -10434,9 +10528,9 @@ dependencies = [
 
 [[package]]
 name = "netlink-proto"
-version = "0.11.4"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2741a6c259755922e3ed29ebce3b299cc2160c4acae94b465b5938ab02c2bbe"
+checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60"
 dependencies = [
  "bytes",
  "futures",
@@ -10541,9 +10635,9 @@ dependencies = [
 
 [[package]]
 name = "ntex"
-version = "2.10.0"
+version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1988f5a24be40f4cd06c5a1d66ff71da4dcf161f19a61013e6f967642c3c5bc"
+checksum = "5f9733b93874b834484947eafbbb8136544b20ea8473a46be40e197cb148284b"
 dependencies = [
  "base64 0.22.1",
  "bitflags 2.8.0",
@@ -10600,9 +10694,9 @@ dependencies = [
 
 [[package]]
 name = "ntex-h2"
-version = "1.6.0"
+version = "1.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad3cfeaff6970ede203c8f81a29e2b5d478b0532b7484b5faf2a2ef211a9e4f8"
+checksum = "8f17e4eec506679d78f67b32c42ee4e35d0dcd97451251a4d891184185187520"
 dependencies = [
  "bitflags 2.8.0",
  "fxhash",
@@ -10613,7 +10707,6 @@ dependencies = [
  "ntex-http",
  "ntex-io",
  "ntex-net",
- "ntex-rt",
  "ntex-service",
  "ntex-util",
  "pin-project-lite",
@@ -10706,12 +10799,13 @@ dependencies = [
 
 [[package]]
 name = "ntex-server"
-version = "2.6.2"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35d4d5a5e06133d1941a56052a827dfe5b529aca810d6480fad993a53a8fb92f"
+checksum = "ebf31f5c770cb0244a8125aba1c5dfaca38f145807a42ba33df30c35bb0c33f7"
 dependencies = [
  "async-broadcast",
  "async-channel",
+ "core_affinity",
  "ctrlc",
  "log",
  "ntex-bytes",
@@ -10875,6 +10969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
+ "libm",
 ]
 
 [[package]]
@@ -10968,18 +11063,18 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 dependencies = [
  "portable-atomic",
 ]
 
 [[package]]
 name = "oneshot"
-version = "0.1.8"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e296cf87e61c9cfc1a61c3c63a0f7f286ed4554e0e22be84e8a38e1d264a2a29"
+checksum = "79d72a7c0f743d2ebb0a2ad1d219db75fdc799092ed3a884c9144c42a31225bd"
 
 [[package]]
 name = "opaque-debug"
@@ -11014,9 +11109,9 @@ dependencies = [
 
 [[package]]
 name = "openssl"
-version = "0.10.68"
+version = "0.10.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
+checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
 dependencies = [
  "bitflags 2.8.0",
  "cfg-if 1.0.0",
@@ -11046,18 +11141,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
 
 [[package]]
 name = "openssl-src"
-version = "300.4.1+3.4.0"
+version = "300.4.2+3.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c"
+checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2"
 dependencies = [
  "cc",
 ]
 
 [[package]]
 name = "openssl-sys"
-version = "0.9.104"
+version = "0.9.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
+checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
 dependencies = [
  "cc",
  "libc",
@@ -11357,9 +11452,9 @@ dependencies = [
 
 [[package]]
 name = "pallet-balances"
-version = "39.0.0"
+version = "39.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c6945b078919acb14d126490e4b0973a688568b30142476ca69c6df2bed27ad"
+checksum = "bcb1f72d7048fbd11e884b4693f7d438b8202340ff252e2a402e04c638fe2d02"
 dependencies = [
  "docify",
  "frame-benchmarking",
@@ -12447,7 +12542,7 @@ dependencies = [
  "polkavm-linker 0.10.0",
  "sp-runtime",
  "tempfile",
- "toml 0.8.19",
+ "toml 0.8.20",
 ]
 
 [[package]]
@@ -13006,9 +13101,9 @@ dependencies = [
 
 [[package]]
 name = "pallet-xcm-bridge-hub"
-version = "0.13.2"
+version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f336403f9e9bf22a0e1fdb90aa5093c52599c9a0639591fbcc1e979b58862d1b"
+checksum = "3bdb76fff08633830063a4cb36664f0cf2f926ac0da02ee439d4f521763e26b7"
 dependencies = [
  "bp-messages",
  "bp-runtime",
@@ -13130,29 +13225,31 @@ checksum = "16b56e3a2420138bdb970f84dfb9c774aea80fa0e7371549eedec0d80c209c67"
 
 [[package]]
 name = "parity-scale-codec"
-version = "3.6.12"
+version = "3.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee"
+checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d"
 dependencies = [
  "arrayvec 0.7.6",
  "bitvec",
  "byte-slice-cast",
  "bytes",
+ "const_format",
  "impl-trait-for-tuples",
  "parity-scale-codec-derive",
+ "rustversion",
  "serde",
 ]
 
 [[package]]
 name = "parity-scale-codec-derive"
-version = "3.6.12"
+version = "3.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c"
+checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3"
 dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 1.0.109",
+ "syn 2.0.98",
 ]
 
 [[package]]
@@ -13239,7 +13336,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
- "redox_syscall 0.5.8",
+ "redox_syscall 0.5.9",
  "smallvec",
  "windows-targets 0.52.6",
 ]
@@ -13366,9 +13463,9 @@ dependencies = [
 
 [[package]]
 name = "pem"
-version = "3.0.4"
+version = "3.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
+checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
 dependencies = [
  "base64 0.22.1",
  "serde",
@@ -13470,7 +13567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
 dependencies = [
  "phf_macros",
- "phf_shared 0.11.3",
+ "phf_shared",
 ]
 
 [[package]]
@@ -13479,7 +13576,7 @@ version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
 dependencies = [
- "phf_shared 0.11.3",
+ "phf_shared",
  "rand 0.8.5",
 ]
 
@@ -13490,28 +13587,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
 dependencies = [
  "phf_generator",
- "phf_shared 0.11.3",
+ "phf_shared",
  "proc-macro2",
  "quote",
  "syn 2.0.98",
 ]
 
-[[package]]
-name = "phf_shared"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
-dependencies = [
- "siphasher 0.3.11",
-]
-
 [[package]]
 name = "phf_shared"
 version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
 dependencies = [
- "siphasher 1.0.1",
+ "siphasher",
 ]
 
 [[package]]
@@ -13525,11 +13613,11 @@ dependencies = [
 
 [[package]]
 name = "pin-project"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
+checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
 dependencies = [
- "pin-project-internal 1.1.8",
+ "pin-project-internal 1.1.9",
 ]
 
 [[package]]
@@ -13545,9 +13633,9 @@ dependencies = [
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
+checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -13622,7 +13710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52b5648a2e8ce1f9a0f8c41c38def670cefd91932cd793468e1a5b0b0b4e4af1"
 dependencies = [
  "bounded-collections",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
  "parity-scale-codec",
  "polkadot-core-primitives",
  "scale-info",
@@ -13757,7 +13845,7 @@ checksum = "1d4cdf181c2419b35c2cbde813da2d8ee777b69b4a6fa346b962d144e3521976"
 dependencies = [
  "bitflags 1.3.2",
  "bitvec",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
  "frame-benchmarking",
  "frame-support",
  "frame-system",
@@ -14335,7 +14423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5"
 dependencies = [
  "fixed-hash",
- "impl-codec 0.7.0",
+ "impl-codec 0.7.1",
  "impl-serde 0.5.0",
  "scale-info",
  "uint 0.10.0",
@@ -14398,9 +14486,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-warning"
-version = "1.0.2"
+version = "1.84.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "834da187cfe638ae8abb0203f0b33e5ccdb02a28e7199f2f47b3e2754f50edca"
+checksum = "75eea531cfcd120e0851a3f8aed42c4841f78c889eefafd96339c72677ae42c3"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -14467,9 +14555,9 @@ dependencies = [
 
 [[package]]
 name = "prost"
-version = "0.13.4"
+version = "0.13.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
 dependencies = [
  "bytes",
  "prost-derive",
@@ -14477,12 +14565,12 @@ dependencies = [
 
 [[package]]
 name = "prost-derive"
-version = "0.13.4"
+version = "0.13.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
 dependencies = [
  "anyhow",
- "itertools 0.13.0",
+ "itertools 0.14.0",
  "proc-macro2",
  "quote",
  "syn 2.0.98",
@@ -14490,18 +14578,18 @@ dependencies = [
 
 [[package]]
 name = "prost-types"
-version = "0.13.4"
+version = "0.13.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
 dependencies = [
  "prost",
 ]
 
 [[package]]
 name = "psm"
-version = "0.1.24"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
+checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88"
 dependencies = [
  "cc",
 ]
@@ -14560,8 +14648,8 @@ dependencies = [
  "pin-project-lite",
  "quinn-proto",
  "quinn-udp",
- "rustc-hash 2.1.0",
- "rustls 0.23.21",
+ "rustc-hash 2.1.1",
+ "rustls 0.23.23",
  "socket2",
  "thiserror 2.0.11",
  "tokio",
@@ -14577,9 +14665,9 @@ dependencies = [
  "bytes",
  "getrandom 0.2.15",
  "rand 0.8.5",
- "ring 0.17.8",
- "rustc-hash 2.1.0",
- "rustls 0.23.21",
+ "ring 0.17.10",
+ "rustc-hash 2.1.1",
+ "rustls 0.23.23",
  "rustls-pki-types",
  "slab",
  "thiserror 2.0.11",
@@ -14590,9 +14678,9 @@ dependencies = [
 
 [[package]]
 name = "quinn-udp"
-version = "0.5.9"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904"
+checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
 dependencies = [
  "cfg_aliases",
  "libc",
@@ -14646,8 +14734,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
 dependencies = [
  "rand_chacha 0.9.0",
- "rand_core 0.9.0",
- "zerocopy 0.8.17",
+ "rand_core 0.9.1",
+ "zerocopy 0.8.20",
 ]
 
 [[package]]
@@ -14667,7 +14755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.9.0",
+ "rand_core 0.9.1",
 ]
 
 [[package]]
@@ -14681,12 +14769,23 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.9.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
+checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
 dependencies = [
  "getrandom 0.3.1",
- "zerocopy 0.8.17",
+ "zerocopy 0.8.20",
+]
+
+[[package]]
+name = "rand_dev"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbee97c27dada05f03db49ffe6516872f6c926e0fd525f9ce0cb3c051adf145c"
+dependencies = [
+ "hex",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
 ]
 
 [[package]]
@@ -14709,9 +14808,9 @@ dependencies = [
 
 [[package]]
 name = "raw-cpuid"
-version = "11.3.0"
+version = "11.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6928fa44c097620b706542d428957635951bade7143269085389d42c8a4927e"
+checksum = "529468c1335c1c03919960dfefdb1b3648858c20d7ec2d0663e728e4a717efbc"
 dependencies = [
  "bitflags 2.8.0",
 ]
@@ -14748,7 +14847,7 @@ version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52c4f3084aa3bc7dfbba4eff4fab2a54db4324965d8872ab933565e6fbd83bc6"
 dependencies = [
- "pem 3.0.4",
+ "pem 3.0.5",
  "ring 0.16.20",
  "time",
  "yasna",
@@ -14780,9 +14879,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.8"
+version = "0.5.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
 dependencies = [
  "bitflags 2.8.0",
 ]
@@ -14798,6 +14897,17 @@ dependencies = [
  "thiserror 1.0.69",
 ]
 
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror 2.0.11",
+]
+
 [[package]]
 name = "ref-cast"
 version = "1.0.23"
@@ -14963,11 +15073,11 @@ dependencies = [
  "encoding_rs",
  "futures-core",
  "futures-util",
- "h2 0.4.7",
+ "h2 0.4.8",
  "http 1.2.0",
  "http-body 1.0.1",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-rustls 0.27.5",
  "hyper-tls 0.6.0",
  "hyper-util",
@@ -14981,7 +15091,7 @@ dependencies = [
  "percent-encoding",
  "pin-project-lite",
  "quinn",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-native-certs 0.8.1",
  "rustls-pemfile 2.2.0",
  "rustls-pki-types",
@@ -15069,15 +15179,14 @@ dependencies = [
 
 [[package]]
 name = "ring"
-version = "0.17.8"
+version = "0.17.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
+checksum = "d34b5020fcdea098ef7d95e9f89ec15952123a4a039badd09fabebe9e963e839"
 dependencies = [
  "cc",
  "cfg-if 1.0.0",
  "getrandom 0.2.15",
  "libc",
- "spin 0.9.8",
  "untrusted 0.9.0",
  "windows-sys 0.52.0",
 ]
@@ -15132,15 +15241,16 @@ dependencies = [
 
 [[package]]
 name = "round-based"
-version = "0.3.2"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81564866f5617d497753563151d8beb80d61e925e904d94b7e8a202b721e931e"
+checksum = "da76edf50de0a9d6911fc79261bb04cc9f3f3a375e0201799f5edf58499af341"
 dependencies = [
- "displaydoc",
  "futures-util",
  "phantom-type",
  "round-based-derive",
- "thiserror 1.0.69",
+ "thiserror 2.0.11",
+ "tokio",
+ "tokio-stream",
  "tracing",
 ]
 
@@ -15175,16 +15285,18 @@ dependencies = [
 
 [[package]]
 name = "ruint"
-version = "1.12.3"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286"
+checksum = "825df406ec217a8116bd7b06897c6cc8f65ffefc15d030ae2c9540acc9ed50b6"
 dependencies = [
  "alloy-rlp",
  "ark-ff 0.3.0",
  "ark-ff 0.4.2",
  "bytes",
- "fastrlp",
+ "fastrlp 0.3.1",
+ "fastrlp 0.4.0",
  "num-bigint 0.4.6",
+ "num-integer",
  "num-traits",
  "parity-scale-codec",
  "primitive-types 0.12.2",
@@ -15255,9 +15367,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustc-hash"
-version = "2.1.0"
+version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
 
 [[package]]
 name = "rustc-hex"
@@ -15344,21 +15456,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
 dependencies = [
  "log",
- "ring 0.17.8",
+ "ring 0.17.10",
  "rustls-webpki 0.101.7",
  "sct",
 ]
 
 [[package]]
 name = "rustls"
-version = "0.23.21"
+version = "0.23.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
+checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
 dependencies = [
  "aws-lc-rs",
  "log",
  "once_cell",
- "ring 0.17.8",
+ "ring 0.17.10",
  "rustls-pki-types",
  "rustls-webpki 0.102.8",
  "subtle",
@@ -15422,9 +15534,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
+checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
 dependencies = [
  "web-time",
 ]
@@ -15440,13 +15552,13 @@ dependencies = [
  "jni",
  "log",
  "once_cell",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-native-certs 0.7.3",
  "rustls-platform-verifier-android",
  "rustls-webpki 0.102.8",
  "security-framework 2.11.1",
  "security-framework-sys",
- "webpki-roots 0.26.7",
+ "webpki-roots 0.26.8",
  "winapi",
 ]
 
@@ -15462,7 +15574,7 @@ version = "0.101.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
 dependencies = [
- "ring 0.17.8",
+ "ring 0.17.10",
  "untrusted 0.9.0",
 ]
 
@@ -15473,7 +15585,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
 dependencies = [
  "aws-lc-rs",
- "ring 0.17.8",
+ "ring 0.17.10",
  "rustls-pki-types",
  "untrusted 0.9.0",
 ]
@@ -15503,7 +15615,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5174a470eeb535a721ae9fdd6e291c2411a906b96592182d05217591d5c5cf7b"
 dependencies = [
  "byteorder",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
 ]
 
 [[package]]
@@ -15513,15 +15625,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1"
 dependencies = [
  "futures",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "static_assertions",
 ]
 
 [[package]]
 name = "ryu"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
 
 [[package]]
 name = "safe-mix"
@@ -15872,7 +15984,7 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
 dependencies = [
- "ring 0.17.8",
+ "ring 0.17.10",
  "untrusted 0.9.0",
 ]
 
@@ -16062,9 +16174,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
 
 [[package]]
 name = "serde"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
 dependencies = [
  "serde_derive",
 ]
@@ -16110,9 +16222,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -16121,9 +16233,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.138"
+version = "1.0.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
+checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
 dependencies = [
  "itoa",
  "memchr",
@@ -16376,12 +16488,6 @@ dependencies = [
  "time",
 ]
 
-[[package]]
-name = "siphasher"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
-
 [[package]]
 name = "siphasher"
 version = "1.0.1"
@@ -16423,9 +16529,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
 dependencies = [
  "serde",
 ]
@@ -16473,7 +16579,7 @@ dependencies = [
  "bs58",
  "chacha20",
  "crossbeam-queue",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
  "ed25519-zebra",
  "either",
  "event-listener",
@@ -16492,7 +16598,7 @@ dependencies = [
  "num-rational",
  "num-traits",
  "pbkdf2 0.12.2",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "poly1305",
  "rand 0.8.5",
  "rand_chacha 0.3.1",
@@ -16502,7 +16608,7 @@ dependencies = [
  "serde_json",
  "sha2 0.10.8",
  "sha3",
- "siphasher 1.0.1",
+ "siphasher",
  "slab",
  "smallvec",
  "soketto",
@@ -16523,7 +16629,7 @@ dependencies = [
  "base64 0.22.1",
  "blake2-rfc",
  "bs58",
- "derive_more 0.99.18",
+ "derive_more 0.99.19",
  "either",
  "event-listener",
  "fnv",
@@ -16536,12 +16642,12 @@ dependencies = [
  "log",
  "lru 0.12.5",
  "parking_lot 0.12.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "rand 0.8.5",
  "rand_chacha 0.3.1",
  "serde",
  "serde_json",
- "siphasher 1.0.1",
+ "siphasher",
  "slab",
  "smol",
  "smoldot",
@@ -16559,7 +16665,7 @@ dependencies = [
  "chacha20poly1305",
  "curve25519-dalek",
  "rand_core 0.6.4",
- "ring 0.17.8",
+ "ring 0.17.10",
  "rustc_version 0.4.1",
  "sha2 0.10.8",
  "subtle",
@@ -17809,9 +17915,9 @@ dependencies = [
 
 [[package]]
 name = "staging-xcm-builder"
-version = "17.0.3"
+version = "17.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f7a92cfaec55a5ed0f9cbbb9076aa8ec0aff1ba90b9804cc5c8f2369fde59c"
+checksum = "e1693870a07e3fd8115c02b44e1223ce149b6cfa0b60f59a1c0fbc26637766a5"
 dependencies = [
  "frame-support",
  "frame-system",
@@ -17870,14 +17976,13 @@ dependencies = [
 
 [[package]]
 name = "string_cache"
-version = "0.8.7"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
+checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe"
 dependencies = [
  "new_debug_unreachable",
- "once_cell",
  "parking_lot 0.12.3",
- "phf_shared 0.10.0",
+ "phf_shared",
  "precomputed-hash",
 ]
 
@@ -17925,6 +18030,15 @@ dependencies = [
  "strum_macros 0.26.4",
 ]
 
+[[package]]
+name = "strum"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+dependencies = [
+ "strum_macros 0.27.1",
+]
+
 [[package]]
 name = "strum_macros"
 version = "0.24.3"
@@ -17951,6 +18065,19 @@ dependencies = [
  "syn 2.0.98",
 ]
 
+[[package]]
+name = "strum_macros"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.98",
+]
+
 [[package]]
 name = "substrate-bip39"
 version = "0.6.0"
@@ -17980,7 +18107,7 @@ dependencies = [
  "sp-maybe-compressed-blob",
  "strum 0.26.3",
  "tempfile",
- "toml 0.8.19",
+ "toml 0.8.20",
  "walkdir",
  "wasm-opt",
 ]
@@ -18089,7 +18216,7 @@ dependencies = [
  "futures-util",
  "getrandom 0.2.15",
  "js-sys",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "send_wrapper 0.6.0",
  "serde",
  "serde_json",
@@ -18182,7 +18309,7 @@ version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11297baafe5fa0c99d5722458eac6a5e25c01eb1b8e5cd137f54079093daa7a4"
 dependencies = [
- "dirs",
+ "dirs 5.0.1",
  "fs2",
  "hex",
  "once_cell",
@@ -18232,9 +18359,9 @@ dependencies = [
 
 [[package]]
 name = "syn-solidity"
-version = "0.8.19"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b84e4d83a0a6704561302b917a932484e1cae2d8c6354c64be8b7bac1c1fe057"
+checksum = "9c2de690018098e367beeb793991c7d4dc7270f42c9d2ac4ccc876c1368ca430"
 dependencies = [
  "paste",
  "proc-macro2",
@@ -18343,9 +18470,9 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
 
 [[package]]
 name = "tangle-subxt"
-version = "0.11.0"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20472342792207ec9356d66a8e1a0f8f4d74c6190851d67935c0ead17115e241"
+checksum = "45139e1710c60b47f7254407d397aa987747bcb6c42b2bdf4f787e8d3baf903b"
 dependencies = [
  "parity-scale-codec",
  "scale-info",
@@ -18369,9 +18496,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
 
 [[package]]
 name = "target-triple"
-version = "0.1.3"
+version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078"
+checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790"
 
 [[package]]
 name = "tempfile"
@@ -18428,9 +18555,9 @@ dependencies = [
 
 [[package]]
 name = "testcontainers"
-version = "0.23.1"
+version = "0.23.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1"
+checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74"
 dependencies = [
  "async-trait",
  "bollard",
@@ -18447,7 +18574,7 @@ dependencies = [
  "serde",
  "serde_json",
  "serde_with",
- "thiserror 1.0.69",
+ "thiserror 2.0.11",
  "tokio",
  "tokio-stream",
  "tokio-tar",
@@ -18627,9 +18754,9 @@ dependencies = [
 
 [[package]]
 name = "tnt-core-bytecode"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aad568c5526da5761e58263eed5ad9a955e8c093c509e7729ea2b3a82f89ffe3"
+checksum = "2720c6e903ef89a610b5aa82f37d4034f987c59bdc30fb6198d96d0dc6182106"
 dependencies = [
  "serde_json",
 ]
@@ -18664,7 +18791,7 @@ dependencies = [
  "num-traits",
  "tokio",
  "tracing",
- "uuid 1.12.1",
+ "uuid 1.14.0",
 ]
 
 [[package]]
@@ -18704,7 +18831,7 @@ version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
 dependencies = [
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "tokio",
 ]
 
@@ -18758,12 +18885,12 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
 dependencies = [
  "futures-util",
  "log",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-pki-types",
  "tokio",
  "tokio-rustls 0.26.1",
  "tungstenite 0.24.0",
- "webpki-roots 0.26.7",
+ "webpki-roots 0.26.8",
 ]
 
 [[package]]
@@ -18805,9 +18932,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
 dependencies = [
  "indexmap 2.7.1",
  "serde",
@@ -18827,15 +18954,15 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.22"
+version = "0.22.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
 dependencies = [
  "indexmap 2.7.1",
  "serde",
  "serde_spanned",
  "toml_datetime",
- "winnow",
+ "winnow 0.7.3",
 ]
 
 [[package]]
@@ -18849,15 +18976,15 @@ dependencies = [
  "axum",
  "base64 0.22.1",
  "bytes",
- "h2 0.4.7",
+ "h2 0.4.8",
  "http 1.2.0",
  "http-body 1.0.1",
  "http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
  "hyper-timeout",
  "hyper-util",
  "percent-encoding",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "prost",
  "rustls-native-certs 0.8.1",
  "rustls-pemfile 2.2.0",
@@ -18880,7 +19007,7 @@ dependencies = [
  "futures-core",
  "futures-util",
  "indexmap 1.9.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "pin-project-lite",
  "rand 0.8.5",
  "slab",
@@ -18979,7 +19106,7 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
 dependencies = [
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "tracing",
 ]
 
@@ -19074,7 +19201,7 @@ dependencies = [
  "serde_json",
  "target-triple",
  "termcolor",
- "toml 0.8.19",
+ "toml 0.8.20",
 ]
 
 [[package]]
@@ -19116,7 +19243,7 @@ dependencies = [
  "httparse",
  "log",
  "rand 0.8.5",
- "rustls 0.23.21",
+ "rustls 0.23.23",
  "rustls-pki-types",
  "sha1",
  "thiserror 1.0.69",
@@ -19169,9 +19296,9 @@ checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
 
 [[package]]
 name = "typenum"
-version = "1.17.0"
+version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 
 [[package]]
 name = "ucd-trie"
@@ -19223,9 +19350,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.15"
+version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
+checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
 
 [[package]]
 name = "unicode-normalization"
@@ -19348,11 +19475,11 @@ dependencies = [
 
 [[package]]
 name = "uuid"
-version = "1.12.1"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
+checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.3.1",
 ]
 
 [[package]]
@@ -19405,9 +19532,9 @@ dependencies = [
 
 [[package]]
 name = "wait-timeout"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
 dependencies = [
  "libc",
 ]
@@ -19875,9 +20002,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
 [[package]]
 name = "webpki-roots"
-version = "0.26.7"
+version = "0.26.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
+checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
 dependencies = [
  "rustls-pki-types",
 ]
@@ -20331,9 +20458,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "0.6.24"
+version = "0.6.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
 dependencies = [
  "memchr",
 ]
@@ -20518,7 +20654,7 @@ dependencies = [
  "log",
  "nohash-hasher",
  "parking_lot 0.12.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "rand 0.8.5",
  "static_assertions",
 ]
@@ -20533,7 +20669,7 @@ dependencies = [
  "log",
  "nohash-hasher",
  "parking_lot 0.12.3",
- "pin-project 1.1.8",
+ "pin-project 1.1.9",
  "rand 0.8.5",
  "static_assertions",
  "web-time",
@@ -20596,11 +20732,11 @@ dependencies = [
 
 [[package]]
 name = "zerocopy"
-version = "0.8.17"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713"
+checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
 dependencies = [
- "zerocopy-derive 0.8.17",
+ "zerocopy-derive 0.8.20",
 ]
 
 [[package]]
@@ -20616,9 +20752,9 @@ dependencies = [
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.8.17"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626"
+checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -20748,9 +20884,9 @@ dependencies = [
 
 [[package]]
 name = "zstd-sys"
-version = "2.0.13+zstd.1.5.6"
+version = "2.0.14+zstd.1.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
+checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5"
 dependencies = [
  "cc",
  "pkg-config",
diff --git a/Cargo.toml b/Cargo.toml
index d9a8a1599..47269476b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,13 +1,7 @@
 [workspace]
 resolver = "2"
-members = [
-	"cli",
-	"blueprints/*",
-	"crates/*",
-]
-exclude = [
-	"blueprints/incredible-squaring-symbiotic",
-]
+members = ["cli", "blueprints/*", "crates/*"]
+exclude = ["blueprints/incredible-squaring-symbiotic"]
 
 [workspace.package]
 authors = ["Tangle Network"]
@@ -28,6 +22,10 @@ all = { level = "deny", priority = -1 }
 single_match_else = "allow"
 uninlined_format_args = "allow"
 needless_late_init = "allow"
+struct_excessive_bools = "allow"
+large_enum_variant = "allow" # TODO: Fix instances of this
+result_large_err = "allow" # TODO: Fix instances of this
+similar_names = "allow"
 
 [workspace.lints.rustdoc]
 broken_intra_doc_links = "deny"
@@ -69,7 +67,6 @@ gadget-clients = { version = "0.1.0", path = "./crates/clients", default-feature
 gadget-client-core = { version = "0.1.0", path = "./crates/clients/core", default-features = false }
 gadget-client-eigenlayer = { version = "0.1.0", path = "./crates/clients/eigenlayer", default-features = false }
 gadget-client-evm = { version = "0.1.0", path = "./crates/clients/evm", default-features = false }
-gadget-client-networking = { version = "0.1.0", path = "./crates/clients/networking", default-features = false }
 gadget-client-tangle = { version = "0.1.0", path = "./crates/clients/tangle", default-features = false }
 gadget-contexts = { version = "0.1.0", path = "./crates/contexts", default-features = false }
 
@@ -105,9 +102,12 @@ gadget-runner-symbiotic = { version = "0.1.0", path = "./crates/runners/symbioti
 gadget-config = { version = "0.1.0", path = "./crates/config", default-features = false }
 gadget-keystore = { version = "0.1.0", path = "./crates/keystore", default-features = false }
 gadget-logging = { version = "0.1.0", path = "./crates/logging", default-features = false }
-gadget-networking = { version = "0.1.0", path = "./crates/networking", default-features = false }
 gadget-std = { version = "0.1.0", path = "./crates/std", default-features = false }
 
+# P2P
+gadget-networking = { version = "0.1.0", path = "./crates/networking", default-features = false }
+gadget-networking-round-based-extension = { version = "0.1.0", path = "./crates/networking/extensions/round-based", default-features = false }
+
 # Utilities
 gadget-utils = { version = "0.1.0", path = "./crates/utils", default-features = false }
 gadget-utils-evm = { version = "0.1.0", path = "./crates/utils/evm", default-features = false }
@@ -127,11 +127,11 @@ gadget-rpc-calls = { version = "0.1.0", path = "./crates/metrics/rpc-calls", def
 
 cargo-tangle = { version = "0.1.0", path = "./cli", default-features = false }
 cargo_metadata = { version = "0.18.1" }
-tnt-core-bytecode = { version = "0.3.0", default-features = false }
+tnt-core-bytecode = { version = "0.4.0", default-features = false }
 
 # Tangle-related dependencies
-tangle-subxt = { version = "0.11.0", default-features = false }
-round-based = { version = "0.3.0", default-features = false }
+tangle-subxt = { version = "0.12.0", default-features = false }
+round-based = { version = "0.4.1", default-features = false }
 
 # Substrate dependencies
 sp-core = { version = "34.0.0", default-features = false }
@@ -140,11 +140,14 @@ sp-runtime = { version = "39.0.0", default-features = false }
 
 # Async & Runtime
 async-trait = { version = "0.1.86", default-features = false }
+crossbeam = { version = "0.8", default-features = false }
+crossbeam-channel = { version = "0.5", default-features = false }
 futures = { version = "0.3.30", default-features = false }
 futures-util = { version = "0.3.31", default-features = false }
 tokio = { version = "1.40", default-features = false }
 tokio-util = { version = "0.7.12", default-features = false }
 tokio-cron-scheduler = "0.13.0"
+tokio-stream = { version = "0.1.17", default-features = false }
 
 # CLI & Configuration
 cargo-generate = { version = "0.22.1", default-features = false }
diff --git a/blueprints/examples/src/eigen_context.rs b/blueprints/examples/src/eigen_context.rs
index c92d9ce0a..6770a7b67 100644
--- a/blueprints/examples/src/eigen_context.rs
+++ b/blueprints/examples/src/eigen_context.rs
@@ -59,7 +59,7 @@ pub async fn constructor(
     id = 0,
     params(task_index, log_index, block_number),
     event_listener(
-        listener = EvmContractEventListener<ExampleTaskManager::NewTaskCreated>,
+        listener = EvmContractEventListener<ExampleEigenContext, ExampleTaskManager::NewTaskCreated>,
         instance = ExampleTaskManager,
         abi = EXAMPLE_TASK_MANAGER_ABI_STRING,
         pre_processor = handle_events,
diff --git a/blueprints/examples/src/lib.rs b/blueprints/examples/src/lib.rs
index 05d044406..0226a863d 100644
--- a/blueprints/examples/src/lib.rs
+++ b/blueprints/examples/src/lib.rs
@@ -1,6 +1,5 @@
 pub mod eigen_context;
 pub mod periodic_web_poller;
-pub mod raw_tangle_events;
 pub mod services_context;
 
 #[cfg(test)]
diff --git a/blueprints/examples/src/main.rs b/blueprints/examples/src/main.rs
index 793a20c55..d1a5b18b9 100644
--- a/blueprints/examples/src/main.rs
+++ b/blueprints/examples/src/main.rs
@@ -1,4 +1,4 @@
-use blueprint_examples::{eigen_context, periodic_web_poller, raw_tangle_events, services_context};
+use blueprint_examples::{eigen_context, periodic_web_poller, services_context};
 use blueprint_sdk::alloy::primitives::Address;
 use blueprint_sdk::logging::info;
 use blueprint_sdk::runners::core::runner::BlueprintRunner;
@@ -19,7 +19,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         "tangle" => {
             info!("Running Tangle examples");
             BlueprintRunner::new(TangleConfig::default(), env.clone())
-                .job(raw_tangle_events::constructor(env.clone()).await?)
                 .job(periodic_web_poller::constructor("1/2 * * * * *"))
                 .job(services_context::constructor(env.clone()).await?)
                 .run()
diff --git a/blueprints/examples/src/raw_tangle_events.rs b/blueprints/examples/src/raw_tangle_events.rs
deleted file mode 100644
index b90cab575..000000000
--- a/blueprints/examples/src/raw_tangle_events.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-use blueprint_sdk::config::GadgetConfiguration;
-use blueprint_sdk::contexts::keystore::KeystoreContext;
-use blueprint_sdk::crypto::sp_core::SpSr25519;
-use blueprint_sdk::event_listeners::core::InitializableEventHandler;
-use blueprint_sdk::event_listeners::tangle::events::{TangleEvent, TangleEventListener};
-use blueprint_sdk::job;
-use blueprint_sdk::keystore::backends::Backend;
-use blueprint_sdk::logging::info;
-use blueprint_sdk::macros::contexts::{ServicesContext, TangleClientContext};
-use blueprint_sdk::tangle_subxt::tangle_testnet_runtime::api;
-
-#[derive(Clone, TangleClientContext, ServicesContext)]
-pub struct MyContext {
-    #[config]
-    sdk_config: GadgetConfiguration,
-    #[call_id]
-    call_id: Option<u64>,
-}
-
-pub async fn constructor(
-    env: GadgetConfiguration,
-) -> color_eyre::Result<impl InitializableEventHandler> {
-    let signer = env
-        .clone()
-        .keystore()
-        .first_local::<SpSr25519>()
-        .map_err(|e| color_eyre::eyre::eyre!(e))?;
-
-    info!("Starting the event watcher for {:?} ...", signer.0);
-    RawEventHandler::new(
-        &env,
-        MyContext {
-            sdk_config: env.clone(),
-            call_id: None,
-        },
-    )
-    .await
-    .map_err(|e| color_eyre::eyre::eyre!(e))
-}
-
-#[job(
-    id = 2,
-    event_listener(
-        listener = TangleEventListener<MyContext>,
-    ),
-)]
-pub fn raw(event: TangleEvent<MyContext>, context: MyContext) -> Result<u64, blueprint_sdk::Error> {
-    if let Some(balance_transfer) = event
-        .evt
-        .as_event::<api::balances::events::Transfer>()
-        .ok()
-        .flatten()
-    {
-        info!("Found a balance transfer: {balance_transfer:?}");
-
-        let result = std::env::var("RAW_EVENT_RESULT").unwrap_or("0".to_string());
-        let result = result.parse::<u64>().unwrap_or(0);
-        let result = result + 1;
-        std::env::set_var("RAW_EVENT_RESULT", result.to_string());
-
-        return Ok(1);
-    }
-    Ok(0)
-}
diff --git a/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs b/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
index bb53ded81..011a4b553 100644
--- a/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
+++ b/blueprints/incredible-squaring-eigenlayer/src/jobs/compute_x_square.rs
@@ -29,7 +29,7 @@ use std::convert::Infallible;
     id = 0,
     params(number_to_be_squared, task_created_block, quorum_numbers, quorum_threshold_percentage, task_index),
     event_listener(
-        listener = EvmContractEventListener<IncredibleSquaringTaskManager::NewTaskCreated>,
+        listener = EvmContractEventListener<EigenSquareContext, IncredibleSquaringTaskManager::NewTaskCreated>,
         instance = IncredibleSquaringTaskManager,
         abi = INCREDIBLE_SQUARING_TASK_MANAGER_ABI_STRING,
         pre_processor = convert_event_to_inputs,
diff --git a/blueprints/incredible-squaring-eigenlayer/src/jobs/initialize_task.rs b/blueprints/incredible-squaring-eigenlayer/src/jobs/initialize_task.rs
index 28fda986a..644e106d6 100644
--- a/blueprints/incredible-squaring-eigenlayer/src/jobs/initialize_task.rs
+++ b/blueprints/incredible-squaring-eigenlayer/src/jobs/initialize_task.rs
@@ -15,7 +15,7 @@ const BLOCK_TIME_SECONDS: u32 = 12;
     id = 1,
     params(task, task_index),
     event_listener(
-        listener = EvmContractEventListener<IncredibleSquaringTaskManager::NewTaskCreated>,
+        listener = EvmContractEventListener<AggregatorContext, IncredibleSquaringTaskManager::NewTaskCreated>,
         instance = IncredibleSquaringTaskManager,
         abi = INCREDIBLE_SQUARING_TASK_MANAGER_ABI_STRING,
         pre_processor = convert_event_to_inputs,
diff --git a/blueprints/incredible-squaring/foundry.toml b/blueprints/incredible-squaring/foundry.toml
index a6a10d696..7b1b47b70 100644
--- a/blueprints/incredible-squaring/foundry.toml
+++ b/blueprints/incredible-squaring/foundry.toml
@@ -6,5 +6,8 @@ remappings = ["incredible-squaring/=contracts/src/"]
 auto_detect_remappings = true
 solc_version = "0.8.20"
 
+[soldeer]
+recursive_deps = true
+
 [dependencies]
-tnt-core = "0.1.0"
+tnt-core = { version = "0.2.0", git = "https://github.com/tangle-network/tnt-core.git", rev = "5de7c3a6336494a231cb86c2b7c32b81b274d5e6" }
diff --git a/blueprints/incredible-squaring/remappings.txt b/blueprints/incredible-squaring/remappings.txt
index 31585642a..01293805d 100644
--- a/blueprints/incredible-squaring/remappings.txt
+++ b/blueprints/incredible-squaring/remappings.txt
@@ -1,1 +1,1 @@
-tnt-core/=dependencies/tnt-core-0.1.0/src/
+tnt-core/=dependencies/tnt-core-0.2.0/src/
diff --git a/blueprints/incredible-squaring/soldeer.lock b/blueprints/incredible-squaring/soldeer.lock
index 5b0485f96..de78ce241 100644
--- a/blueprints/incredible-squaring/soldeer.lock
+++ b/blueprints/incredible-squaring/soldeer.lock
@@ -1,6 +1,5 @@
 [[dependencies]]
 name = "tnt-core"
-version = "0.1.0"
-url = "https://soldeer-revisions.s3.amazonaws.com/tnt-core/0_1_0_10-12-2024_20:51:20_tnt-core.zip"
-checksum = "628b2475c4d629c1cb2578b41b2e10daa3711d711aa5f5b0dd6153959808620f"
-integrity = "c380cd3e1def40cd40d70db357afdca4efba79d2561f850f05f395dc634a774e"
+version = "0.2.0"
+git = "https://github.com/tangle-network/tnt-core.git"
+rev = "5de7c3a6336494a231cb86c2b7c32b81b274d5e6"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 9325643c0..a201aa03e 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -123,3 +123,7 @@ evm = [
     "gadget-keystore/evm",
     "gadget-clients/evm",
 ]
+
+[lints.clippy]
+large_enum_variant = "allow" # TODO
+result_large_err = "allow"
diff --git a/cli/src/deploy/tangle.rs b/cli/src/deploy/tangle.rs
index f88ecd172..52b6be24f 100644
--- a/cli/src/deploy/tangle.rs
+++ b/cli/src/deploy/tangle.rs
@@ -3,14 +3,16 @@ use alloy_provider::{Provider, WsConnect};
 use alloy_rpc_types_eth::TransactionRequest;
 use alloy_signer_local::PrivateKeySigner;
 use color_eyre::eyre::{self, Context, ContextCompat, Result};
-use gadget_blueprint_proc_macro_core::{BlueprintManager, ServiceBlueprint};
+use gadget_blueprint_proc_macro_core::BlueprintServiceManager;
 use gadget_crypto::tangle_pair_signer::TanglePairSigner;
 use gadget_std::fmt::Debug;
 use gadget_std::path::PathBuf;
+use serde_json::Value;
 use subxt::tx::Signer;
 use tangle_subxt::subxt;
 use tangle_subxt::tangle_testnet_runtime::api as TangleApi;
 use tangle_subxt::tangle_testnet_runtime::api::services::calls::types;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::service::ServiceBlueprint;
 
 #[derive(Clone)]
 pub struct Opts {
@@ -141,7 +143,7 @@ pub async fn deploy_to_tangle(
 
 pub fn load_blueprint_metadata(
     package: &cargo_metadata::Package,
-) -> Result<ServiceBlueprint<'static>> {
+) -> Result<gadget_blueprint_proc_macro_core::ServiceBlueprint<'static>> {
     let blueprint_json_path = package
         .manifest_path
         .parent()
@@ -167,14 +169,14 @@ pub fn load_blueprint_metadata(
 async fn deploy_contracts_to_tangle(
     rpc_url: &str,
     package: &cargo_metadata::Package,
-    blueprint: &mut ServiceBlueprint<'_>,
+    blueprint: &mut gadget_blueprint_proc_macro_core::ServiceBlueprint<'static>,
     signer_evm: Option<PrivateKeySigner>,
 ) -> Result<()> {
     enum ContractKind {
         Manager,
     }
     let contract_paths = match blueprint.manager {
-        BlueprintManager::Evm(ref path) => vec![(ContractKind::Manager, path)],
+        BlueprintServiceManager::Evm(ref path) => vec![(ContractKind::Manager, path)],
         _ => return Err(Error::UnsupportedBlueprintManager.into()),
     };
 
@@ -249,7 +251,7 @@ async fn deploy_contracts_to_tangle(
             tracing::info!("Contract {name} deployed at: {contract_address}");
             match kind {
                 ContractKind::Manager => {
-                    blueprint.manager = BlueprintManager::Evm(contract_address.to_string());
+                    blueprint.manager = BlueprintServiceManager::Evm(contract_address.to_string());
                 }
             }
         } else {
@@ -263,10 +265,10 @@ async fn deploy_contracts_to_tangle(
 /// Checks if the contracts need to be built and builds them if needed.
 fn build_contracts_if_needed(
     package: &cargo_metadata::Package,
-    blueprint: &ServiceBlueprint,
+    blueprint: &gadget_blueprint_proc_macro_core::ServiceBlueprint<'static>,
 ) -> Result<()> {
     let pathes_to_check = match blueprint.manager {
-        BlueprintManager::Evm(ref path) => vec![path],
+        BlueprintServiceManager::Evm(ref path) => vec![path],
         _ => return Err(Error::UnsupportedBlueprintManager.into()),
     };
 
@@ -307,8 +309,8 @@ fn build_contracts_if_needed(
 
 /// Converts the ServiceBlueprint to a format that can be sent to the Tangle Network.
 fn bake_blueprint(
-    blueprint: ServiceBlueprint,
-) -> Result<TangleApi::runtime_types::tangle_primitives::services::ServiceBlueprint> {
+    blueprint: gadget_blueprint_proc_macro_core::ServiceBlueprint<'static>,
+) -> Result<ServiceBlueprint> {
     let mut blueprint_json = serde_json::to_value(&blueprint)?;
     convert_to_bytes_or_null(&mut blueprint_json["metadata"]["name"]);
     convert_to_bytes_or_null(&mut blueprint_json["metadata"]["description"]);
@@ -368,6 +370,10 @@ fn bake_blueprint(
         }
     }
 
+    // TODO
+    blueprint_json["supported_membership_models"] =
+        Value::Array(vec![Value::String(String::from("Fixed"))]);
+
     let blueprint = serde_json::from_value(blueprint_json)?;
     Ok(blueprint)
 }
diff --git a/clippy.toml b/clippy.toml
new file mode 100644
index 000000000..015df30b9
--- /dev/null
+++ b/clippy.toml
@@ -0,0 +1,1 @@
+doc-valid-idents = ["EigenLayer", ".."]
\ No newline at end of file
diff --git a/crates/blueprint/manager/src/executor/event_handler.rs b/crates/blueprint/manager/src/executor/event_handler.rs
index 85f1d06fd..0a691b162 100644
--- a/crates/blueprint/manager/src/executor/event_handler.rs
+++ b/crates/blueprint/manager/src/executor/event_handler.rs
@@ -14,7 +14,7 @@ use gadget_logging::{error, info, trace, warn};
 use std::fmt::Debug;
 use std::sync::atomic::Ordering;
 use tangle_subxt::subxt::utils::AccountId32;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::gadget::{
     Gadget, GadgetSourceFetcher,
 };
 use tangle_subxt::tangle_testnet_runtime::api::services::events::{
diff --git a/crates/blueprint/manager/src/gadget/native.rs b/crates/blueprint/manager/src/gadget/native.rs
index 5cf46adae..0b53d54c5 100644
--- a/crates/blueprint/manager/src/gadget/native.rs
+++ b/crates/blueprint/manager/src/gadget/native.rs
@@ -1,6 +1,6 @@
 use crate::sdk::utils::get_formatted_os_string;
 use gadget_config::Protocol;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::gadget::{
     Gadget, GadgetBinary,
 };
 
diff --git a/crates/blueprint/manager/src/sdk/utils.rs b/crates/blueprint/manager/src/sdk/utils.rs
index 11467525f..84d318533 100644
--- a/crates/blueprint/manager/src/sdk/utils.rs
+++ b/crates/blueprint/manager/src/sdk/utils.rs
@@ -5,7 +5,7 @@ use std::path::Path;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::field::BoundedString;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::gadget::{
     GadgetBinary, GithubFetcher,
 };
 
diff --git a/crates/blueprint/manager/src/sources/github.rs b/crates/blueprint/manager/src/sources/github.rs
index b1266a85e..91cbd009e 100644
--- a/crates/blueprint/manager/src/sources/github.rs
+++ b/crates/blueprint/manager/src/sources/github.rs
@@ -6,7 +6,7 @@ use crate::sources::BinarySourceFetcher;
 use async_trait::async_trait;
 use gadget_logging::info;
 use std::path::PathBuf;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::GithubFetcher;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::gadget::GithubFetcher;
 use tokio::io::AsyncWriteExt;
 
 pub struct GithubBinaryFetcher {
diff --git a/crates/blueprint/metadata/src/lib.rs b/crates/blueprint/metadata/src/lib.rs
index f7e4300b1..36ee7ed3d 100644
--- a/crates/blueprint/metadata/src/lib.rs
+++ b/crates/blueprint/metadata/src/lib.rs
@@ -5,7 +5,7 @@ use gadget_std::process::{Command, Stdio};
 
 use cargo_metadata::{Metadata, Package};
 use gadget_blueprint_proc_macro_core::{
-    BlueprintManager, FieldType, Gadget, GadgetSource, GadgetSourceFetcher, JobDefinition,
+    BlueprintServiceManager, FieldType, Gadget, GadgetSource, GadgetSourceFetcher, JobDefinition,
     MasterBlueprintServiceManagerRevision, NativeGadget, ServiceBlueprint, ServiceMetadata,
     TestFetcher,
 };
@@ -305,7 +305,7 @@ fn extract_metadata() -> Result<Metadata, Error> {
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 struct BlueprintMetadata {
-    manager: BlueprintManager,
+    manager: BlueprintServiceManager,
     #[serde(alias = "master_revision", default)]
     master_blueprint_service_manager_revision: MasterBlueprintServiceManagerRevision,
 }
@@ -320,7 +320,7 @@ fn extract_blueprint_metadata(package: &Package) -> Result<BlueprintMetadata, Er
     let mut metadata: BlueprintMetadata =
         serde_json::from_value(blueprint.clone()).map_err(Error::DeserializeGadget)?;
     match &mut metadata.manager {
-        BlueprintManager::Evm(manager) => {
+        BlueprintServiceManager::Evm(manager) => {
             let path = resolve_evm_contract_path_by_name(manager);
             *manager = path.display().to_string();
         }
diff --git a/crates/clients/Cargo.toml b/crates/clients/Cargo.toml
index b080eb982..eae5fe236 100644
--- a/crates/clients/Cargo.toml
+++ b/crates/clients/Cargo.toml
@@ -12,7 +12,6 @@ repository.workspace = true
 gadget-client-eigenlayer = { workspace = true, optional = true }
 gadget-client-evm = { workspace = true, optional = true }
 gadget-client-tangle = { workspace = true, optional = true }
-gadget-client-networking = { workspace = true, optional = true }
 gadget-client-core = { workspace = true }
 gadget-std.workspace = true
 
@@ -23,14 +22,13 @@ default = ["std"]
 std = [
 	"gadget-client-eigenlayer?/std",
 	"gadget-client-evm?/std",
-	"gadget-client-networking?/std",
 	"gadget-client-tangle?/std",
 	"gadget-std/std",
-	"thiserror/std"
+	"thiserror/std",
 ]
 web = ["gadget-client-tangle?/web"]
 
 eigenlayer = ["dep:gadget-client-eigenlayer"]
 evm = ["dep:gadget-client-evm"]
-networking = ["dep:gadget-client-networking"]
-tangle = ["dep:gadget-client-tangle"]
\ No newline at end of file
+tangle = ["dep:gadget-client-tangle"]
+networking = []
diff --git a/crates/clients/eigenlayer/Cargo.toml b/crates/clients/eigenlayer/Cargo.toml
index aa5647db0..c0a5351cb 100644
--- a/crates/clients/eigenlayer/Cargo.toml
+++ b/crates/clients/eigenlayer/Cargo.toml
@@ -51,4 +51,7 @@ std = [
 	"tokio/full",
 	"gadget-client-core/std",
 	"url/std",
-]
\ No newline at end of file
+]
+
+[lints.clippy]
+large_enum_variant = "allow" # TODO
\ No newline at end of file
diff --git a/crates/clients/networking/Cargo.toml b/crates/clients/networking/Cargo.toml
deleted file mode 100644
index 1b37a9963..000000000
--- a/crates/clients/networking/Cargo.toml
+++ /dev/null
@@ -1,34 +0,0 @@
-[package]
-name = "gadget-client-networking"
-version = "0.1.0"
-description = "Networking client for Tangle Blueprints"
-authors.workspace = true
-edition.workspace = true
-license.workspace = true
-homepage.workspace = true
-repository.workspace = true
-
-[dependencies]
-gadget-config = { workspace = true, features = ["networking"] }
-gadget-crypto = { workspace = true, features = ["k256"] }
-gadget-logging = { workspace = true }
-gadget-networking = { workspace = true, features = ["round-based-compat"] }
-gadget-client-core = { workspace = true }
-gadget-std = { workspace = true }
-libp2p = { workspace = true }
-serde = { workspace = true }
-serde_json = { workspace = true, features = ["alloc"] }
-thiserror = { workspace = true }
-
-[features]
-default = ["std"]
-std = [
-    "gadget-config/std",
-    "gadget-crypto/std",
-    "gadget-logging/std",
-    "gadget-client-core/std",
-    "gadget-networking/std",
-    "gadget-std/std",
-    "serde/std",
-    "serde_json/std",
-] 
\ No newline at end of file
diff --git a/crates/clients/networking/src/error.rs b/crates/clients/networking/src/error.rs
deleted file mode 100644
index 8d1e18031..000000000
--- a/crates/clients/networking/src/error.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-use gadget_std::string::String;
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum Error {
-    #[error("P2P error: {0}")]
-    P2p(String),
-    #[error("Transport error: {0}")]
-    Transport(String),
-    #[error("Protocol error: {0}")]
-    Protocol(String),
-    #[error("Configuration error: {0}")]
-    Configuration(String),
-}
-
-impl From<Error> for gadget_client_core::error::Error {
-    fn from(value: Error) -> Self {
-        gadget_client_core::error::Error::Network(value.to_string())
-    }
-}
-
-pub type Result<T> = gadget_std::result::Result<T, Error>;
diff --git a/crates/clients/networking/src/lib.rs b/crates/clients/networking/src/lib.rs
deleted file mode 100644
index 893f5304b..000000000
--- a/crates/clients/networking/src/lib.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-#![cfg_attr(not(feature = "std"), no_std)]
-
-pub mod error;
-pub mod p2p;
diff --git a/crates/clients/networking/src/p2p.rs b/crates/clients/networking/src/p2p.rs
deleted file mode 100644
index 266a1c00f..000000000
--- a/crates/clients/networking/src/p2p.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-use crate::error::{Error, Result};
-use gadget_config::GadgetConfiguration;
-use gadget_networking::gossip::GossipHandle;
-use gadget_networking::round_based_compat::NetworkDeliveryWrapper;
-use gadget_networking::setup::NetworkConfig;
-use gadget_networking::{networking::NetworkMultiplexer, round_based};
-use gadget_networking::{GossipMsgKeyPair, GossipMsgPublicKey};
-use gadget_std::collections::BTreeMap;
-use gadget_std::sync::Arc;
-use round_based::PartyIndex;
-
-pub struct P2PClient {
-    name: String,
-    config: GadgetConfiguration,
-    target_port: u16,
-    gossip_msg_keypair: GossipMsgKeyPair,
-}
-
-impl P2PClient {
-    pub fn new(
-        name: String,
-        config: GadgetConfiguration,
-        target_port: u16,
-        gossip_msg_keypair: GossipMsgKeyPair,
-    ) -> Self {
-        Self {
-            name,
-            config,
-            target_port,
-            gossip_msg_keypair,
-        }
-    }
-
-    pub fn config(&self) -> &GadgetConfiguration {
-        &self.config
-    }
-
-    /// Returns the network protocol identifier
-    pub fn network_protocol(&self, version: Option<String>) -> String {
-        let name = self.name.to_lowercase();
-        match version {
-            Some(v) => format!("/{}/{}", name, v),
-            None => format!("/{}/1.0.0", name),
-        }
-    }
-
-    pub fn libp2p_identity(&self, ed25519_seed: Vec<u8>) -> Result<libp2p::identity::Keypair> {
-        let mut seed_bytes = ed25519_seed;
-        let keypair = libp2p::identity::Keypair::ed25519_from_bytes(&mut seed_bytes)
-            .map_err(|err| Error::Configuration(err.to_string()))?;
-        Ok(keypair)
-    }
-
-    /// Returns a new `NetworkConfig` for the current environment.
-    pub fn libp2p_network_config<T: Into<String>>(
-        &self,
-        network_name: T,
-        ed25519_seed: Vec<u8>,
-    ) -> Result<NetworkConfig> {
-        let network_identity = self.libp2p_identity(ed25519_seed)?;
-        let network_config = NetworkConfig::new_service_network(
-            network_identity,
-            self.gossip_msg_keypair.clone(),
-            self.config.bootnodes.clone(),
-            self.target_port,
-            network_name,
-        );
-
-        Ok(network_config)
-    }
-
-    /// Starts the P2P network and returns the gossip handle
-    pub fn start_p2p_network<T: Into<String>>(
-        &self,
-        network_name: T,
-        ed25519_seed: Vec<u8>,
-    ) -> Result<GossipHandle> {
-        let network_config = self.libp2p_network_config(network_name, ed25519_seed)?;
-        match gadget_networking::setup::start_p2p_network(network_config) {
-            Ok(handle) => Ok(handle),
-            Err(err) => {
-                gadget_logging::error!("Failed to start network: {}", err.to_string());
-                Err(Error::Protocol(format!("Failed to start network: {err}")))
-            }
-        }
-    }
-
-    /// Creates a network multiplexer backend
-    pub fn create_network_multiplexer<T: Into<String>>(
-        &self,
-        network_name: T,
-        ed25519_seed: Vec<u8>,
-    ) -> Result<Arc<NetworkMultiplexer>> {
-        let handle = self.start_p2p_network(network_name, ed25519_seed)?;
-        Ok(Arc::new(NetworkMultiplexer::new(handle)))
-    }
-
-    /// Creates a network delivery wrapper
-    pub fn create_network_delivery_wrapper<M>(
-        &self,
-        mux: Arc<NetworkMultiplexer>,
-        party_index: PartyIndex,
-        task_hash: [u8; 32],
-        parties: BTreeMap<PartyIndex, GossipMsgPublicKey>,
-    ) -> NetworkDeliveryWrapper<M>
-    where
-        M: Clone
-            + Send
-            + Unpin
-            + 'static
-            + serde::Serialize
-            + serde::de::DeserializeOwned
-            + round_based::ProtocolMessage,
-    {
-        NetworkDeliveryWrapper::new(mux, party_index, task_hash, parties)
-    }
-}
diff --git a/crates/clients/tangle/src/client.rs b/crates/clients/tangle/src/client.rs
index d790568fa..cac1a9f6f 100644
--- a/crates/clients/tangle/src/client.rs
+++ b/crates/clients/tangle/src/client.rs
@@ -45,8 +45,6 @@ pub struct TangleClient {
     services_client: TangleServicesClient<TangleConfig>,
 }
 
-const KEY_ID: &str = "tangle-default";
-
 impl TangleClient {
     /// Create a new Tangle runtime client from an existing [`GadgetConfiguration`].
     pub async fn new(config: GadgetConfiguration) -> std::result::Result<Self, Error> {
@@ -154,16 +152,19 @@ impl TangleClient {
         Error,
     > {
         let parties = self.get_operators().await?;
-        let my_id = self.keystore.get_public_key_local::<SpEcdsa>(KEY_ID)?;
+        let my_id = self
+            .keystore
+            .first_local::<SpSr25519>()
+            .map_err(Error::Keystore)?;
 
         gadget_logging::trace!(
-            "Looking for {my_id:?} in parties: {:?}",
+            "Looking for {my_id} in parties: {:?}",
             parties.keys().collect::<Vec<_>>()
         );
 
         let index_of_my_id = parties
             .iter()
-            .position(|(_id, key)| key == &my_id.0)
+            .position(|(id, _key)| id.0 == my_id.0.to_raw())
             .ok_or(Error::PartyNotFound)?;
 
         Ok((index_of_my_id, parties))
@@ -303,7 +304,7 @@ impl GadgetServicesClient for TangleClient {
     async fn operator_id(
         &self,
     ) -> std::result::Result<Self::PublicApplicationIdentity, Self::Error> {
-        Ok(self.keystore.get_public_key_local::<SpEcdsa>(KEY_ID)?.0)
+        Ok(self.keystore.first_local::<SpEcdsa>()?.0)
     }
 
     /// Retrieves the current blueprint ID from the configuration
diff --git a/crates/clients/tangle/src/services.rs b/crates/clients/tangle/src/services.rs
index e6221ef9a..c06c2c698 100644
--- a/crates/clients/tangle/src/services.rs
+++ b/crates/clients/tangle/src/services.rs
@@ -8,10 +8,9 @@ use subxt::utils::H256;
 use subxt::{Config, OnlineClient};
 use tangle_subxt::subxt;
 use tangle_subxt::tangle_testnet_runtime::api;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::sp_arithmetic::per_things::Percent;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::{
-    self, ServiceBlueprint,
-};
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::types::AssetSecurityCommitment;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::service::ServiceBlueprint;
 
 /// A client for interacting with the services API
 #[derive(Debug, Clone)]
@@ -35,7 +34,8 @@ impl<C: Config> gadget_std::ops::Deref for TangleServicesClient<C> {
 }
 
 /// A list of services provided by an operator, along with their blueprint
-pub type RpcServicesWithBlueprint = services::RpcServicesWithBlueprint<AccountId32, u64, u128>;
+pub type RpcServicesWithBlueprint =
+    services::service::RpcServicesWithBlueprint<AccountId32, u64, u128>;
 
 impl<C: Config> TangleServicesClient<C>
 where
@@ -105,12 +105,18 @@ where
         &self,
         at: [u8; 32],
         service_id: u64,
-    ) -> Result<Vec<(AccountId32, Percent)>> {
+    ) -> Result<Vec<(AccountId32, Vec<AssetSecurityCommitment<u128>>)>> {
         let call = api::storage().services().instances(service_id);
         let at = BlockRef::from_hash(H256::from_slice(&at));
         let ret = self.rpc_client.storage().at(at).fetch(&call).await?;
         match ret {
-            Some(instances) => Ok(instances.operators.0),
+            Some(instances) => {
+                let mut ret = Vec::new();
+                for (account, security_commitments) in instances.operator_security_commitments.0 {
+                    ret.push((account, security_commitments.0))
+                }
+                Ok(ret)
+            }
             None => Ok(Vec::new()),
         }
     }
diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml
index 80c51bac8..a008c6f1c 100644
--- a/crates/config/Cargo.toml
+++ b/crates/config/Cargo.toml
@@ -52,3 +52,6 @@ keystore = [
 
 # Testing features
 test-utils = ["std"]
+
+[lints]
+workspace = true
\ No newline at end of file
diff --git a/crates/config/src/context_config.rs b/crates/config/src/context_config.rs
index 39b2d06f8..90176eb9c 100644
--- a/crates/config/src/context_config.rs
+++ b/crates/config/src/context_config.rs
@@ -1,9 +1,9 @@
 #![allow(unused_variables, dead_code)]
-use super::*;
+use super::supported_chains::SupportedChains;
+use super::{Protocol, ProtocolSettings};
 use gadget_std::fmt::Debug;
 use gadget_std::str::FromStr;
 use serde::{Deserialize, Serialize};
-use supported_chains::SupportedChains;
 use url::Url;
 
 #[cfg(feature = "networking")]
@@ -33,6 +33,22 @@ pub enum GadgetCLICoreSettings {
         #[arg(long, value_parser = <Multiaddr as gadget_std::str::FromStr>::from_str, action = clap::ArgAction::Append, env)]
         #[serde(default)]
         bootnodes: Option<Vec<Multiaddr>>,
+        #[cfg(feature = "networking")]
+        #[arg(long, env)]
+        #[serde(default)]
+        network_bind_port: Option<u16>,
+        #[cfg(feature = "networking")]
+        #[arg(long, env)]
+        #[serde(default)]
+        enable_mdns: bool,
+        #[cfg(feature = "networking")]
+        #[arg(long, env)]
+        #[serde(default)]
+        enable_kademlia: bool,
+        #[cfg(feature = "networking")]
+        #[arg(long, env)]
+        #[serde(default)]
+        target_peer_count: Option<u32>,
         #[arg(long, short = 'd', env)]
         keystore_uri: String,
         #[arg(long, value_enum, env)]
@@ -212,6 +228,14 @@ impl Default for GadgetCLICoreSettings {
             ws_rpc_url: default_ws_rpc_url(),
             #[cfg(feature = "networking")]
             bootnodes: None,
+            #[cfg(feature = "networking")]
+            network_bind_port: None,
+            #[cfg(feature = "networking")]
+            enable_mdns: false,
+            #[cfg(feature = "networking")]
+            enable_kademlia: false,
+            #[cfg(feature = "networking")]
+            target_peer_count: None,
             keystore_uri: String::new(),
             chain: SupportedChains::default(),
             verbose: 0,
@@ -270,7 +294,12 @@ impl ContextConfig {
     /// - `symbiotic_contract_addresses`: The [`contract addresses`](SymbioticContractAddresses) for the necessary Symbiotic contracts
     /// - `blueprint_id`: The blueprint ID - only required for Tangle
     /// - `service_id`: The service ID - only required for Tangle
-    #[allow(clippy::too_many_arguments)]
+    #[allow(
+        clippy::too_many_arguments,
+        clippy::too_many_lines,
+        clippy::match_wildcard_for_single_variants
+    )]
+    #[must_use]
     pub fn create_config(
         http_rpc_url: Url,
         ws_rpc_url: Url,
@@ -336,12 +365,25 @@ impl ContextConfig {
         #[cfg(feature = "tangle")]
         let service_id = tangle_settings.and_then(|s| s.service_id);
 
+        #[cfg(feature = "networking")]
+        let enable_mdns = cfg!(debug_assertions);
+        #[cfg(feature = "networking")]
+        let enable_kademlia = !cfg!(debug_assertions);
+
         ContextConfig {
             gadget_core_settings: GadgetCLICoreSettings::Run {
                 test_mode: false,
                 http_rpc_url,
                 #[cfg(feature = "networking")]
                 bootnodes: None,
+                #[cfg(feature = "networking")]
+                network_bind_port: None,
+                #[cfg(feature = "networking")]
+                enable_mdns,
+                #[cfg(feature = "networking")]
+                enable_kademlia,
+                #[cfg(feature = "networking")]
+                target_peer_count: None,
                 keystore_uri,
                 chain,
                 verbose: 3,
@@ -390,11 +432,12 @@ impl ContextConfig {
     /// Creates a new context config with the given parameters
     ///
     /// # Defaults
-    /// - `target_addr`: The same host address as the given http_rpc_url, defaulting to 127.0.0.1 if an error occurs
-    /// - `target_port`: The same port as the given http_rpc_url, defaulting to 0 if an error occurs
+    /// - `target_addr`: The same host address as the given `http_rpc_url`, defaulting to 127.0.0.1 if an error occurs
+    /// - `target_port`: The same port as the given `http_rpc_url`, defaulting to 0 if an error occurs
     /// - `skip_registration`: false
     /// - `keystore_password`: None
     #[allow(clippy::too_many_arguments)]
+    #[must_use]
     pub fn create_config_with_defaults(
         http_rpc_url: Url,
         ws_rpc_url: Url,
@@ -417,6 +460,7 @@ impl ContextConfig {
 
     /// Creates a new context config with defaults for Eigenlayer
     #[cfg(feature = "eigenlayer")]
+    #[must_use]
     pub fn create_eigenlayer_config(
         http_rpc_url: Url,
         ws_rpc_url: Url,
@@ -438,6 +482,7 @@ impl ContextConfig {
 
     /// Creates a new context config with defaults for Symbiotic
     #[cfg(feature = "symbiotic")]
+    #[must_use]
     pub fn create_symbiotic_config(
         http_rpc_url: Url,
         ws_rpc_url: Url,
@@ -459,6 +504,7 @@ impl ContextConfig {
 
     /// Creates a new context config with defaults for Tangle
     #[cfg(feature = "tangle")]
+    #[must_use]
     pub fn create_tangle_config(
         http_rpc_url: Url,
         ws_rpc_url: Url,
@@ -468,7 +514,7 @@ impl ContextConfig {
         blueprint_id: u64,
         service_id: Option<u64>,
     ) -> Self {
-        use protocol::TangleInstanceSettings;
+        use super::protocol::TangleInstanceSettings;
 
         Self::create_config_with_defaults(
             http_rpc_url,
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 0e2d31e10..a83af4375 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -1,4 +1,5 @@
 #![allow(unused_variables, unreachable_code)]
+
 use gadget_std::fmt::Debug;
 use gadget_std::string::{String, ToString};
 
@@ -68,14 +69,31 @@ pub enum Error {
     /// Missing `SymbioticContractAddresses`
     #[error("Missing SymbioticContractAddresses")]
     MissingSymbioticContractAddresses,
+
+    #[cfg(feature = "networking")]
+    #[error(transparent)]
+    Networking(#[from] gadget_networking::error::Error),
+
     #[error("Bad RPC Connection: {0}")]
     BadRpcConnection(String),
     #[error("Configuration error: {0}")]
     ConfigurationError(String),
+
+    #[cfg(feature = "networking")]
+    #[error("Failed to parse Multiaddr: {0}")]
+    Multiaddr(#[from] libp2p::multiaddr::Error),
 }
 
 #[cfg(feature = "networking")]
-use libp2p::Multiaddr;
+pub use networking_imports::*;
+#[cfg(feature = "networking")]
+mod networking_imports {
+    // pub use gadget_networking::networking::NetworkMultiplexer;
+    // pub use gadget_networking::start_p2p_network;
+    pub use gadget_networking::NetworkConfig;
+    pub use libp2p::Multiaddr;
+    pub use std::sync::Arc;
+}
 
 /// Gadget environment.
 #[non_exhaustive]
@@ -94,24 +112,65 @@ pub struct GadgetConfiguration {
     /// The list of bootnodes to connect to
     #[cfg(feature = "networking")]
     pub bootnodes: Vec<Multiaddr>,
+    /// The port to bind the network to
+    #[cfg(feature = "networking")]
+    pub network_bind_port: u16,
     /// The type of protocol the gadget is executing on.
     pub protocol: Protocol,
     /// Protocol-specific settings
     pub protocol_settings: ProtocolSettings,
     /// Whether the gadget is in test mode
     pub test_mode: bool,
+    /// Whether to enable mDNS
+    #[cfg(feature = "networking")]
+    pub enable_mdns: bool,
+    /// Whether to enable Kademlia
+    #[cfg(feature = "networking")]
+    pub enable_kademlia: bool,
+    /// The target number of peers to connect to
+    #[cfg(feature = "networking")]
+    pub target_peer_count: u32,
 }
 
 impl GadgetConfiguration {
+    /// Start a p2p network with the given `network_config`
+    ///
+    /// # Errors
+    ///
+    /// See [`NetworkService::new()`]
+    ///
+    /// [`NetworkService::new()`]: gadget_networking::NetworkService::new
+    #[cfg(feature = "networking")]
+    pub fn libp2p_start_network(
+        &self,
+        network_config: gadget_networking::NetworkConfig,
+        allowed_keys: gadget_std::collections::HashSet<gadget_networking::InstanceMsgPublicKey>,
+    ) -> Result<gadget_networking::service_handle::NetworkServiceHandle, Error> {
+        let networking_service =
+            gadget_networking::NetworkService::new(network_config, allowed_keys)?;
+
+        let handle = networking_service.start();
+
+        Ok(handle)
+    }
+
     /// Returns a new `NetworkConfig` for the current environment.
+    ///
+    /// # Errors
+    ///
+    /// Missing the following keys in the keystore:
+    ///
+    /// * `Ed25519`
+    /// * `ECDSA`
     #[cfg(feature = "networking")]
+    #[allow(clippy::missing_panics_doc)] // Known good Multiaddr
     pub fn libp2p_network_config(
         &self,
         network_name: impl Into<String>,
-    ) -> Result<gadget_networking::setup::NetworkConfig, Error> {
+    ) -> Result<gadget_networking::NetworkConfig, Error> {
         use gadget_keystore::backends::Backend;
         use gadget_keystore::crypto::sp_core::SpEd25519 as LibP2PKeyType;
-        use gadget_networking::key_types::Curve as GossipMsgKeyPair;
+        use gadget_networking::key_types::Curve as InstanceMsgKeyPair;
 
         let keystore_config = gadget_keystore::KeystoreConfig::new().fs_root(&self.keystore_uri);
         let keystore = gadget_keystore::Keystore::new(keystore_config)
@@ -125,67 +184,36 @@ impl GadgetConfiguration {
         let network_identity = libp2p::identity::Keypair::ed25519_from_bytes(ed25519_pair.seed())
             .map_err(|err| Error::ConfigurationError(err.to_string()))?;
 
-        let port = self.get_port()?;
         let ecdsa_pub_key = keystore
-            .first_local::<GossipMsgKeyPair>()
+            .first_local::<InstanceMsgKeyPair>()
             .map_err(|err| Error::ConfigurationError(err.to_string()))?;
         let ecdsa_pair = keystore
-            .get_secret::<GossipMsgKeyPair>(&ecdsa_pub_key)
+            .get_secret::<InstanceMsgKeyPair>(&ecdsa_pub_key)
             .map_err(|err| Error::ConfigurationError(err.to_string()))?;
 
-        let network_config = gadget_networking::setup::NetworkConfig::new_service_network(
-            network_identity,
-            ecdsa_pair,
-            self.bootnodes.clone(),
-            port,
+        let listen_addr: Multiaddr = format!("/ip4/0.0.0.0/tcp/{}", self.network_bind_port)
+            .parse()
+            .expect("valid multiaddr; qed");
+
+        let network_name: String = network_name.into();
+        let network_config = NetworkConfig {
+            instance_id: network_name.clone(),
             network_name,
-        );
+            instance_key_pair: ecdsa_pair,
+            local_key: network_identity,
+            listen_addr,
+            target_peer_count: self.target_peer_count,
+            bootstrap_peers: self.bootnodes.clone(),
+            enable_mdns: self.enable_mdns,
+            enable_kademlia: self.enable_kademlia,
+        };
 
         Ok(network_config)
     }
-
-    /// Attempt to look at the ws_rpc_endpoint and extract the port. If not found,
-    /// looks at the http_rpc_endpoint and tries to extract the port.
-    ///
-    /// Fails if no port is found
-    #[cfg(feature = "networking")]
-    fn get_port(&self) -> Result<u16, Error> {
-        let ws_port = self.ws_rpc_endpoint.split(":").last();
-
-        if let Some(port) = ws_port {
-            return get_port_sanitized(port).ok_or_else(|| {
-                Error::BadRpcConnection(format!("Bad WS port formatting from: {ws_port:?}"))
-            });
-        }
-
-        let http_port = self.http_rpc_endpoint.split(":").last();
-
-        if let Some(port) = http_port {
-            return get_port_sanitized(port).ok_or_else(|| {
-                Error::BadRpcConnection(format!("Bad HTTP port formatting from: {http_port:?}"))
-            });
-        }
-
-        Err(Error::BadRpcConnection("No port found".to_string()))
-    }
-}
-
-#[cfg(feature = "networking")]
-/// Sanitizes the string
-fn get_port_sanitized<T: AsRef<str>>(port: T) -> Option<u16> {
-    let port = port.as_ref();
-    // Only get the numberic values
-    let mut port_sanitized = String::new();
-    for c in port.chars() {
-        if c.is_numeric() {
-            port_sanitized.push(c);
-        }
-    }
-
-    port_sanitized.parse().ok()
 }
 
 /// Loads the [`GadgetConfiguration`] from the current environment.
+///
 /// # Errors
 ///
 /// This function will return an error if any of the required environment variables are missing.
@@ -193,6 +221,7 @@ pub fn load(config: ContextConfig) -> Result<GadgetConfiguration, Error> {
     load_inner(config)
 }
 
+#[allow(clippy::too_many_lines)]
 fn load_inner(config: ContextConfig) -> Result<GadgetConfiguration, Error> {
     tracing::info_span!("gadget");
     let ContextConfig {
@@ -203,6 +232,14 @@ fn load_inner(config: ContextConfig) -> Result<GadgetConfiguration, Error> {
                 ws_rpc_url,
                 #[cfg(feature = "networking")]
                 bootnodes,
+                #[cfg(feature = "networking")]
+                network_bind_port,
+                #[cfg(feature = "networking")]
+                enable_mdns,
+                #[cfg(feature = "networking")]
+                enable_kademlia,
+                #[cfg(feature = "networking")]
+                target_peer_count,
                 keystore_uri,
                 protocol,
                 #[cfg(feature = "tangle")]
@@ -320,6 +357,14 @@ fn load_inner(config: ContextConfig) -> Result<GadgetConfiguration, Error> {
         data_dir: None,
         #[cfg(feature = "networking")]
         bootnodes: bootnodes.unwrap_or_default(),
+        #[cfg(feature = "networking")]
+        network_bind_port: network_bind_port.unwrap_or_default(),
+        #[cfg(feature = "networking")]
+        enable_mdns,
+        #[cfg(feature = "networking")]
+        enable_kademlia,
+        #[cfg(feature = "networking")]
+        target_peer_count: target_peer_count.unwrap_or(24),
         protocol,
         protocol_settings,
     })
diff --git a/crates/config/src/protocol.rs b/crates/config/src/protocol.rs
index a4e600e51..4358292f0 100644
--- a/crates/config/src/protocol.rs
+++ b/crates/config/src/protocol.rs
@@ -1,4 +1,5 @@
-use super::*;
+use super::Error;
+
 use core::fmt::Debug;
 use serde::{Deserialize, Serialize};
 
@@ -39,11 +40,16 @@ impl Protocol {
     }
 
     /// Returns the protocol from the environment variable `PROTOCOL`.
+    ///
+    /// # Errors
+    ///
+    /// Infallible
     #[cfg(not(feature = "std"))]
     pub fn from_env() -> Result<Self, Error> {
         Ok(Protocol::default())
     }
 
+    #[must_use]
     pub fn as_str(&self) -> &'static str {
         match self {
             Self::Tangle => "tangle",
@@ -54,7 +60,7 @@ impl Protocol {
 }
 
 impl core::fmt::Display for Protocol {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         write!(f, "{}", self.as_str())
     }
 }
@@ -156,6 +162,7 @@ impl Default for ProtocolSettings {
 }
 
 impl ProtocolSettings {
+    #[must_use]
     pub fn as_str(&self) -> &'static str {
         match self {
             #[cfg(feature = "tangle")]
@@ -168,7 +175,13 @@ impl ProtocolSettings {
         }
     }
 
+    /// Attempt to extract the [`TangleInstanceSettings`]
+    ///
+    /// # Errors
+    ///
+    /// `self` is not [`ProtocolSettings::Tangle`]
     #[cfg(feature = "tangle")]
+    #[allow(clippy::match_wildcard_for_single_variants)]
     pub fn tangle(&self) -> Result<&TangleInstanceSettings, Error> {
         match self {
             Self::Tangle(settings) => Ok(settings),
@@ -176,7 +189,13 @@ impl ProtocolSettings {
         }
     }
 
+    /// Attempt to extract the [`EigenlayerContractAddresses`]
+    ///
+    /// # Errors
+    ///
+    /// `self` is not [`ProtocolSettings::Eigenlayer`]
     #[cfg(feature = "eigenlayer")]
+    #[allow(clippy::match_wildcard_for_single_variants)]
     pub fn eigenlayer(&self) -> Result<&EigenlayerContractAddresses, Error> {
         match self {
             Self::Eigenlayer(settings) => Ok(settings),
@@ -184,7 +203,13 @@ impl ProtocolSettings {
         }
     }
 
+    /// Attempt to extract the [`SymbioticContractAddresses`]
+    ///
+    /// # Errors
+    ///
+    /// `self` is not [`ProtocolSettings::Symbiotic`]
     #[cfg(feature = "symbiotic")]
+    #[allow(clippy::match_wildcard_for_single_variants)]
     pub fn symbiotic(&self) -> Result<&SymbioticContractAddresses, Error> {
         match self {
             Self::Symbiotic(settings) => Ok(settings),
@@ -193,16 +218,19 @@ impl ProtocolSettings {
     }
 
     #[cfg(feature = "tangle")]
+    #[must_use]
     pub fn from_tangle(settings: TangleInstanceSettings) -> Self {
         Self::Tangle(settings)
     }
 
     #[cfg(feature = "eigenlayer")]
+    #[must_use]
     pub fn from_eigenlayer(settings: EigenlayerContractAddresses) -> Self {
         Self::Eigenlayer(settings)
     }
 
     #[cfg(feature = "symbiotic")]
+    #[must_use]
     pub fn from_symbiotic(settings: SymbioticContractAddresses) -> Self {
         Self::Symbiotic(settings)
     }
diff --git a/crates/contexts/Cargo.toml b/crates/contexts/Cargo.toml
index 11356861e..9c7e88b84 100644
--- a/crates/contexts/Cargo.toml
+++ b/crates/contexts/Cargo.toml
@@ -22,19 +22,11 @@ tangle-subxt = { workspace = true, optional = true }
 
 [features]
 default = ["std", "keystore"]
-std = [
-	"gadget-std/std",
-	"gadget-clients/std",
-	"gadget-keystore?/std",
-	"gadget-networking?/std",
-	"tangle-subxt?/std",
-]
-web = [
-	"tangle-subxt?/web",
-]
+std = ["gadget-std/std", "gadget-clients/std", "gadget-keystore?/std", "gadget-networking?/std", "tangle-subxt?/std"]
+web = ["tangle-subxt?/web"]
 
 evm = ["gadget-clients/evm"]
 eigenlayer = ["gadget-clients/eigenlayer"]
-networking = ["gadget-clients/networking", "dep:gadget-networking", "dep:proc-macro2"]
+networking = ["dep:gadget-networking", "dep:proc-macro2"]
 keystore = ["dep:gadget-config", "dep:gadget-keystore"]
-tangle = ["dep:tangle-subxt", "gadget-clients/tangle"]
\ No newline at end of file
+tangle = ["dep:tangle-subxt", "gadget-clients/tangle"]
diff --git a/crates/contexts/src/lib.rs b/crates/contexts/src/lib.rs
index 6a4c038a7..f3de4e538 100644
--- a/crates/contexts/src/lib.rs
+++ b/crates/contexts/src/lib.rs
@@ -4,8 +4,6 @@ pub mod eigenlayer;
 pub mod instrumented_evm_client;
 #[cfg(feature = "keystore")]
 pub mod keystore;
-#[cfg(feature = "networking")]
-pub mod p2p;
 #[cfg(feature = "tangle")]
 pub mod services;
 #[cfg(feature = "tangle")]
diff --git a/crates/contexts/src/p2p.rs b/crates/contexts/src/p2p.rs
index 6461f9001..e69de29bb 100644
--- a/crates/contexts/src/p2p.rs
+++ b/crates/contexts/src/p2p.rs
@@ -1,14 +0,0 @@
-pub use gadget_clients::networking::p2p::P2PClient;
-pub use gadget_networking::GossipMsgKeyPair;
-pub use gadget_std::net::IpAddr;
-pub use proc_macro2;
-
-/// `P2pContext` trait provides access to a peer to peer networking client.
-pub trait P2pContext {
-    fn p2p_client(
-        &self,
-        name: gadget_std::string::String,
-        target_port: u16,
-        my_ecdsa_key: GossipMsgKeyPair,
-    ) -> P2PClient;
-}
diff --git a/crates/crypto/k256/src/lib.rs b/crates/crypto/k256/src/lib.rs
index ed85d095f..2c927c97d 100644
--- a/crates/crypto/k256/src/lib.rs
+++ b/crates/crypto/k256/src/lib.rs
@@ -13,44 +13,11 @@ use gadget_std::string::{String, ToString};
 use gadget_std::UniformRand;
 use k256::ecdsa::signature::SignerMut;
 use k256::ecdsa::{SigningKey, VerifyingKey};
-use serde::{Deserialize, Serialize};
+use std::hash::{Hash, Hasher};
 
 /// ECDSA key type
 pub struct K256Ecdsa;
 
-#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
-pub struct K256VerifyingKey(pub VerifyingKey);
-
-impl From<K256VerifyingKey> for VerifyingKey {
-    fn from(key: K256VerifyingKey) -> Self {
-        key.0
-    }
-}
-
-impl KeyEncoding for K256VerifyingKey {
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.to_sec1_bytes().to_vec()
-    }
-
-    fn from_bytes(bytes: &[u8]) -> core::result::Result<Self, serde::de::value::Error> {
-        let vk = VerifyingKey::from_sec1_bytes(bytes)
-            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
-        Ok(K256VerifyingKey(vk))
-    }
-}
-
-impl PartialOrd for K256VerifyingKey {
-    fn partial_cmp(&self, other: &Self) -> Option<gadget_std::cmp::Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for K256VerifyingKey {
-    fn cmp(&self, other: &Self) -> gadget_std::cmp::Ordering {
-        self.0.to_sec1_bytes().cmp(&other.0.to_sec1_bytes())
-    }
-}
-
 macro_rules! impl_serde_bytes {
     ($wrapper:ident, $inner:path) => {
         #[derive(Clone, PartialEq, Eq, Debug)]
@@ -95,14 +62,43 @@ macro_rules! impl_serde_bytes {
                 D: serde::Deserializer<'de>,
             {
                 let bytes = Vec::<u8>::deserialize(deserializer)?;
-                let inner = <$inner>::from_slice(&bytes)
+                let ret = <$wrapper as KeyEncoding>::from_bytes(&bytes)
                     .map_err(|e| serde::de::Error::custom(e.to_string()))?;
-                Ok($wrapper(inner))
+                Ok(ret)
+            }
+        }
+
+        impl gadget_std::fmt::Display for $wrapper {
+            fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
+                write!(f, "{}", hex::encode(self.to_bytes()))
             }
         }
     };
 }
 
+impl_serde_bytes!(K256VerifyingKey, k256::ecdsa::VerifyingKey);
+
+impl K256VerifyingKey {
+    #[allow(clippy::wrong_self_convention)]
+    fn to_bytes_impl(&self) -> Vec<u8> {
+        self.0.to_sec1_bytes().to_vec()
+    }
+
+    fn from_bytes_impl(bytes: &[u8]) -> Result<Self> {
+        let vk = VerifyingKey::from_sec1_bytes(bytes)
+            .map_err(|e| K256Error::InvalidSigner(e.to_string()))?;
+        Ok(K256VerifyingKey(vk))
+    }
+}
+
+impl Hash for K256VerifyingKey {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        state.write(self.to_bytes().as_slice());
+    }
+}
+
+impl Copy for K256VerifyingKey {}
+
 impl_serde_bytes!(K256SigningKey, k256::ecdsa::SigningKey);
 
 impl K256SigningKey {
diff --git a/crates/crypto/sp-core/src/lib.rs b/crates/crypto/sp-core/src/lib.rs
index ce2cbd02c..ce4c16c70 100644
--- a/crates/crypto/sp-core/src/lib.rs
+++ b/crates/crypto/sp-core/src/lib.rs
@@ -77,6 +77,12 @@ macro_rules! impl_sp_core_pair_public {
             #[derive(Clone, serde::Serialize, serde::Deserialize)]
             pub struct [<Sp $key_type Public>](pub <$pair_type as sp_core::Pair>::Public);
 
+            impl gadget_std::hash::Hash for [<Sp $key_type Public>] {
+                fn hash<H: gadget_std::hash::Hasher>(&self, state: &mut H) {
+                    self.0.to_raw_vec().hash(state);
+                }
+            }
+
             impl KeyEncoding for [<Sp $key_type Public>] {
                 fn to_bytes(&self) -> Vec<u8> {
                     self.0.to_raw_vec()
@@ -112,6 +118,12 @@ macro_rules! impl_sp_core_pair_public {
                     write!(f, "{:?}", self.to_bytes())
                 }
             }
+
+            impl gadget_std::fmt::Display for [<Sp $key_type Public>] {
+                fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
+                    write!(f, "{}", hex::encode(self.to_bytes()))
+                }
+            }
         }
     };
 }
@@ -120,7 +132,7 @@ macro_rules! impl_sp_core_pair_public {
 macro_rules! impl_sp_core_signature {
     ($key_type:ident, $pair_type:ty) => {
         paste::paste! {
-            #[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+            #[derive(Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
             pub struct [<Sp $key_type Signature>](pub <$pair_type as sp_core::Pair>::Signature);
 
             impl PartialOrd for [<Sp $key_type Signature>] {
@@ -140,6 +152,12 @@ macro_rules! impl_sp_core_signature {
                     write!(f, "{:?}", self.0.0)
                 }
             }
+
+            impl gadget_std::fmt::Display for [<Sp $key_type Signature>] {
+                fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
+                    write!(f, "{}", hex::encode(self.0.0))
+                }
+            }
         }
     };
 }
diff --git a/crates/event-listeners/core/Cargo.toml b/crates/event-listeners/core/Cargo.toml
index 4ee62ea10..e4e7eff18 100644
--- a/crates/event-listeners/core/Cargo.toml
+++ b/crates/event-listeners/core/Cargo.toml
@@ -15,6 +15,7 @@ gadget-std = { workspace = true }
 async-trait = { workspace = true }
 thiserror = { workspace = true }
 tokio = { workspace = true, features = ["sync"] }
+auto_impl = { workspace = true }
 
 [features]
 default = ["std"]
diff --git a/crates/event-listeners/core/src/executor.rs b/crates/event-listeners/core/src/executor.rs
index d3754d839..fa21d447a 100644
--- a/crates/event-listeners/core/src/executor.rs
+++ b/crates/event-listeners/core/src/executor.rs
@@ -10,39 +10,47 @@ use gadget_std::pin::Pin;
 /// Provides the structure for the workflow of taking events and running them through a series of steps.
 /// This is meant to be auto-implemented by the job macro onto the provided structs that implement T: EventListener.
 #[async_trait]
-pub trait EventFlowExecutor<T, Ctx>
+pub trait EventFlowExecutor<T, Ctx, Creator>
 where
     T: Send + 'static,
-    Ctx: Send + 'static,
-    Self: EventListener<T, Ctx>,
+    Ctx: Clone + Send + 'static,
+    Creator: Send + 'static,
+    Self: EventListener<T, Ctx, Creator>,
 {
     type PreprocessedEvent: Send + 'static;
     type PreProcessor: ProcessorFunction<
         T,
         Result<
             Option<Self::PreprocessedEvent>,
-            Error<<Self as EventListener<T, Ctx>>::ProcessorError>,
+            Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
         >,
         BoxedFuture<
             Result<
                 Option<Self::PreprocessedEvent>,
-                Error<<Self as EventListener<T, Ctx>>::ProcessorError>,
+                Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
             >,
         >,
     >;
     type JobProcessor: ProcessorFunction<
-        Self::PreprocessedEvent,
-        Result<Self::JobProcessedEvent, Error<<Self as EventListener<T, Ctx>>::ProcessorError>>,
+        (Self::PreprocessedEvent, Ctx),
+        Result<
+            Self::JobProcessedEvent,
+            Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
+        >,
         BoxedFuture<
-            Result<Self::JobProcessedEvent, Error<<Self as EventListener<T, Ctx>>::ProcessorError>>,
+            Result<
+                Self::JobProcessedEvent,
+                Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
+            >,
         >,
     >;
     type JobProcessedEvent: Send + 'static;
     type PostProcessor: ProcessorFunction<
         Self::JobProcessedEvent,
-        Result<(), Error<<Self as EventListener<T, Ctx>>::ProcessorError>>,
-        BoxedFuture<Result<(), Error<<Self as EventListener<T, Ctx>>::ProcessorError>>>,
+        Result<(), Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>>,
+        BoxedFuture<Result<(), Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>>>,
     >;
+    fn get_context(&self) -> &Ctx;
 
     fn get_preprocessor(&mut self) -> &mut Self::PreProcessor;
     fn get_job_processor(&mut self) -> &mut Self::JobProcessor;
@@ -53,7 +61,7 @@ where
         event: T,
     ) -> Result<
         Option<Self::PreprocessedEvent>,
-        Error<<Self as EventListener<T, Ctx>>::ProcessorError>,
+        Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
     > {
         self.get_preprocessor()(event).await
     }
@@ -61,21 +69,24 @@ where
     async fn process(
         &mut self,
         preprocessed_event: Self::PreprocessedEvent,
-    ) -> Result<Self::JobProcessedEvent, Error<<Self as EventListener<T, Ctx>>::ProcessorError>>
-    {
-        self.get_job_processor()(preprocessed_event).await
+    ) -> Result<
+        Self::JobProcessedEvent,
+        Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>,
+    > {
+        let context = self.get_context().clone();
+        self.get_job_processor()((preprocessed_event, context)).await
     }
 
     async fn post_process(
         &mut self,
         job_output: Self::JobProcessedEvent,
-    ) -> Result<(), Error<<Self as EventListener<T, Ctx>>::ProcessorError>> {
+    ) -> Result<(), Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>> {
         self.get_postprocessor()(job_output).await
     }
 
     async fn event_loop(
         &mut self,
-    ) -> Result<(), Error<<Self as EventListener<T, Ctx>>::ProcessorError>> {
+    ) -> Result<(), Error<<Self as EventListener<T, Ctx, Creator>>::ProcessorError>> {
         // TODO: add exponential backoff logic here
         while let Some(event) = self.next_event().await {
             match self.pre_process(event).await {
@@ -98,7 +109,7 @@ where
     }
 }
 
-pub trait ProcessorFunction<Event, Out, Fut>: Fn(Event) -> Fut
+pub trait ProcessorFunction<Event, Out, Fut>: FnMut(Event) -> Fut
 where
     Fut: Future<Output = Out> + Send + 'static,
     Event: Send + 'static,
@@ -109,7 +120,7 @@ where
 // Blanket implementation of ProcessorFunction for all functions that satisfy the constraints
 impl<F, Event, Out, Fut> ProcessorFunction<Event, Out, Fut> for F
 where
-    F: Fn(Event) -> Fut,
+    F: FnMut(Event) -> Fut,
     Fut: Future<Output = Out> + Send + 'static,
     Event: Send + 'static,
     Out: Send + 'static,
@@ -120,51 +131,72 @@ pub type BoxedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
 
 #[allow(clippy::type_complexity)]
 pub struct EventFlowWrapper<
-    Ctx: Send + 'static,
+    Ctx: Clone + Send + 'static,
+    Creator: Send + 'static,
     Event: Send + 'static,
     PreProcessOut: Send + 'static,
     JobOutput: Send + 'static,
     ProcessorError: core::error::Error + Send + Sync + 'static,
 > {
-    event_listener: Box<dyn EventListener<Event, Ctx, ProcessorError = ProcessorError>>,
+    context: Ctx,
+    event_listener: Box<dyn EventListener<Event, Ctx, Creator, ProcessorError = ProcessorError>>,
     preprocessor: Box<
         dyn Fn(Event) -> BoxedFuture<Result<Option<PreProcessOut>, Error<ProcessorError>>> + Send,
     >,
-    job_processor:
-        Box<dyn Fn(PreProcessOut) -> BoxedFuture<Result<JobOutput, Error<ProcessorError>>> + Send>,
-    postprocessor: Box<dyn Fn(JobOutput) -> BoxedFuture<Result<(), Error<ProcessorError>>> + Send>,
+    job_processor: Box<
+        dyn Fn((PreProcessOut, Ctx)) -> BoxedFuture<Result<JobOutput, Error<ProcessorError>>>
+            + Send,
+    >,
+    postprocessor:
+        Box<dyn FnMut(JobOutput) -> BoxedFuture<Result<(), Error<ProcessorError>>> + Send>,
     _pd: PhantomData<Ctx>,
 }
 
+/*
+pub struct EventFlowFilter<O: Send + 'static> {
+    filte
+}*/
+
 impl<
-        Ctx: Send + 'static,
+        Ctx: Clone + Send + 'static,
+        Creator: Send + 'static,
         Event: Send + 'static,
         PreProcessOut: Send + 'static,
         JobOutput: Send + 'static,
         ProcessorError: core::error::Error + Send + Sync + 'static,
-    > EventFlowWrapper<Ctx, Event, PreProcessOut, JobOutput, ProcessorError>
+    > EventFlowWrapper<Ctx, Creator, Event, PreProcessOut, JobOutput, ProcessorError>
 {
-    pub fn new<T, Pre, PreFut, Job, JobFut, Post, PostFut>(
-        event_listener: T,
-        preprocessor: Pre,
-        job_processor: Job,
-        postprocessor: Post,
+    pub fn new<
+        PreProcessor,
+        PreProcessorFut,
+        JobProcessor,
+        JobProcessorFut,
+        PostProcessor,
+        PostProcessorFut,
+        L,
+    >(
+        context: Ctx,
+        event_listener: L,
+        preprocessor: PreProcessor,
+        job_processor: JobProcessor,
+        mut postprocessor: PostProcessor,
     ) -> Self
     where
-        T: EventListener<Event, Ctx, ProcessorError = ProcessorError>,
-        Pre: Fn(Event) -> PreFut + Send + 'static,
-        PreFut:
+        PreProcessor: Fn(Event) -> PreProcessorFut + Send + 'static,
+        PreProcessorFut:
             Future<Output = Result<Option<PreProcessOut>, Error<ProcessorError>>> + Send + 'static,
-        Job: Fn(PreProcessOut) -> JobFut + Send + 'static,
-        JobFut: Future<Output = Result<JobOutput, Error<ProcessorError>>> + Send + 'static,
-        Post: Fn(JobOutput) -> PostFut + Send + 'static,
-        PostFut: Future<Output = Result<(), Error<ProcessorError>>> + Send + 'static,
+        JobProcessor: Fn((PreProcessOut, Ctx)) -> JobProcessorFut + Send + 'static,
+        JobProcessorFut: Future<Output = Result<JobOutput, Error<ProcessorError>>> + Send + 'static,
+        PostProcessor: FnMut(JobOutput) -> PostProcessorFut + Send + 'static,
+        PostProcessorFut: Future<Output = Result<(), Error<ProcessorError>>> + Send + 'static,
+        L: EventListener<Event, Ctx, Creator, ProcessorError = ProcessorError> + 'static,
     {
         Self {
+            context,
             event_listener: Box::new(event_listener),
-            preprocessor: Box::new(move |event| Box::pin(preprocessor(event))),
-            job_processor: Box::new(move |event| Box::pin(job_processor(event))),
-            postprocessor: Box::new(move |event| Box::pin(postprocessor(event))),
+            preprocessor: Box::new(move |evt| Box::pin(preprocessor(evt))),
+            job_processor: Box::new(move |input| Box::pin(job_processor(input))),
+            postprocessor: Box::new(move |output| Box::pin(postprocessor(output))),
             _pd: PhantomData,
         }
     }
@@ -172,13 +204,14 @@ impl<
 
 #[async_trait]
 impl<
-        Ctx: Send + 'static,
+        Ctx: Clone + Send + 'static,
+        Creator: Send + 'static,
         Event: Send + 'static,
         PreProcessOut: Send + 'static,
         JobOutput: Send + 'static,
         ProcessorError: core::error::Error + Send + Sync + 'static,
-    > EventFlowExecutor<Event, Ctx>
-    for EventFlowWrapper<Ctx, Event, PreProcessOut, JobOutput, ProcessorError>
+    > EventFlowExecutor<Event, Ctx, Creator>
+    for EventFlowWrapper<Ctx, Creator, Event, PreProcessOut, JobOutput, ProcessorError>
 {
     type PreprocessedEvent = PreProcessOut;
     type PreProcessor = Box<
@@ -187,15 +220,19 @@ impl<
     >;
     type JobProcessor = Box<
         dyn Fn(
-                Self::PreprocessedEvent,
+                (Self::PreprocessedEvent, Ctx),
             ) -> BoxedFuture<Result<Self::JobProcessedEvent, Error<ProcessorError>>>
             + Send,
     >;
     type JobProcessedEvent = JobOutput;
     type PostProcessor = Box<
-        dyn Fn(Self::JobProcessedEvent) -> BoxedFuture<Result<(), Error<ProcessorError>>> + Send,
+        dyn FnMut(Self::JobProcessedEvent) -> BoxedFuture<Result<(), Error<ProcessorError>>> + Send,
     >;
 
+    fn get_context(&self) -> &Ctx {
+        &self.context
+    }
+
     fn get_preprocessor(&mut self) -> &mut Self::PreProcessor {
         &mut self.preprocessor
     }
@@ -211,17 +248,18 @@ impl<
 
 #[async_trait]
 impl<
-        Ctx: Send + 'static,
+        Ctx: Clone + Send + 'static,
+        Creator: Send + 'static,
         Event: Send + 'static,
         PreProcessOut: Send + 'static,
         JobOutput: Send + 'static,
         ProcessorError: core::error::Error + Send + Sync + 'static,
-    > EventListener<Event, Ctx>
-    for EventFlowWrapper<Ctx, Event, PreProcessOut, JobOutput, ProcessorError>
+    > EventListener<Event, Ctx, Creator>
+    for EventFlowWrapper<Ctx, Creator, Event, PreProcessOut, JobOutput, ProcessorError>
 {
     type ProcessorError = ProcessorError;
 
-    async fn new(_context: &Ctx) -> Result<Self, Error<Self::ProcessorError>> {
+    async fn new(_context: &Creator) -> Result<Self, Error<Self::ProcessorError>> {
         unreachable!("Not called here")
     }
 
@@ -265,7 +303,9 @@ mod tests {
         Ok(Some((amount, event)))
     }
 
-    async fn job_processor(preprocessed_event: (u64, TestEvent)) -> Result<u64, Error<Infallible>> {
+    async fn job_processor(
+        preprocessed_event: ((u64, TestEvent), Arc<AtomicU64>),
+    ) -> Result<u64, Error<Infallible>> {
         let amount = preprocessed_event.1.fetch_add(1, Ordering::SeqCst) + 1;
         Ok(amount)
     }
@@ -278,6 +318,7 @@ mod tests {
     async fn test_event_flow_executor_builds() {
         let counter = Arc::new(AtomicU64::new(0));
         let _event_listener = EventFlowWrapper::new(
+            counter.clone(),
             DummyEventListener(counter.clone()),
             preprocess,
             job_processor,
@@ -289,6 +330,7 @@ mod tests {
     async fn test_event_flow_executor_executes() {
         let counter = &Arc::new(AtomicU64::new(0));
         let mut event_listener = EventFlowWrapper::new(
+            counter.clone(),
             DummyEventListener(counter.clone()),
             preprocess,
             job_processor,
diff --git a/crates/event-listeners/core/src/lib.rs b/crates/event-listeners/core/src/lib.rs
index dcfbcf212..c5ddd7760 100644
--- a/crates/event-listeners/core/src/lib.rs
+++ b/crates/event-listeners/core/src/lib.rs
@@ -8,41 +8,44 @@ pub mod executor;
 pub mod testing;
 
 use async_trait::async_trait;
+use auto_impl::auto_impl;
 use exponential_backoff::ExponentialBackoff;
 use gadget_std::iter::Take;
 
 /// The [`EventListener`] trait defines the interface for event listeners.
 #[async_trait]
-pub trait EventListener<T: Send + 'static, Ctx: Send + 'static>: Send + 'static {
+pub trait EventListener<Event: Send + 'static, Ctx: Send + 'static, Creator = Ctx>:
+    Send + 'static
+{
     type ProcessorError: core::error::Error + Send + Sync + 'static;
 
-    async fn new(context: &Ctx) -> Result<Self, Error<Self::ProcessorError>>
+    async fn new(context: &Creator) -> Result<Self, Error<Self::ProcessorError>>
     where
         Self: Sized;
 
     /// Obtains the next event to be processed by the event listener.
-    async fn next_event(&mut self) -> Option<T>;
+    async fn next_event(&mut self) -> Option<Event>;
 }
 
 pub fn get_exponential_backoff<const N: usize>() -> Take<ExponentialBackoff> {
     ExponentialBackoff::from_millis(2).factor(1000).take(N)
 }
 
-#[async_trait]
 pub trait CloneableEventHandler: Send {
-    fn clone_box(&self) -> Box<dyn InitializableEventHandler + Send>;
+    fn clone_box(&self) -> Box<dyn InitializableEventHandler + Send + Sync>;
 }
 
 impl<T> CloneableEventHandler for T
 where
-    T: InitializableEventHandler + Clone + 'static,
+    T: InitializableEventHandler + Clone + Sync + 'static,
 {
-    fn clone_box(&self) -> Box<dyn InitializableEventHandler + Send> {
+    fn clone_box(&self) -> Box<dyn InitializableEventHandler + Send + Sync> {
         Box::new(self.clone())
     }
 }
 
 #[async_trait]
+#[auto_impl(Arc, Box)]
 pub trait InitializableEventHandler: Send + CloneableEventHandler {
     async fn init_event_handler(
         &self,
diff --git a/crates/event-listeners/evm/src/lib.rs b/crates/event-listeners/evm/src/lib.rs
index 5ba78f0f2..83c3b5d4e 100644
--- a/crates/event-listeners/evm/src/lib.rs
+++ b/crates/event-listeners/evm/src/lib.rs
@@ -18,26 +18,33 @@ use uuid::Uuid;
 pub type AlloyRootProvider = RootProvider<BoxTransport>;
 pub type AlloyContractInstance = ContractInstance<BoxTransport, AlloyRootProvider, Ethereum>;
 
-pub struct EvmContractEventListener<E: SolEvent + Send + 'static> {
+pub struct EvmContractEventListener<Ctx: Send + 'static, E: SolEvent + Send + 'static> {
     instance: AlloyContractInstance,
     chain_id: u64,
     local_db: LocalDatabase<u64>,
     should_cooldown: bool,
     enqueued_events: VecDeque<(E, alloy_rpc_types::Log)>,
+    #[allow(dead_code)]
+    context: Ctx,
 }
 
 #[async_trait::async_trait]
-impl<E: SolEvent + Send + Sync + 'static>
-    EventListener<(E, alloy_rpc_types::Log), AlloyContractInstance>
-    for EvmContractEventListener<E>
+impl<Creator, Context, E> EventListener<(E, alloy_rpc_types::Log), Context, Creator>
+    for EvmContractEventListener<Context, E>
+where
+    Creator: ContractProvider + ContextProvider<Context> + Send + Sync + 'static,
+    Context: Send + Sync + 'static,
+    E: SolEvent + Send + Sync + 'static,
 {
     type ProcessorError = Error;
 
-    async fn new(context: &AlloyContractInstance) -> Result<Self, CoreError<Self::ProcessorError>>
+    async fn new(creator: &Creator) -> Result<Self, CoreError<Self::ProcessorError>>
     where
         Self: Sized,
     {
-        let provider = context.provider().root();
+        let contract = creator.contract();
+
+        let provider = contract.provider().root();
         // Add more detailed error handling and logging
         let chain_id = provider
             .get_chain_id()
@@ -50,7 +57,8 @@ impl<E: SolEvent + Send + Sync + 'static>
             should_cooldown: false,
             enqueued_events: VecDeque::new(),
             local_db,
-            instance: context.clone(),
+            instance: contract.clone(),
+            context: creator.context(),
         })
     }
 
@@ -80,7 +88,12 @@ impl<E: SolEvent + Send + Sync + 'static>
         let should_cooldown = block >= target_block_number;
         if should_cooldown {
             self.should_cooldown = true;
-            return self.next_event().await;
+            return <EvmContractEventListener<Context, E> as EventListener<
+                (E, alloy_rpc_types::Log),
+                Context,
+                Creator,
+            >>::next_event::<'_, '_>(self)
+            .await;
         }
 
         let dest_block = core::cmp::min(block + step, target_block_number);
@@ -108,12 +121,22 @@ impl<E: SolEvent + Send + Sync + 'static>
 
                 if events.is_empty() {
                     self.should_cooldown = true;
-                    return self.next_event().await;
+                    return <EvmContractEventListener<Context, E> as EventListener<
+                        (E, alloy_rpc_types::Log),
+                        Context,
+                        Creator,
+                    >>::next_event::<'_, '_>(self)
+                    .await;
                 }
 
                 self.enqueued_events = events;
 
-                self.next_event().await
+                <EvmContractEventListener<Context, E> as EventListener<
+                    (E, alloy_rpc_types::Log),
+                    Context,
+                    Creator,
+                >>::next_event::<'_, '_>(self)
+                .await
             }
             Err(e) => {
                 gadget_logging::error!(?e, %self.chain_id, "Error while querying events");
@@ -122,3 +145,14 @@ impl<E: SolEvent + Send + Sync + 'static>
         }
     }
 }
+
+pub trait ContractProvider {
+    fn contract(&self) -> &AlloyContractInstance;
+}
+
+pub trait ContextProvider<Ctx>
+where
+    Ctx: Send + 'static,
+{
+    fn context(&self) -> Ctx;
+}
diff --git a/crates/logging/src/lib.rs b/crates/logging/src/lib.rs
index 5792e546a..184a282cc 100644
--- a/crates/logging/src/lib.rs
+++ b/crates/logging/src/lib.rs
@@ -1,6 +1,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 pub use tracing;
+use tracing::level_filters::LevelFilter;
 pub use tracing_subscriber;
 
 /// A [`trace`] log with the target `"gadget"`
@@ -75,7 +76,11 @@ pub fn setup_log() {
     let _ = tracing_subscriber::fmt::SubscriberBuilder::default()
         .without_time()
         .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NONE)
-        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
+        .with_env_filter(
+            tracing_subscriber::EnvFilter::builder()
+                .with_default_directive(LevelFilter::INFO.into())
+                .from_env_lossy(),
+        )
         .finish()
         .try_init();
 }
diff --git a/crates/macros/blueprint-proc-macro/src/job/args/event_listener.rs b/crates/macros/blueprint-proc-macro/src/job/args/event_listener.rs
index 5254807ea..9aeab371a 100644
--- a/crates/macros/blueprint-proc-macro/src/job/args/event_listener.rs
+++ b/crates/macros/blueprint-proc-macro/src/job/args/event_listener.rs
@@ -1,6 +1,7 @@
 #[cfg(feature = "evm")]
 use crate::job::evm::EvmArgs;
 use crate::job::ParameterType;
+use crate::shared;
 #[cfg(feature = "evm")]
 use proc_macro2::Ident;
 #[cfg(feature = "tangle")]
@@ -11,7 +12,7 @@ use std::str::FromStr;
 use syn::parse::{Parse, ParseBuffer, ParseStream};
 #[cfg(any(not(feature = "evm"), not(feature = "tangle")))]
 use syn::spanned::Spanned;
-use syn::{Index, Token, Type};
+use syn::{GenericArgument, Index, PathArguments, Token, Type};
 
 const EVM_EVENT_LISTENER_TAG: &str = "EvmContractEventListener";
 const TANGLE_EVENT_LISTENER_TAG: &str = "TangleEventListener";
@@ -64,7 +65,8 @@ pub enum ListenerType {
     Custom,
 }
 
-pub(crate) struct SingleListener {
+#[derive(Debug, Clone)]
+pub struct SingleListener {
     pub listener: Type,
     #[cfg(feature = "evm")]
     pub evm_args: Option<EvmArgs>,
@@ -87,7 +89,8 @@ impl SingleListener {
 
 /// `#[job(event_listener(MyCustomListener, MyCustomListener2)]`
 /// Accepts an optional argument that specifies the event listener to use that implements EventListener
-pub(crate) struct EventListenerArgs {
+#[derive(Debug)]
+pub struct EventListenerArgs {
     pub(crate) listeners: Vec<SingleListener>,
 }
 
@@ -172,8 +175,8 @@ impl Parse for EventListenerArgs {
                 }
             } else {
                 return Err(content.error(
-					"Unexpected field parsed. Expected one of `listener`, `event`, `pre_processor`, `post_processor`",
-				));
+                    "Unexpected field parsed. Expected one of `listener`, `event`, `pre_processor`, `post_processor`",
+                ));
             }
         }
 
@@ -253,43 +256,106 @@ impl EventListenerArgs {
         let listener_type = self.get_event_listener().listener_type;
 
         params
-			.iter()
-			.enumerate()
-			.map(|(i, param_ty)| {
-				let ident = format_ident!("param{i}");
-				let index = Index::from(i);
-				match listener_type {
-					#[cfg(feature = "tangle")]
-					ListenerType::Tangle => {
-						let ty_token_stream = proc_macro2::TokenStream::from_str(&param_ty.ty.as_rust_type()).expect("should be valid");
-						let ty_tokens = quote_spanned! {param_ty.span.expect("should always be available")=>
+            .iter()
+            .enumerate()
+            .map(|(i, param_ty)| {
+                let ident = format_ident!("param{i}");
+                let index = Index::from(i);
+                match listener_type {
+                    #[cfg(feature = "tangle")]
+                    ListenerType::Tangle => {
+                        let ty_token_stream = proc_macro2::TokenStream::from_str(&param_ty.ty.as_rust_type()).expect("should be valid");
+                        let ty_tokens = quote_spanned! {param_ty.span.expect("should always be available")=>
                             #ty_token_stream
                         };
-						quote! {
+                        quote! {
                             let __arg = args.next().expect("parameter count checked before");
                             let Ok(#ident) = ::blueprint_sdk::macros::ext::blueprint_serde::from_field::<#ty_tokens>(__arg) else {
                                 return Err(::blueprint_sdk::macros::ext::event_listeners::core::Error::BadArgumentDecoding(format!("Failed to decode the field `{}` to `{}`", stringify!(#ident), stringify!(#ty_tokens))));
                             };
                         }
-					}
+                    }
 
-					#[cfg(feature = "evm")]
-					ListenerType::Evm => {
+                    #[cfg(feature = "evm")]
+                    ListenerType::Evm => {
                         let _ = param_ty;
-						quote! {
+                        quote! {
                             let #ident = inputs.#index;
                         }
-					}
+                    }
 
-					// All other event listeners will return just one type
-					ListenerType::Custom => {
-						quote! {
+                    ListenerType::Custom => {
+                        quote! {
                             let #ident = inputs.#index;
                         }
-					}
-				}
-			})
-			.collect::<Vec<_>>()
+                    }
+                }
+            })
+            .collect::<Vec<_>>()
+    }
+
+    pub fn infer_types(&mut self, sig: &syn::Signature) -> syn::Result<()> {
+        for listener in &mut self.listeners {
+            if let Type::Path(path) = &mut listener.listener {
+                if let Some(last_segment) = path.path.segments.last_mut() {
+                    // Don't try to infer types if they're explicitly specified
+                    let has_explicit_types = match &last_segment.arguments {
+                        PathArguments::AngleBracketed(args) => !args.args.is_empty(),
+                        _ => false,
+                    };
+
+                    if !has_explicit_types {
+                        // Get all parameter types from the function signature
+                        let param_types = shared::param_types(sig)?;
+
+                        // Find the context parameter - it's usually the one with a type containing "Context" or ending with "Ctx"
+                        let mut context_type = None;
+                        let mut event_type = None;
+                        for ty in param_types.values() {
+                            if let Type::Path(type_path) = ty {
+                                let type_name = type_path
+                                    .path
+                                    .segments
+                                    .last()
+                                    .map(|seg| seg.ident.to_string())
+                                    .unwrap_or_default();
+                                if type_name.contains("Context") || type_name.ends_with("Ctx") {
+                                    context_type = Some(ty.clone());
+                                } else {
+                                    // If it's not a context type, it's probably an event type
+                                    event_type = Some(ty.clone());
+                                }
+                            }
+                        }
+
+                        // If we couldn't find a context type, default to unit type
+                        if context_type.is_none() {
+                            context_type = Some(syn::parse_quote! { () });
+                        }
+
+                        // If we couldn't find an event type, default to unit type
+                        if event_type.is_none() {
+                            event_type = Some(syn::parse_quote! { () });
+                        }
+
+                        // Create the generic arguments based on what we found
+                        let generic_args = syn::AngleBracketedGenericArguments {
+                            colon2_token: None,
+                            lt_token: syn::token::Lt::default(),
+                            args: syn::punctuated::Punctuated::from_iter(vec![
+                                GenericArgument::Type(event_type.unwrap()),
+                                GenericArgument::Type(context_type.unwrap()),
+                            ]),
+                            gt_token: syn::token::Gt::default(),
+                        };
+
+                        // Set the generic arguments directly
+                        last_segment.arguments = PathArguments::AngleBracketed(generic_args);
+                    }
+                }
+            }
+        }
+        Ok(())
     }
 
     #[cfg(feature = "tangle")]
diff --git a/crates/macros/blueprint-proc-macro/src/job/args/mod.rs b/crates/macros/blueprint-proc-macro/src/job/args/mod.rs
index 68530b80c..41ed01c2f 100644
--- a/crates/macros/blueprint-proc-macro/src/job/args/mod.rs
+++ b/crates/macros/blueprint-proc-macro/src/job/args/mod.rs
@@ -1,5 +1,6 @@
-mod event_listener;
-pub(crate) use event_listener::{EventListenerArgs, ListenerType};
+pub mod event_listener;
+
+pub use event_listener::{EventListenerArgs, ListenerType};
 
 use super::{Results, ResultsKind};
 use crate::shared::MacroExt;
diff --git a/crates/macros/blueprint-proc-macro/src/job/evm.rs b/crates/macros/blueprint-proc-macro/src/job/evm.rs
index bc57461ba..653458e04 100644
--- a/crates/macros/blueprint-proc-macro/src/job/evm.rs
+++ b/crates/macros/blueprint-proc-macro/src/job/evm.rs
@@ -13,6 +13,7 @@ mod kw {
     syn::custom_keyword!(abi);
 }
 
+#[derive(Debug, Clone)]
 pub(crate) struct EvmArgs {
     pub instance: Option<Ident>,
     pub abi: Option<Type>,
@@ -106,7 +107,7 @@ pub(crate) fn generate_evm_specific_impl(
         contract_instance: Default::default(),
     });
 
-    for (field_name, ty) in non_job_param_map {
+    for (field_name, ty) in &non_job_param_map {
         new_function_signature.push(quote! {
             #field_name: #ty,
         });
@@ -115,9 +116,14 @@ pub(crate) fn generate_evm_specific_impl(
         })
     }
 
+    let (context_field, context_type) = non_job_param_map
+        .get_index(0)
+        .expect("context should exist");
+
     let struct_name_as_literal = struct_name.to_string();
 
     Ok(quote! {
+        #[automatically_derived]
         impl #struct_name {
             /// Create a new
             #[doc = "[`"]
@@ -132,10 +138,8 @@ pub(crate) fn generate_evm_specific_impl(
         }
 
         #[automatically_derived]
-        impl ::blueprint_sdk::macros::ext::std::ops::Deref for #struct_name
-        {
-            type Target = ::blueprint_sdk::macros::ext::event_listeners::evm::AlloyContractInstance;
-            fn deref(&self) -> &Self::Target {
+        impl ::blueprint_sdk::macros::ext::event_listeners::evm::ContractProvider for #struct_name {
+            fn contract(&self) -> &::blueprint_sdk::macros::ext::event_listeners::evm::AlloyContractInstance {
                 self.contract_instance.get_or_init(|| {
                     let abi_location = ::blueprint_sdk::alloy::contract::Interface::new(::blueprint_sdk::alloy::json_abi::JsonAbi::from_json_str(#abi_string).unwrap());
                     ::blueprint_sdk::alloy::contract::ContractInstance::new(self.contract.address().clone(), self.contract.provider().clone(), abi_location )
@@ -143,11 +147,19 @@ pub(crate) fn generate_evm_specific_impl(
             }
         }
 
+        #[automatically_derived]
+        impl ::blueprint_sdk::macros::ext::event_listeners::evm::ContextProvider<#context_type> for #struct_name {
+            fn context(&self) -> #context_type {
+                self.#context_field.clone()
+            }
+        }
+
         #[automatically_derived]
         impl ::blueprint_sdk::macros::ext::event_listeners::core::marker::IsEvm for #struct_name {}
     })
 }
 
+#[allow(clippy::too_many_arguments)]
 pub(crate) fn get_evm_job_processor_wrapper(
     params: &[Ident],
     param_types: &IndexMap<Ident, Type>,
@@ -156,10 +168,13 @@ pub(crate) fn get_evm_job_processor_wrapper(
     fn_name_ident: &Ident,
     asyncness: &TokenStream,
     return_type: &Type,
+    ctx_pos_in_ordered_inputs: usize,
 ) -> syn::Result<TokenStream> {
     let params = declared_params_to_field_types(params, param_types)?;
     let params_tokens = event_listeners.get_param_name_tokenstream(&params);
 
+    ordered_inputs[ctx_pos_in_ordered_inputs] = quote! { context };
+
     let job_processor_call = if params_tokens.is_empty() {
         let second_param = ordered_inputs
             .pop()
@@ -186,7 +201,7 @@ pub(crate) fn get_evm_job_processor_wrapper(
         .collect::<Vec<_>>();
 
     Ok(quote! {
-        move |param0: (#(#inner_param_type)*)| async move {
+        move |(param0, context): ((#(#inner_param_type)*), _)| async move {
             #job_processor_call
             #job_processor_call_return
         }
diff --git a/crates/macros/blueprint-proc-macro/src/job/mod.rs b/crates/macros/blueprint-proc-macro/src/job/mod.rs
index bc87d10e0..5ae704b05 100644
--- a/crates/macros/blueprint-proc-macro/src/job/mod.rs
+++ b/crates/macros/blueprint-proc-macro/src/job/mod.rs
@@ -11,17 +11,16 @@ mod tangle;
 #[cfg(feature = "tangle")]
 use tangle::{generate_tangle_specific_impl, get_tangle_job_processor_wrapper};
 
-use crate::shared::{self, get_return_type_wrapper, pascal_case, type_to_field_type, MacroExt};
-
+use crate::shared::{self, pascal_case, type_to_field_type, MacroExt};
 use gadget_blueprint_proc_macro_core::{FieldType, JobDefinition, JobMetadata};
 use gadget_std::str::FromStr;
-use indexmap::{IndexMap, IndexSet};
+use indexmap::IndexMap;
 use itertools::Itertools;
-use proc_macro2::Span;
-use proc_macro2::TokenStream;
+use proc_macro2::{Span, TokenStream};
 use quote::{format_ident, quote, ToTokens};
 use syn::parse::{Parse, ParseStream};
-use syn::{Ident, ItemFn, LitInt, Token, Type};
+use syn::spanned::Spanned;
+use syn::{GenericArgument, Ident, ItemFn, LitInt, PathArguments, Token, Type};
 
 /// Defines custom keywords for defining Job arguments
 mod kw {
@@ -60,9 +59,25 @@ impl JobDef {
     fn generate(self) -> syn::Result<TokenStream> {
         let param_map = shared::param_types(&self.input.sig)?;
         let params_type = declared_params_to_field_types(&self.args.params, &param_map)?;
-
         let result = get_return_type(&self.input);
-        let result_type = self.args.result_to_field_types(&result)?;
+        let result_type = if let Type::Array(arr) = &result {
+            let elem_type = &*arr.elem;
+            let len = arr
+                .len
+                .to_token_stream()
+                .to_string()
+                .parse::<u64>()
+                .map_err(|_| {
+                    syn::Error::new_spanned(&arr.len, "array length must be a constant")
+                })?;
+            let field_type = type_to_field_type(elem_type)?;
+            vec![ParameterType {
+                ty: FieldType::Array(len, Box::new(field_type.ty)),
+                span: Some(result.span()),
+            }]
+        } else {
+            self.args.result_to_field_types(&result)?
+        };
 
         let job_id = &self.args.id;
         let job_const_block =
@@ -123,9 +138,31 @@ impl JobDef {
 
 /// Job Macro implementation
 pub(crate) fn job_impl(args: JobArgs, input: ItemFn) -> syn::Result<TokenStream> {
-    let def = JobDef { args, input };
+    // Get the return type before generating
+    let return_type = get_return_type(&input);
+
+    // Handle type inference for both results and event listeners
+    let mut args = args;
+
+    // Handle result type inference
+    let mut ret_type: Type = return_type.clone();
+    if let Type::Infer(_) = ret_type {
+        ret_type = syn::parse_quote! { () };
+    }
+    if let ResultsKind::Infered = args.result {
+        args.result = ResultsKind::Types(vec![ret_type.clone()]);
+    } else if let ResultsKind::Types(types) = &args.result {
+        if types.iter().any(|ty| matches!(ty, Type::Infer(_))) {
+            args.result = ResultsKind::Types(vec![ret_type.clone()]);
+        }
+    }
+
+    // Infer event listener generic types based on the function signature
+    args.event_listener.infer_types(&input.sig)?;
 
-    def.generate()
+    let def = JobDef { args, input };
+    let expanded = def.generate()?;
+    Ok(expanded)
 }
 
 pub fn get_return_type(input: &ItemFn) -> Type {
@@ -215,8 +252,9 @@ pub(crate) fn generate_event_workflow_tokenstream(
 ) -> syn::Result<(Vec<TokenStream>, Vec<TokenStream>)> {
     let return_type = get_return_type(input);
 
-    let (mut event_handler_args, _event_handler_arg_types) =
+    let (mut event_handler_args, event_handler_arg_types) =
         get_event_handler_args(param_types, params)?;
+
     let (_, _, struct_name) = generate_fn_name_and_struct(input, suffix);
     #[cfg(feature = "tangle")]
     let (fn_name_string, _job_def_name, job_id_name) = get_job_id_field_name(input);
@@ -235,8 +273,7 @@ pub(crate) fn generate_event_workflow_tokenstream(
             suffix.to_lowercase()
         );
 
-        // convert the listener var, which is just a struct name, to an ident
-        let listener = listener_meta.listener.to_token_stream();
+        let listener_ty = listener_meta.listener.clone();
 
         // Raw events have no pre-processor, therefore their inputs are passed directly into the job function
         // and NOT as job params
@@ -246,13 +283,50 @@ pub(crate) fn generate_event_workflow_tokenstream(
         // We assume the first supplied event handler arg is the context we are injecting into the event listener
         // Then, pass that into the EventFlowWrapper
         let fn_name_ident = &input.sig.ident;
-        let static_ctx_get_override = quote! { CTX.get().unwrap() };
-        #[cfg(feature = "tangle")]
-        let (ctx_pos_in_ordered_inputs, mut ordered_inputs) =
-            get_fn_call_ordered(param_types, params, Some(static_ctx_get_override), is_raw)?;
-        #[cfg(not(feature = "tangle"))]
-        let (_ctx_pos_in_ordered_inputs, mut ordered_inputs) =
-            get_fn_call_ordered(param_types, params, Some(static_ctx_get_override), is_raw)?;
+
+        let (ctx_pos_in_ordered_inputs, mut ordered_inputs) = match listener_meta.listener_type {
+            #[cfg(feature = "evm")]
+            ListenerType::Evm => get_fn_call_ordered(param_types, params, is_raw)?,
+            #[cfg(feature = "tangle")]
+            ListenerType::Tangle => get_fn_call_ordered(param_types, params, is_raw)?,
+            ListenerType::Custom => {
+                let (ctx_pos, mut ordered_inputs) =
+                    get_fn_call_ordered(param_types, params, is_raw)?;
+
+                // For custom event listeners, we need to map event parameters to function parameters
+                if let Type::Path(path) = &listener_ty {
+                    if let Some(last_seg) = path.path.segments.last() {
+                        if let PathArguments::AngleBracketed(args) = &last_seg.arguments {
+                            // Find which generic parameter is the context type
+                            let mut event_param_pos = None;
+                            for (i, arg) in args.args.iter().enumerate() {
+                                if let GenericArgument::Type(Type::Path(p)) = arg {
+                                    if let Some(seg) = p.path.segments.last() {
+                                        if !seg.ident.to_string().ends_with("Context") {
+                                            event_param_pos = Some(i);
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+
+                            // Map event parameter to function parameter
+                            if let Some(event_pos) = event_param_pos {
+                                for (pos, (ident, _)) in param_types.iter().enumerate() {
+                                    if pos != ctx_pos && !params.contains(ident) {
+                                        // This is the event parameter position
+                                        let param_ident = format_ident!("param{}", event_pos);
+                                        ordered_inputs[pos] = quote! { #param_ident };
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                (ctx_pos, ordered_inputs)
+            }
+        };
 
         let asyncness = get_asyncness(input);
 
@@ -262,11 +336,14 @@ pub(crate) fn generate_event_workflow_tokenstream(
             let _ = event_handler_args.remove(0);
         }
 
-        let field_in_self_getter = event_handler_args
+        let (context_field, context_ty) = event_handler_args
             .first()
             .map(|field_in_self| {
                 // If is_raw, assume the actual context is the second param
-                quote! { ctx. #field_in_self .clone() }
+                (
+                    quote! { handler. #field_in_self .clone() },
+                    event_handler_arg_types[0],
+                )
             })
             .ok_or_else(|| {
                 syn::Error::new(
@@ -287,19 +364,65 @@ pub(crate) fn generate_event_workflow_tokenstream(
             listeners.push(#listener_function_name(&self).await.expect("Event listener already initialized"));
         });
 
+        let context_declaration = match listener_meta.listener_type {
+            #[cfg(feature = "tangle")]
+            ListenerType::Tangle => {
+                quote! {
+                    let context: ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleListenerInput<#context_ty> = ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleListenerInput {
+                        client: handler.client.subxt_client().clone(),
+                        signer: handler.signer.clone(),
+                        job_id: #job_id_name,
+                        service_id: handler.service_id,
+                        context: #context_field,
+                    };
+
+                    let client = context.client.clone();
+                    let signer = context.signer.clone();
+                    let service_id = context.service_id;
+                }
+            }
+
+            #[cfg(feature = "evm")]
+            ListenerType::Evm => {
+                quote! {
+                    let context = #context_field;
+                }
+            }
+
+            ListenerType::Custom => {
+                quote! { let context = #context_field; }
+            }
+        };
+
+        let context_to_listener = match listener_meta.listener_type {
+            #[cfg(feature = "tangle")]
+            ListenerType::Tangle => quote! { &context },
+            #[cfg(feature = "evm")]
+            ListenerType::Evm => quote! { &handler },
+            ListenerType::Custom => quote! { &context },
+        };
+
+        let event_listener_creator_param = match listener_meta.listener_type {
+            #[cfg(feature = "tangle")]
+            ListenerType::Tangle => quote! { _ },
+            #[cfg(feature = "evm")]
+            ListenerType::Evm => quote! { #autogen_struct_name },
+            ListenerType::Custom => quote! { _ },
+        };
+
+        let wrapper_type_params = match listener_meta.listener_type {
+            #[cfg(feature = "evm")]
+            ListenerType::Evm => quote! { ::<_, _, #autogen_struct_name> },
+            _ => quote! {},
+        };
+
         let pre_processor_function = if let Some(preprocessor) = &listener_meta.pre_processor {
             quote! { #preprocessor }
         } else {
-            // identity transformation
-            quote! {
-                |evt| async move {
-                    Ok(Some(evt))
-                }
-            }
+            quote! { |evt| async move { Ok(Some(evt)) } }
         };
 
-        // The job_processor is just the job function. Since it may contain multiple params, we need a new function to call it.
-        let job_processor_wrapper = match listener_meta.listener_type {
+        let job_processor_tokens = match listener_meta.listener_type {
             #[cfg(feature = "tangle")]
             ListenerType::Tangle => get_tangle_job_processor_wrapper(
                 params,
@@ -309,9 +432,9 @@ pub(crate) fn generate_event_workflow_tokenstream(
                 fn_name_ident,
                 &asyncness,
                 &return_type,
+                context_ty,
                 ctx_pos_in_ordered_inputs,
             )?,
-
             #[cfg(feature = "evm")]
             ListenerType::Evm => get_evm_job_processor_wrapper(
                 params,
@@ -321,15 +444,39 @@ pub(crate) fn generate_event_workflow_tokenstream(
                 fn_name_ident,
                 &asyncness,
                 &return_type,
+                ctx_pos_in_ordered_inputs,
             )?,
-
             ListenerType::Custom => {
-                let job_processor_call_return = get_return_type_wrapper(&return_type, None);
-
+                let has_job_params = !params.is_empty();
+                let job_processor_call = if has_job_params {
+                    quote! {
+                        let res = #fn_name_ident(#(#ordered_inputs),*) #asyncness;
+                    }
+                } else {
+                    quote! {
+                        let res = #fn_name_ident(#(#ordered_inputs),*) #asyncness;
+                    }
+                };
+                let is_result = return_type.is_result_type();
+                let process_result = if is_result {
+                    quote! {
+                        match res {
+                            Ok(val) => Ok(val),
+                            Err(e) => Err(::blueprint_sdk::macros::ext::event_listeners::core::Error::Other(e.to_string())),
+                        }
+                    }
+                } else {
+                    quote! { Ok(res) }
+                };
                 quote! {
-                    move |param0| async move {
-                        let res = #fn_name_ident (#(#ordered_inputs),*) #asyncness;
-                        #job_processor_call_return
+                    move |params| {
+                        let handler = handler.clone();
+                        let (event, _) = params;
+                        let param0 = event;
+                        async move {
+                            #job_processor_call
+                            #process_result
+                        }
                     }
                 }
             }
@@ -340,94 +487,59 @@ pub(crate) fn generate_event_workflow_tokenstream(
                 #[cfg(feature = "tangle")]
                 ListenerType::Tangle => {
                     quote! {
-                        |(mut client_context, job_result)| async move {
-                            let ctx = CTX.get().unwrap();
-                            let call_id = ::blueprint_sdk::macros::ext::contexts::services::ServicesContext::get_call_id(&mut client_context).expect("Tangle call ID was not injected into context");
-                            let tangle_job_result = ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleResult::<_> {
-                                results: job_result,
-                                service_id: ctx.service_id,
-                                call_id,
-                                client: ctx.client.subxt_client().clone(),
-                                signer: ctx.signer.clone(),
-                            };
-
-                            #postprocessor(tangle_job_result).await
+                        move |(mut context, job_result)| {
+                            let client = client.clone();
+                            let signer = signer.clone();
+                            async move {
+                                let call_id = ::blueprint_sdk::macros::ext::contexts::services::ServicesContext::get_call_id(&mut context).expect("Tangle call ID was not injected into context");
+                                let tangle_job_result = ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleResult::<_> {
+                                    results: job_result,
+                                    service_id,
+                                    call_id,
+                                    client,
+                                    signer,
+                                };
+                                #postprocessor(tangle_job_result).await
+                            }
                         }
                     }
                 }
-
                 #[cfg(feature = "evm")]
                 ListenerType::Evm => {
                     quote! { #postprocessor }
                 }
-
                 ListenerType::Custom => {
                     quote! { #postprocessor }
                 }
             }
         } else {
-            // no-op default
             quote! { |_evt| async move { Ok(()) } }
         };
 
-        let context_declaration = match listener_meta.listener_type {
-            #[cfg(feature = "tangle")]
-            ListenerType::Tangle => {
-                quote! {
-                    let context = ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleListenerInput {
-                        client: ctx.client.subxt_client().clone(),
-                        signer: ctx.signer.clone(),
-                        job_id: #job_id_name,
-                        service_id: ctx.service_id,
-                        context: #field_in_self_getter,
-                    };
-                }
-            }
-
-            #[cfg(feature = "evm")]
-            ListenerType::Evm => {
-                quote! {
-                    let context = ::blueprint_sdk::macros::ext::std::ops::Deref::deref(&ctx).clone();
-                }
-            }
-
-            ListenerType::Custom => {
-                quote! { let context = #field_in_self_getter; }
-            }
-        };
-
         let next_listener = quote! {
-            async fn #listener_function_name (ctx: &#autogen_struct_name) -> Option<::blueprint_sdk::macros::ext::tokio::sync::oneshot::Receiver<Result<(), Box<dyn ::core::error::Error + Send>>>> {
-                static ONCE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
-                if !ONCE.fetch_or(true, std::sync::atomic::Ordering::Relaxed) {
-                    let (tx, rx) = ::blueprint_sdk::macros::ext::tokio::sync::oneshot::channel();
-
-                    static CTX: ::blueprint_sdk::macros::ext::tokio::sync::OnceCell<#autogen_struct_name> = ::blueprint_sdk::macros::ext::tokio::sync::OnceCell::const_new();
-                    #context_declaration
-
-                    if let Err(_err) = CTX.set(ctx.clone()) {
-                        ::blueprint_sdk::macros::ext::logging::error!("Failed to set the context");
-                        return None;
-                    }
-                    let job_processor = #job_processor_wrapper;
-
-                    let listener = <#listener as ::blueprint_sdk::macros::ext::event_listeners::core::EventListener<_, _>>::new(&context).await.expect("Failed to create event listener");
-                    let mut event_workflow = ::blueprint_sdk::macros::ext::event_listeners::core::executor::EventFlowWrapper::new(
-                        listener,
-                        #pre_processor_function,
-                        job_processor,
-                        #post_processor_function,
-                    );
-
-                    let task = async move {
-                        let res = ::blueprint_sdk::macros::ext::event_listeners::core::executor::EventFlowExecutor::event_loop(&mut event_workflow).await.map_err(|e| Box::new(e) as Box<dyn ::core::error::Error + Send>);
-                        let _ = tx.send(res);
-                    };
-                    ::blueprint_sdk::macros::ext::tokio::task::spawn(task);
-                    return Some(rx)
-                }
-
-                None
+            async fn #listener_function_name (handler: &#autogen_struct_name) -> Option<::blueprint_sdk::macros::ext::tokio::sync::oneshot::Receiver<Result<(), Box<dyn ::core::error::Error + Send>>>> {
+                let handler = handler.clone();
+                let (tx, rx) = ::blueprint_sdk::macros::ext::tokio::sync::oneshot::channel();
+                #context_declaration
+
+                let job_processor = #job_processor_tokens;
+                let listener = <#listener_ty as ::blueprint_sdk::macros::ext::event_listeners::core::EventListener<_, _, #event_listener_creator_param>>::new(#context_to_listener)
+                    .await
+                    .expect("Failed to create event listener");
+                let mut event_workflow = ::blueprint_sdk::macros::ext::event_listeners::core::executor::EventFlowWrapper::new(
+                    context,
+                    listener,
+                    #pre_processor_function,
+                    job_processor,
+                    #post_processor_function,
+                );
+
+                let task = async move {
+                    let res = ::blueprint_sdk::macros::ext::event_listeners::core::executor::EventFlowExecutor #wrapper_type_params::event_loop(&mut event_workflow).await.map_err(|e| Box::new(e) as Box<dyn ::core::error::Error + Send>);
+                    let _ = tx.send(res);
+                };
+                ::blueprint_sdk::macros::ext::tokio::task::spawn(task);
+                Some(rx)
             }
         };
 
@@ -443,18 +555,14 @@ pub(crate) fn get_event_handler_args<'a>(
     param_types: &'a IndexMap<Ident, Type>,
     params: &'a [Ident],
 ) -> syn::Result<(Vec<&'a Ident>, Vec<&'a Type>)> {
-    let x = param_types.keys().collect::<IndexSet<_>>();
-    let y = params.iter().collect::<IndexSet<_>>();
-    let event_handler_args = x.difference(&y).copied().collect::<Vec<_>>();
-    let event_handler_types = Itertools::try_collect(event_handler_args.iter().map(|r| {
-        param_types.get(*r).ok_or_else(|| {
-            syn::Error::new_spanned(
-                r,
-                "Could not find the type of the parameter in the function signature",
-            )
-        })
-    }))?;
-
+    let mut event_handler_args = Vec::new();
+    let mut event_handler_types = Vec::new();
+    for (ident, ty) in param_types {
+        if !params.contains(ident) {
+            event_handler_args.push(ident);
+            event_handler_types.push(ty);
+        }
+    }
     Ok((event_handler_args, event_handler_types))
 }
 
@@ -558,7 +666,6 @@ pub fn generate_autogen_struct(
 pub fn get_fn_call_ordered(
     param_types: &IndexMap<Ident, Type>,
     params_from_job_args: &[Ident],
-    replacement_for_self: Option<TokenStream>,
     is_raw: bool,
 ) -> syn::Result<(usize, Vec<TokenStream>)> {
     let (event_handler_args, _) = get_event_handler_args(param_types, params_from_job_args)?;
@@ -577,7 +684,7 @@ pub fn get_fn_call_ordered(
     let mut job_var_idx = 0;
     let mut non_job_var_count = 0;
     let mut ctx_pos = None;
-    let this = replacement_for_self.unwrap_or_else(|| quote! { self });
+    let this = quote! { handler };
     let ret = param_types
         .iter()
         .enumerate()
@@ -717,18 +824,15 @@ pub(crate) struct Results(pub(crate) ResultsKind);
 
 impl Parse for Results {
     fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
-        let _ = input.parse::<kw::result>();
+        let _ = input.parse::<kw::result>()?;
         let content;
         let _ = syn::parenthesized!(content in input);
         let names = content.parse_terminated(Type::parse, Token![,])?;
         if names.is_empty() {
-            return Err(syn::Error::new_spanned(
-                names,
-                "Expected at least one parameter",
-            ));
+            return Ok(Self(ResultsKind::Infered));
         }
         if names.iter().any(|ty| matches!(ty, Type::Infer(_))) {
-            // Infer the types from the retun type
+            // If any type is _, treat it as inference
             return Ok(Self(ResultsKind::Infered));
         }
         let mut items = Vec::new();
diff --git a/crates/macros/blueprint-proc-macro/src/job/tangle.rs b/crates/macros/blueprint-proc-macro/src/job/tangle.rs
index 928941a2e..44ac51fd4 100644
--- a/crates/macros/blueprint-proc-macro/src/job/tangle.rs
+++ b/crates/macros/blueprint-proc-macro/src/job/tangle.rs
@@ -2,7 +2,7 @@ use crate::job::args::EventListenerArgs;
 use crate::job::declared_params_to_field_types;
 use crate::shared::{get_non_job_arguments, get_return_type_wrapper};
 use indexmap::IndexMap;
-use proc_macro2::{Span, TokenStream};
+use proc_macro2::TokenStream;
 use quote::quote;
 use syn::{Ident, Type};
 
@@ -103,6 +103,7 @@ pub(crate) fn get_tangle_job_processor_wrapper(
     fn_name_ident: &Ident,
     asyncness: &TokenStream,
     return_type: &Type,
+    context_ty: &Type,
     ctx_pos_in_ordered_inputs: usize,
 ) -> syn::Result<TokenStream> {
     let params = declared_params_to_field_types(job_params, param_map)?;
@@ -113,27 +114,22 @@ pub(crate) fn get_tangle_job_processor_wrapper(
         const PARAMETER_COUNT: usize = #parameter_count;
     };
 
-    let injected_context_var_name = quote! { injected_context };
-
-    let injected_context = ordered_inputs[ctx_pos_in_ordered_inputs].clone();
+    let injected_context_name = quote! { injected_context };
+    let context_field = quote! { context.context };
     let call_id_injector = quote! {
-        let mut #injected_context_var_name = #injected_context;
+        let mut #injected_context_name = #context_field;
         if let Some(call_id) = tangle_event.call_id {
-            ::blueprint_sdk::macros::ext::contexts::services::ServicesContext::set_call_id(&mut #injected_context_var_name, call_id);
+            ::blueprint_sdk::macros::ext::contexts::services::ServicesContext::set_call_id(&mut #injected_context_name, call_id);
         }
     };
 
-    // Clone to allow passing to the post-processor closure
-    ordered_inputs[ctx_pos_in_ordered_inputs] = quote! { injected_context.clone() };
+    ordered_inputs[ctx_pos_in_ordered_inputs] = quote! { #injected_context_name.clone() };
 
     let job_processor_call = if params_tokens.is_empty() {
-        let second_param = ordered_inputs
-            .pop()
-            .ok_or_else(|| syn::Error::new(Span::call_site(), "Context type required"))?;
         quote! {
             #call_id_injector
             // If no args are specified, assume this job has no parameters and thus takes in the raw event
-            let res = #fn_name_ident (tangle_event, #second_param) #asyncness;
+            let res = #fn_name_ident (tangle_event, #injected_context_name.clone()) #asyncness;
         }
     } else {
         quote! {
@@ -155,10 +151,10 @@ pub(crate) fn get_tangle_job_processor_wrapper(
     };
 
     let job_processor_call_return =
-        get_return_type_wrapper(return_type, Some(injected_context_var_name));
+        get_return_type_wrapper(return_type, Some(injected_context_name));
 
     Ok(quote! {
-        move |tangle_event: ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleEvent<_, _>| async move {
+        move |(tangle_event, context): (::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleEvent<_, _>, ::blueprint_sdk::macros::ext::event_listeners::tangle::events::TangleListenerInput<#context_ty>)| async move {
 
             #job_processor_call
             #job_processor_call_return
diff --git a/crates/macros/blueprint-proc-macro/src/shared.rs b/crates/macros/blueprint-proc-macro/src/shared.rs
index 11e1ad824..625b6841d 100644
--- a/crates/macros/blueprint-proc-macro/src/shared.rs
+++ b/crates/macros/blueprint-proc-macro/src/shared.rs
@@ -2,6 +2,7 @@ use crate::job::ParameterType;
 use crate::job::{IsResultType, ResultsKind};
 use gadget_blueprint_proc_macro_core::FieldType;
 use indexmap::IndexMap;
+use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
 use syn::spanned::Spanned;
 use syn::{Ident, Signature, Type};
@@ -41,24 +42,12 @@ pub fn ident_to_field_type(ident: &Ident) -> syn::Result<FieldType> {
     }
 }
 
-pub fn type_to_field_type(ty: &Type) -> syn::Result<ParameterType> {
-    let field_type = match ty {
-        Type::Array(arr) => {
-            let elem_type = type_to_field_type(&arr.elem)?;
-            // convert arr.len expr to u64
-            let len = match arr.len.to_token_stream().to_string().parse::<u64>() {
-                Ok(l) => l,
-                Err(_) => {
-                    return Err(syn::Error::new_spanned(
-                        &arr.len,
-                        "array length must be a constant",
-                    ))
-                }
-            };
-            Ok(FieldType::Array(len, Box::new(elem_type.ty)))
-        }
+/// Helper function to convert types that don't need special processing like arrays.
+fn base_type_to_field_type(ty: &Type) -> syn::Result<FieldType> {
+    match ty {
         Type::Path(inner) => path_to_field_type(&inner.path),
         Type::Reference(type_reference) => {
+            // For references, remove reference and convert inner type.
             type_to_field_type(&type_reference.elem).map(|ref_ty| ref_ty.ty)
         }
         Type::Tuple(tuple) => {
@@ -71,12 +60,45 @@ pub fn type_to_field_type(ty: &Type) -> syn::Result<ParameterType> {
         }
         _ => Err(syn::Error::new_spanned(
             ty,
-            "unsupported type (type_to_field_type)",
+            "unsupported type (base_type_to_field_type)",
         )),
-    };
+    }
+}
+
+/// Trait to convert a syn::Type to our FieldType, with special handling for arrays and debug logging.
+pub trait IntoFieldType {
+    #[allow(clippy::wrong_self_convention)]
+    fn into_field_type(&self) -> syn::Result<FieldType>;
+}
 
+impl IntoFieldType for Type {
+    fn into_field_type(&self) -> syn::Result<FieldType> {
+        match self {
+            Type::Infer(_) => {
+                let unit: Type = syn::parse_quote! { () };
+                unit.into_field_type()
+            }
+            Type::Array(arr) => {
+                let elem_field_type = type_to_field_type(&arr.elem)?;
+                let len = arr
+                    .len
+                    .to_token_stream()
+                    .to_string()
+                    .parse::<u64>()
+                    .map_err(|_| {
+                        syn::Error::new_spanned(&arr.len, "array length must be a constant")
+                    })?;
+                Ok(FieldType::Array(len, Box::new(elem_field_type.ty)))
+            }
+            _ => base_type_to_field_type(self),
+        }
+    }
+}
+
+/// Updated public API that uses our trait.
+pub fn type_to_field_type(ty: &Type) -> syn::Result<ParameterType> {
     Ok(ParameterType {
-        ty: field_type?,
+        ty: ty.into_field_type()?,
         span: Some(ty.span()),
     })
 }
@@ -185,8 +207,31 @@ pub fn get_non_job_arguments(
 
 pub(crate) trait MacroExt {
     fn result_to_field_types(&self, result: &Type) -> syn::Result<Vec<ParameterType>> {
+        // If the result type is inferred ('_'), default to unit type
+        let effective_result = if let Type::Infer(_) = result {
+            syn::parse_quote! { () }
+        } else {
+            result.clone()
+        };
         match self.return_type() {
-            ResultsKind::Infered => type_to_field_type(result).map(|x| vec![x]),
+            ResultsKind::Infered => {
+                // For Result types, extract the Ok type
+                if effective_result.is_result_type() {
+                    if let Type::Path(type_path) = &effective_result {
+                        if let Some(last_seg) = type_path.path.segments.last() {
+                            if let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments {
+                                if let Some(syn::GenericArgument::Type(inner_type)) =
+                                    args.args.first()
+                                {
+                                    return type_to_field_type(inner_type).map(|x| vec![x]);
+                                }
+                            }
+                        }
+                    }
+                }
+                // For non-Result types, use the type directly
+                type_to_field_type(&effective_result).map(|x| vec![x])
+            }
             ResultsKind::Types(types) => {
                 let xs = types
                     .iter()
@@ -224,8 +269,8 @@ pub(crate) fn param_types(sig: &Signature) -> syn::Result<IndexMap<Ident, Type>>
 
 pub fn get_return_type_wrapper(
     return_type: &Type,
-    injected_context_var_name: Option<proc_macro2::TokenStream>,
-) -> proc_macro2::TokenStream {
+    injected_context_var_name: Option<TokenStream>,
+) -> TokenStream {
     if let Some(context_var_name) = injected_context_var_name {
         if return_type.is_result_type() {
             quote! { res.map_err(|err| ::blueprint_sdk::macros::ext::event_listeners::core::Error::Other(err.to_string())).map(|res| (#context_var_name, res)) }
diff --git a/crates/macros/context-derive/src/lib.rs b/crates/macros/context-derive/src/lib.rs
index 620e7bdbc..75dd22a8d 100644
--- a/crates/macros/context-derive/src/lib.rs
+++ b/crates/macros/context-derive/src/lib.rs
@@ -20,9 +20,6 @@ mod eigenlayer;
 mod evm;
 /// Keystore context extension implementation.
 mod keystore;
-/// P2P context extension implementation.
-#[cfg(feature = "networking")]
-mod p2p;
 /// Tangle context extensions.
 #[cfg(feature = "tangle")]
 mod tangle;
@@ -116,18 +113,3 @@ pub fn derive_eigenlayer_context(input: TokenStream) -> TokenStream {
         Err(err) => TokenStream::from(err.to_compile_error()),
     }
 }
-
-/// Derive macro for generating Context Extensions trait implementation for `P2pContext`.
-#[proc_macro_derive(P2pContext, attributes(config))]
-#[cfg(feature = "networking")]
-pub fn derive_p2p_context(input: TokenStream) -> TokenStream {
-    let input = syn::parse_macro_input!(input as syn::DeriveInput);
-    let result =
-        cfg::find_config_field(&input.ident, &input.data, CONFIG_TAG_NAME, CONFIG_TAG_TYPE)
-            .map(|config_field| p2p::generate_context_impl(input, config_field));
-
-    match result {
-        Ok(expanded) => TokenStream::from(expanded),
-        Err(err) => TokenStream::from(err.to_compile_error()),
-    }
-}
diff --git a/crates/macros/context-derive/src/p2p.rs b/crates/macros/context-derive/src/p2p.rs
deleted file mode 100644
index c2fb1756f..000000000
--- a/crates/macros/context-derive/src/p2p.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use quote::quote;
-use syn::DeriveInput;
-
-use crate::cfg::FieldInfo;
-
-/// Generate the `MPCContext` implementation for the given struct.
-#[allow(clippy::too_many_lines)]
-pub fn generate_context_impl(
-    DeriveInput {
-        ident: name,
-        generics,
-        ..
-    }: DeriveInput,
-    config_field: FieldInfo,
-) -> proc_macro2::TokenStream {
-    let field_access = match config_field {
-        FieldInfo::Named(ident) => quote! { self.#ident },
-        FieldInfo::Unnamed(index) => quote! { self.#index },
-    };
-
-    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-
-    quote! {
-        #[::blueprint_sdk::macros::ext::async_trait::async_trait]
-        impl #impl_generics ::blueprint_sdk::macros::ext::contexts::p2p::P2pContext for #name #ty_generics #where_clause {
-            fn p2p_client(
-                &self,
-                name: ::blueprint_sdk::macros::ext::std::string::String,
-                target_port: u16,
-                my_ecdsa_key: ::blueprint_sdk::macros::ext::contexts::p2p::GossipMsgKeyPair,
-            ) -> ::blueprint_sdk::macros::ext::contexts::p2p::P2PClient {
-                ::blueprint_sdk::macros::ext::contexts::p2p::P2PClient::new(
-                    name,
-                    #field_access.clone(),
-                    target_port,
-                    my_ecdsa_key.clone()
-                )
-            }
-        }
-    }
-}
diff --git a/crates/macros/context-derive/src/tangle/client.rs b/crates/macros/context-derive/src/tangle/client.rs
index a4f24e856..466a23e9d 100644
--- a/crates/macros/context-derive/src/tangle/client.rs
+++ b/crates/macros/context-derive/src/tangle/client.rs
@@ -25,18 +25,7 @@ pub fn generate_context_impl(
         #[::blueprint_sdk::macros::ext::async_trait::async_trait]
         impl #impl_generics ::blueprint_sdk::macros::ext::contexts::tangle::TangleClientContext for #name #ty_generics #where_clause {
             async fn tangle_client(&self) -> std::result::Result<#config_ty, ::blueprint_sdk::macros::ext::clients::Error> {
-                use ::blueprint_sdk::macros::ext::tangle::tangle_subxt::subxt;
-
-                static CLIENT: std::sync::OnceLock<#config_ty> = std::sync::OnceLock::new();
-                match CLIENT.get() {
-                    Some(client) => Ok(client.clone()),
-                    None => {
-                        let client = ::blueprint_sdk::macros::ext::contexts::tangle::TangleClientContext::tangle_client(&#field_access_config).await?;
-                        CLIENT.set(client.clone()).map(|_| client).map_err(|_| {
-                            ::blueprint_sdk::macros::ext::clients::Error::msg("Failed to set client")
-                        })
-                    }
-                }
+                ::blueprint_sdk::macros::ext::contexts::tangle::TangleClientContext::tangle_client(&#field_access_config).await
             }
         }
     }
diff --git a/crates/macros/core/src/lib.rs b/crates/macros/core/src/lib.rs
index 00f0bde94..80027aed3 100644
--- a/crates/macros/core/src/lib.rs
+++ b/crates/macros/core/src/lib.rs
@@ -123,7 +123,7 @@ pub struct ServiceBlueprint<'a> {
     /// The metadata of the service.
     pub metadata: ServiceMetadata<'a>,
     /// The blueprint manager that will be used to manage the blueprints lifecycle.
-    pub manager: BlueprintManager,
+    pub manager: BlueprintServiceManager,
     /// The Revision number of the Master Blueprint Service Manager.
     ///
     /// If not sure what to use, use `MasterBlueprintServiceManagerRevision::default()` which will use
@@ -264,7 +264,7 @@ pub struct ReportMetadata<'a> {
 /// Service Blueprint Manager is a smart contract that will manage the service lifecycle.
 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
 #[non_exhaustive]
-pub enum BlueprintManager {
+pub enum BlueprintServiceManager {
     /// A Smart contract that will manage the service lifecycle.
     Evm(String),
 }
diff --git a/crates/networking/Cargo.toml b/crates/networking/Cargo.toml
index 21a07ebff..46917174c 100644
--- a/crates/networking/Cargo.toml
+++ b/crates/networking/Cargo.toml
@@ -15,9 +15,11 @@ gadget-std = { workspace = true }
 
 # Core dependencies
 auto_impl = { workspace = true }
+blake3 = { workspace = true }
 dashmap = { workspace = true }
 libp2p = { workspace = true }
 tokio = { workspace = true, features = ["macros"] }
+tokio-stream = { workspace = true, features = ["time"] }
 futures = { workspace = true }
 tracing = { workspace = true }
 bincode = { workspace = true }
@@ -29,14 +31,14 @@ hex = { workspace = true }
 itertools = { workspace = true, features = ["use_alloc"] }
 parking_lot = { workspace = true }
 thiserror = { workspace = true }
+anyhow = { workspace = true }
+crossbeam-channel = { workspace = true }
 
 # Crypto dependencies
 gadget-crypto = { workspace = true, features = ["k256", "hashing"] }
+gadget-crypto-core = { workspace = true, features = ["k256"] }
 k256 = { workspace = true }
 
-# Round-based protocol support
-round-based = { workspace = true, optional = true }
-
 [target.'cfg(not(target_family = "wasm"))'.dependencies.libp2p]
 workspace = true
 features = [
@@ -57,11 +59,17 @@ features = [
 	"ping",
 	"dns",
 	"autonat",
+	"upnp",
+	"serde",
 ]
 
 [dev-dependencies]
+gadget-networking = { workspace = true, features = ["sp-core-ecdsa"] }
+gadget-crypto = { workspace = true, features = ["sp-core"] }
+gadget-crypto-core = { workspace = true, features = ["tangle"] }
 tracing-subscriber = { workspace = true }
 lazy_static = { workspace = true }
+fastrand = "2.0"
 
 [features]
 default = ["std"]
@@ -74,11 +82,8 @@ std = [
 	"tokio/full",
 	"serde/std",
 	"serde_json/std",
-	"round-based?/std",
 ]
 
-round-based-compat = ["dep:round-based"]
-
 # Only one of these features should be enabled at a time.
 # If none are enabled, k256 ECDSA will be used by default.
 sp-core-ecdsa = ["gadget-crypto/sp-core"]
diff --git a/crates/networking/README.md b/crates/networking/README.md
new file mode 100644
index 000000000..88136d528
--- /dev/null
+++ b/crates/networking/README.md
@@ -0,0 +1,178 @@
+# Networking Protocol Documentation
+
+This document outlines the key protocols used in the networking layer.
+
+## Handshake Protocol
+
+The handshake protocol ensures mutual authentication between peers before allowing protocol messages.
+
+```mermaid
+sequenceDiagram
+    participant A as Peer A
+    participant B as Peer B
+
+    Note over A,B: Initial TCP/QUIC Connection Established
+
+    Note over A: Create handshake message with:<br/>1. A's peer ID<br/>2. Current timestamp
+    Note over A: Sign(A_id | B_id | timestamp)
+    A->>+B: HandshakeRequest {
+        public_key: A_pub,
+        signature: sign(msg),
+        msg: HandshakeMessage {
+            sender: A_id,
+            timestamp: now
+        }
+    }
+
+    Note over B: 1. Verify timestamp is fresh<br/>2. Verify A_pub derives to A_id<br/>3. Verify signature<br/>4. Store A's public key
+
+    Note over B: Create handshake message with:<br/>1. B's peer ID<br/>2. Current timestamp
+    Note over B: Sign(B_id | A_id | timestamp)
+    B-->>-A: HandshakeResponse {
+        public_key: B_pub,
+        signature: sign(msg),
+        msg: HandshakeMessage {
+            sender: B_id,
+            timestamp: now
+        }
+    }
+
+    Note over A: 1. Verify timestamp is fresh<br/>2. Verify B_pub derives to B_id<br/>3. Verify signature<br/>4. Store B's public key
+
+    Note over A,B: ✓ Handshake Complete
+    Note over A,B: ✓ Protocol Messages Allowed
+```
+
+### Handshake States
+
+```mermaid
+stateDiagram-v2
+    direction LR
+    [*] --> Connected: New Connection
+
+    Connected --> OutboundPending: Send Handshake
+    Connected --> InboundPending: Receive Handshake
+
+    OutboundPending --> Verifying: Valid Response
+    OutboundPending --> Failed: Invalid/Timeout
+
+    InboundPending --> Verifying: Valid Request
+    InboundPending --> Failed: Invalid/Timeout
+
+    Verifying --> Verified: All Checks Pass
+    Verifying --> Failed: Checks Fail
+
+    Verified --> [*]: Connection Closed
+    Failed --> [*]: Connection Closed
+
+    note right of Connected
+        Initial TCP/QUIC connection established
+    end note
+
+    note right of Verifying
+        Checks:
+        1. Timestamp fresh
+        2. PubKey matches PeerId
+        3. Signature valid
+        4. Key whitelisted
+    end note
+
+    note right of Verified
+        Both peers authenticated
+        Protocol messages allowed
+    end note
+```
+
+## Protocol Message Exchange
+
+After handshake completion, peers can exchange protocol messages through direct P2P or broadcast channels.
+
+```mermaid
+sequenceDiagram
+    participant A as Peer A (Verified)
+    participant B as Peer B (Verified)
+
+    Note over A,B: ✓ Handshake Completed
+
+    A->>+B: InstanceMessageRequest {
+        protocol: String,
+        payload: Vec<u8>,
+        metadata: Option<Vec<u8>>
+    }
+
+    alt Success Case
+        B-->>-A: InstanceMessageResponse::Success {
+            data: Option<Vec<u8>>
+        }
+    else Protocol Response
+        B-->>-A: InstanceMessageResponse::Protocol {
+            data: Vec<u8>
+        }
+    else Error Case
+        B-->>-A: InstanceMessageResponse::Error {
+            code: u16,
+            message: String
+        }
+    end
+```
+
+### Message Flow States
+
+```mermaid
+stateDiagram-v2
+    direction LR
+    [*] --> Handshaked: Peers Verified
+
+    Handshaked --> RequestPending: Send Request
+    RequestPending --> Processing: Request Received
+
+    Processing --> ResponseSent: Success/Protocol
+    Processing --> ErrorSent: Error
+
+    ResponseSent --> Handshaked: Complete
+    ErrorSent --> Handshaked: Complete
+
+    Handshaked --> [*]: Connection Closed
+
+    note right of Handshaked
+        Peers authenticated
+        Ready for messages
+    end note
+
+    note right of Processing
+        Validating request
+        Processing payload
+    end note
+```
+
+## Protocol Details
+
+### Handshake Protocol
+
+- Initiated on first connection
+- Mutual authentication using public key cryptography
+- Signatures verify peer identity and ownership
+- Timestamps prevent replay attacks
+- Timeouts after 30 seconds
+- Handles concurrent handshakes gracefully
+
+### Protocol Message Types
+
+- Direct P2P messages:
+  - Targeted to specific peer
+  - Requires peer verification
+  - Guaranteed delivery attempt
+- Broadcast messages:
+  - Sent to all peers
+  - Uses gossipsub protocol
+  - Best-effort delivery
+
+### Security Features
+
+- Peer verification before message acceptance
+- Public key to peer ID verification
+- Timestamp-based replay protection
+- Signature verification for handshakes
+- Banned peer tracking
+- Connection limits
+- Protocol version validation
diff --git a/crates/networking/extensions/round-based/Cargo.toml b/crates/networking/extensions/round-based/Cargo.toml
new file mode 100644
index 000000000..006905b4d
--- /dev/null
+++ b/crates/networking/extensions/round-based/Cargo.toml
@@ -0,0 +1,58 @@
+[package]
+name = "gadget-networking-round-based-extension"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[dependencies]
+gadget-networking = { workspace = true }
+round-based = { workspace = true }
+tokio = { workspace = true }
+futures = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+tracing = { workspace = true }
+dashmap = { workspace = true }
+crossbeam = { workspace = true }
+crossbeam-channel = { workspace = true }
+thiserror = { workspace = true }
+
+[dev-dependencies]
+round-based = { workspace = true, features = ["derive", "sim", "sim-async"] }
+libp2p = { workspace = true, features = [
+  "tokio",
+  "gossipsub",
+  "mdns",
+  "noise",
+  "macros",
+  "yamux",
+  "tcp",
+  "quic",
+  "request-response",
+  "cbor",
+  "identify",
+  "kad",
+  "dcutr",
+  "relay",
+  "ping",
+  "dns",
+  "autonat",
+  "upnp",
+] }
+gadget-crypto = { workspace = true, features = ["sp-core"] }
+gadget-crypto-core = { workspace = true, features = ["tangle"] }
+
+tokio = { workspace = true, features = ["full"] }
+tracing-subscriber = { workspace = true }
+sha2 = { workspace = true }
+rand_dev = "0.1"
+rand = { workspace = true }
+hex = { workspace = true }
+# We don't use it directly, but we need to enable `serde` feature
+generic-array = { version = "0.14", features = ["serde"] }
+
+[lints]
+workspace = true
diff --git a/crates/networking/extensions/round-based/src/lib.rs b/crates/networking/extensions/round-based/src/lib.rs
new file mode 100644
index 000000000..f7044a7a6
--- /dev/null
+++ b/crates/networking/extensions/round-based/src/lib.rs
@@ -0,0 +1,246 @@
+use dashmap::DashMap;
+use futures::{Sink, Stream};
+use gadget_networking::{
+    key_types::InstanceMsgPublicKey,
+    service_handle::NetworkServiceHandle,
+    types::{ParticipantInfo, ProtocolMessage},
+};
+use round_based::{Delivery, Incoming, MessageDestination, MessageType, Outgoing, PartyIndex};
+use serde::{de::DeserializeOwned, Serialize};
+use std::{
+    collections::HashMap,
+    pin::Pin,
+    sync::{
+        atomic::{AtomicU64, Ordering},
+        Arc,
+    },
+    task::{Context, Poll},
+};
+
+/// Wrapper to adapt [`NetworkServiceHandle`] to round-based protocols
+pub struct RoundBasedNetworkAdapter<M> {
+    /// The underlying network handle
+    handle: NetworkServiceHandle,
+    /// Current party's index
+    party_index: PartyIndex,
+    /// Mapping of party indices to their public keys
+    parties: Arc<DashMap<PartyIndex, InstanceMsgPublicKey>>,
+    /// Counter for message IDs
+    next_msg_id: Arc<AtomicU64>,
+    /// Protocol identifier
+    protocol_id: String,
+    _phantom: std::marker::PhantomData<M>,
+}
+
+impl<M> RoundBasedNetworkAdapter<M>
+where
+    M: Clone + Send + Sync + Unpin + 'static,
+    M: Serialize + DeserializeOwned,
+    M: round_based::ProtocolMessage,
+{
+    pub fn new(
+        handle: NetworkServiceHandle,
+        party_index: PartyIndex,
+        parties: HashMap<PartyIndex, InstanceMsgPublicKey>,
+        protocol_id: impl Into<String>,
+    ) -> Self {
+        Self {
+            handle,
+            party_index,
+            parties: Arc::new(DashMap::from_iter(parties)),
+            next_msg_id: Arc::new(AtomicU64::new(0)),
+            protocol_id: protocol_id.into(),
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<M> Delivery<M> for RoundBasedNetworkAdapter<M>
+where
+    M: Clone + Send + Sync + Unpin + 'static,
+    M: Serialize + DeserializeOwned,
+    M: round_based::ProtocolMessage,
+{
+    type Send = RoundBasedSender<M>;
+    type Receive = RoundBasedReceiver<M>;
+    type SendError = NetworkError;
+    type ReceiveError = NetworkError;
+
+    fn split(self) -> (Self::Receive, Self::Send) {
+        let RoundBasedNetworkAdapter {
+            handle,
+            party_index,
+            parties,
+            next_msg_id,
+            protocol_id,
+            ..
+        } = self;
+
+        let sender = RoundBasedSender {
+            handle: handle.clone(),
+            party_index,
+            parties: parties.clone(),
+            next_msg_id: next_msg_id.clone(),
+            protocol_id: protocol_id.clone(),
+            _phantom: std::marker::PhantomData,
+        };
+
+        let receiver = RoundBasedReceiver::new(handle, party_index);
+
+        (receiver, sender)
+    }
+}
+
+pub struct RoundBasedSender<M> {
+    handle: NetworkServiceHandle,
+    party_index: PartyIndex,
+    parties: Arc<DashMap<PartyIndex, InstanceMsgPublicKey>>,
+    next_msg_id: Arc<AtomicU64>,
+    protocol_id: String,
+    _phantom: std::marker::PhantomData<M>,
+}
+
+impl<M> Sink<Outgoing<M>> for RoundBasedSender<M>
+where
+    M: Serialize + round_based::ProtocolMessage + Clone + Unpin,
+{
+    type Error = NetworkError;
+
+    fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn start_send(self: Pin<&mut Self>, outgoing: Outgoing<M>) -> Result<(), Self::Error> {
+        let this = self.get_mut();
+        let msg_id = this.next_msg_id.fetch_add(1, Ordering::Relaxed);
+        let round = outgoing.msg.round();
+
+        tracing::trace!(
+            i = %this.party_index,
+            recipient = ?outgoing.recipient,
+            %round,
+            %msg_id,
+            protocol_id = %this.protocol_id,
+            "Sending message",
+        );
+
+        let (recipient, recipient_key) = match outgoing.recipient {
+            MessageDestination::AllParties => (None, None),
+            MessageDestination::OneParty(p) => {
+                let key = this.parties.get(&p).map(|k| *k);
+                (Some(p), key)
+            }
+        };
+
+        let protocol_message = ProtocolMessage {
+            protocol: format!("{}/{}", this.protocol_id, round),
+            routing: gadget_networking::types::MessageRouting {
+                message_id: msg_id,
+                round_id: round,
+                sender: ParticipantInfo {
+                    id: gadget_networking::types::ParticipantId(this.party_index),
+                    public_key: this.parties.get(&this.party_index).map(|k| *k),
+                },
+                recipient: recipient.map(|p| ParticipantInfo {
+                    id: gadget_networking::types::ParticipantId(p),
+                    public_key: recipient_key,
+                }),
+            },
+            payload: serde_json::to_vec(&outgoing.msg).map_err(NetworkError::Serialization)?,
+        };
+
+        tracing::trace!(
+            %round,
+            %msg_id,
+            protocol_id = %this.protocol_id,
+            "Sending message to network",
+        );
+
+        this.handle
+            .send(protocol_message.routing, protocol_message.payload)
+            .map_err(NetworkError::Send)
+    }
+
+    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+}
+
+pub struct RoundBasedReceiver<M> {
+    handle: NetworkServiceHandle,
+    party_index: PartyIndex,
+    _phantom: std::marker::PhantomData<M>,
+}
+
+impl<M> RoundBasedReceiver<M> {
+    fn new(handle: NetworkServiceHandle, party_index: PartyIndex) -> Self {
+        Self {
+            handle,
+            party_index,
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<M> Stream for RoundBasedReceiver<M>
+where
+    M: DeserializeOwned + round_based::ProtocolMessage + Unpin,
+{
+    type Item = Result<Incoming<M>, NetworkError>;
+
+    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+        // Get a mutable reference to self
+        let this = self.get_mut();
+
+        let next_protocol_message = this.handle.next_protocol_message();
+        match next_protocol_message {
+            Some(protocol_message) => {
+                let msg_type = if protocol_message.routing.recipient.is_some() {
+                    MessageType::P2P
+                } else {
+                    MessageType::Broadcast
+                };
+
+                let sender = protocol_message.routing.sender.id.0;
+                let id = protocol_message.routing.message_id;
+
+                tracing::trace!(
+                    i = %this.party_index,
+                    sender = ?sender,
+                    %id,
+                    protocol_id = %protocol_message.protocol,
+                    ?msg_type,
+                    size = %protocol_message.payload.len(),
+                    "Received message",
+                );
+                match serde_json::from_slice(&protocol_message.payload) {
+                    Ok(msg) => Poll::Ready(Some(Ok(Incoming {
+                        msg,
+                        sender,
+                        id,
+                        msg_type,
+                    }))),
+                    Err(e) => Poll::Ready(Some(Err(NetworkError::Serialization(e)))),
+                }
+            }
+            None => {
+                //tracing::trace!(i = %this.party_index, "No message received; the waker will wake us up when there is a new message");
+                // In this case, tell the waker to wake us up when there is a new message
+                cx.waker().wake_by_ref();
+                Poll::Pending
+            }
+        }
+    }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum NetworkError {
+    #[error("Failed to serialize message: {0}")]
+    Serialization(#[from] serde_json::Error),
+    #[error("Network error: {0}")]
+    Send(String),
+}
diff --git a/crates/networking/src/behaviours.rs b/crates/networking/src/behaviours.rs
new file mode 100644
index 000000000..3d56be0d3
--- /dev/null
+++ b/crates/networking/src/behaviours.rs
@@ -0,0 +1,133 @@
+use crate::error::Result as NetworkingResult;
+use crate::key_types::InstanceMsgKeyPair;
+use crate::types::ProtocolMessage;
+use crate::{
+    blueprint_protocol::{BlueprintProtocolBehaviour, BlueprintProtocolEvent},
+    discovery::{
+        behaviour::{DiscoveryBehaviour, DiscoveryEvent},
+        config::DiscoveryConfig,
+        PeerInfo, PeerManager,
+    },
+};
+use crossbeam_channel::Sender;
+use libp2p::{
+    connection_limits::{self, ConnectionLimits},
+    identity::Keypair,
+    kad::QueryId,
+    ping,
+    swarm::NetworkBehaviour,
+    Multiaddr, PeerId,
+};
+use std::{
+    collections::{HashMap, HashSet},
+    sync::Arc,
+    time::Duration,
+};
+use tracing::{debug, info};
+
+/// Events that can be emitted by the `GadgetBehavior`
+#[derive(Debug)]
+pub enum GadgetEvent {
+    /// Discovery-related events
+    Discovery(DiscoveryEvent),
+    /// Ping events for connection liveness
+    Ping(ping::Event),
+    /// Blueprint protocol events
+    Blueprint(BlueprintProtocolEvent),
+}
+
+#[derive(NetworkBehaviour)]
+pub struct GadgetBehaviour {
+    /// Connection limits to prevent `DoS`
+    connection_limits: connection_limits::Behaviour,
+    /// Discovery mechanisms (Kademlia, mDNS, etc)
+    pub(super) discovery: DiscoveryBehaviour,
+    /// Direct P2P messaging and gossip
+    pub(super) blueprint_protocol: BlueprintProtocolBehaviour,
+    /// Connection liveness checks
+    ping: ping::Behaviour,
+}
+
+impl GadgetBehaviour {
+    /// Create a new `GadgetBehaviour`
+    ///
+    /// # Errors
+    ///
+    /// See [`DiscoveryConfig::build()`]
+    pub fn new(
+        network_name: &str,
+        blueprint_protocol_name: &str,
+        local_key: &Keypair,
+        instance_key_pair: &InstanceMsgKeyPair,
+        target_peer_count: u32,
+        peer_manager: Arc<PeerManager>,
+        protocol_message_sender: Sender<ProtocolMessage>,
+    ) -> NetworkingResult<Self> {
+        let connection_limits = connection_limits::Behaviour::new(
+            ConnectionLimits::default()
+                .with_max_pending_incoming(Some(target_peer_count))
+                .with_max_pending_outgoing(Some(target_peer_count))
+                .with_max_established_incoming(Some(target_peer_count))
+                .with_max_established_outgoing(Some(target_peer_count))
+                .with_max_established_per_peer(Some(target_peer_count)),
+        );
+
+        let ping = ping::Behaviour::new(ping::Config::new().with_interval(Duration::from_secs(30)));
+
+        info!(
+            "Setting up discovery behavior with network name: {}",
+            network_name
+        );
+        let discovery = DiscoveryConfig::new(local_key.public(), network_name)
+            .mdns(true)
+            .kademlia(true)
+            .target_peer_count(target_peer_count)
+            .build()?;
+
+        info!(
+            "Setting up blueprint protocol with name: {}",
+            blueprint_protocol_name
+        );
+        let blueprint_protocol = BlueprintProtocolBehaviour::new(
+            local_key,
+            instance_key_pair,
+            peer_manager,
+            blueprint_protocol_name,
+            protocol_message_sender,
+        );
+
+        debug!("Created GadgetBehaviour with all components initialized");
+        Ok(Self {
+            connection_limits,
+            discovery,
+            blueprint_protocol,
+            ping,
+        })
+    }
+
+    /// Bootstrap Kademlia network
+    ///
+    /// # Errors
+    ///
+    /// See [`DiscoveryBehaviour::bootstrap()`]
+    pub fn bootstrap(&mut self) -> NetworkingResult<QueryId> {
+        self.discovery.bootstrap()
+    }
+
+    /// Returns a set of peer ids
+    #[must_use]
+    pub fn peers(&self) -> &HashSet<PeerId> {
+        self.discovery.get_peers()
+    }
+
+    /// Returns a map of peer ids and their multi-addresses
+    #[must_use]
+    pub fn peer_addresses(&self) -> HashMap<PeerId, HashSet<Multiaddr>> {
+        self.discovery.get_peer_addresses()
+    }
+
+    #[must_use]
+    pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> {
+        self.discovery.get_peer_info(peer_id)
+    }
+}
diff --git a/crates/networking/src/blueprint_protocol/behaviour.rs b/crates/networking/src/blueprint_protocol/behaviour.rs
new file mode 100644
index 000000000..7ea0e9fd8
--- /dev/null
+++ b/crates/networking/src/blueprint_protocol/behaviour.rs
@@ -0,0 +1,524 @@
+use super::{InstanceMessageRequest, InstanceMessageResponse};
+use crate::blueprint_protocol::HandshakeMessage;
+use crate::discovery::PeerManager;
+use crate::{
+    types::ProtocolMessage, Curve, InstanceMsgKeyPair, InstanceMsgPublicKey,
+    InstanceSignedMsgSignature,
+};
+use bincode;
+use crossbeam_channel::Sender;
+use dashmap::DashMap;
+use gadget_crypto::KeyType;
+use libp2p::{
+    core::transport::PortUse,
+    gossipsub::{self, IdentTopic, MessageId, Sha256Topic},
+    identity::Keypair,
+    request_response::{self, OutboundRequestId, ResponseChannel},
+    swarm::{
+        ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent,
+        THandlerOutEvent, ToSwarm,
+    },
+    Multiaddr, PeerId, StreamProtocol,
+};
+use std::{
+    sync::Arc,
+    task::Poll,
+    time::{Duration, Instant},
+};
+use tracing::{debug, error, info, trace, warn};
+
+#[derive(NetworkBehaviour)]
+pub struct DerivedBlueprintProtocolBehaviour {
+    /// Request/response protocol for p2p messaging
+    request_response:
+        request_response::cbor::Behaviour<InstanceMessageRequest, InstanceMessageResponse>,
+    /// Gossipsub for broadcast messaging
+    gossipsub: gossipsub::Behaviour,
+}
+
+/// Events emitted by the `BlueprintProtocolBehaviour`
+#[derive(Debug)]
+pub enum BlueprintProtocolEvent {
+    /// Request received from a peer
+    Request {
+        peer: PeerId,
+        request: InstanceMessageRequest,
+        channel: ResponseChannel<InstanceMessageResponse>,
+    },
+    /// Response received from a peer
+    Response {
+        peer: PeerId,
+        request_id: OutboundRequestId,
+        response: InstanceMessageResponse,
+    },
+    /// Gossip message received
+    GossipMessage {
+        source: PeerId,
+        message: Vec<u8>,
+        topic: IdentTopic,
+    },
+}
+
+/// Behaviour that handles the blueprint protocol request/response and gossip
+pub struct BlueprintProtocolBehaviour {
+    /// Request/response protocol for direct messaging
+    blueprint_protocol: DerivedBlueprintProtocolBehaviour,
+    /// Name of the blueprint protocol
+    pub(crate) blueprint_protocol_name: String,
+    /// Peer manager for tracking peer states
+    pub(crate) peer_manager: Arc<PeerManager>,
+    /// Libp2p peer ID
+    pub(crate) local_peer_id: PeerId,
+    /// Instance key pair for handshakes and blueprint protocol
+    pub(crate) instance_key_pair: InstanceMsgKeyPair,
+    /// Peers with pending inbound handshakes
+    pub(crate) inbound_handshakes: DashMap<PeerId, Instant>,
+    /// Peers with pending outbound handshakes
+    pub(crate) outbound_handshakes: DashMap<PeerId, Instant>,
+    /// Active response channels
+    #[expect(dead_code)] // TODO
+    pub(crate) response_channels:
+        DashMap<OutboundRequestId, ResponseChannel<InstanceMessageResponse>>,
+    /// Protocol message sender
+    pub(crate) protocol_message_sender: Sender<ProtocolMessage>,
+}
+
+impl BlueprintProtocolBehaviour {
+    /// Create a new blueprint protocol behaviour
+    #[must_use]
+    #[allow(clippy::missing_panics_doc)] // Known good gossipsub config
+    pub fn new(
+        local_key: &Keypair,
+        instance_key_pair: &InstanceMsgKeyPair,
+        peer_manager: Arc<PeerManager>,
+        blueprint_protocol_name: &str,
+        protocol_message_sender: Sender<ProtocolMessage>,
+    ) -> Self {
+        let blueprint_protocol_name = blueprint_protocol_name.to_string();
+        let protocols = vec![(
+            StreamProtocol::try_from_owned(blueprint_protocol_name.to_string())
+                .unwrap_or_else(|_| StreamProtocol::new("/blueprint_protocol/1.0.0")),
+            request_response::ProtocolSupport::Full,
+        )];
+
+        // Initialize gossipsub with message signing
+        let gossipsub_config = gossipsub::ConfigBuilder::default()
+            .heartbeat_interval(Duration::from_secs(1))
+            .validation_mode(gossipsub::ValidationMode::Strict)
+            .mesh_n_low(2)
+            .mesh_n(4)
+            .mesh_n_high(8)
+            .gossip_lazy(3)
+            .history_length(10)
+            .history_gossip(3)
+            .flood_publish(true)
+            .build()
+            .expect("Valid gossipsub config");
+
+        let gossipsub = gossipsub::Behaviour::new(
+            gossipsub::MessageAuthenticity::Signed(local_key.clone()),
+            gossipsub_config,
+        )
+        .expect("Valid gossipsub behaviour");
+
+        let config = request_response::Config::default()
+            .with_request_timeout(Duration::from_secs(30))
+            .with_max_concurrent_streams(50);
+
+        let blueprint_protocol = DerivedBlueprintProtocolBehaviour {
+            request_response: request_response::cbor::Behaviour::new(protocols, config),
+            gossipsub,
+        };
+
+        let local_peer_id = local_key.public().to_peer_id();
+
+        Self {
+            blueprint_protocol,
+            blueprint_protocol_name,
+            peer_manager,
+            local_peer_id,
+            instance_key_pair: instance_key_pair.clone(),
+            inbound_handshakes: DashMap::new(),
+            outbound_handshakes: DashMap::new(),
+            response_channels: DashMap::new(),
+            protocol_message_sender,
+        }
+    }
+
+    /// Sign a handshake message for a peer
+    #[allow(clippy::unused_self)]
+    pub(crate) fn sign_handshake(
+        &self,
+        key_pair: &mut InstanceMsgKeyPair,
+        peer: &PeerId,
+        handshake_msg: &HandshakeMessage,
+    ) -> Option<InstanceSignedMsgSignature> {
+        let msg = handshake_msg.to_bytes(peer);
+        match <Curve as KeyType>::sign_with_secret(key_pair, &msg) {
+            Ok(signature) => {
+                let public_key = key_pair.public();
+                let hex_msg = hex::encode(msg);
+
+                debug!(%peer, ?hex_msg, %public_key, %signature, "signing handshake");
+                Some(signature)
+            }
+            Err(e) => {
+                warn!("Failed to sign handshake message: {e}");
+                None
+            }
+        }
+    }
+
+    /// Send a request to a peer
+    pub fn send_request(
+        &mut self,
+        peer: &PeerId,
+        request: InstanceMessageRequest,
+    ) -> OutboundRequestId {
+        debug!(%peer, ?request, "sending request");
+        self.blueprint_protocol
+            .request_response
+            .send_request(peer, request)
+    }
+
+    /// Send a response through a response channel
+    ///
+    /// # Errors
+    ///
+    /// See [`libp2p::request_response::Behaviour::send_response`]
+    pub fn send_response(
+        &mut self,
+        channel: ResponseChannel<InstanceMessageResponse>,
+        response: InstanceMessageResponse,
+    ) -> Result<(), InstanceMessageResponse> {
+        debug!(?response, "sending response");
+        self.blueprint_protocol
+            .request_response
+            .send_response(channel, response)
+    }
+
+    /// Subscribe to a gossip topic
+    ///
+    /// # Errors
+    ///
+    /// See [`libp2p::gossipsub::SubscriptionError`]
+    pub fn subscribe(&mut self, topic: &str) -> Result<bool, gossipsub::SubscriptionError> {
+        let topic = Sha256Topic::new(topic);
+        self.blueprint_protocol.gossipsub.subscribe(&topic)
+    }
+
+    /// Publish a message to a gossip topic
+    ///
+    /// # Errors
+    ///
+    /// See [`libp2p::gossipsub::PublishError`]
+    pub fn publish(
+        &mut self,
+        topic: &str,
+        data: impl Into<Vec<u8>>,
+    ) -> Result<MessageId, gossipsub::PublishError> {
+        let topic = Sha256Topic::new(topic);
+        self.blueprint_protocol.gossipsub.publish(topic, data)
+    }
+
+    /// Verify and handle a handshake with a peer
+    ///
+    /// # Errors
+    ///
+    /// The handshake expired or has an invalid signature
+    pub fn verify_handshake(
+        &self,
+        msg: &HandshakeMessage,
+        public_key: &InstanceMsgPublicKey,
+        signature: &InstanceSignedMsgSignature,
+    ) -> Result<(), InstanceMessageResponse> {
+        if msg.is_expired(HandshakeMessage::MAX_AGE) {
+            error!(%msg.sender, "Handshake message expired");
+            return Err(InstanceMessageResponse::Error {
+                code: 400,
+                message: "Handshake message expired".to_string(),
+            });
+        }
+
+        let msg_bytes = msg.to_bytes(&self.local_peer_id);
+        let hex_msg = hex::encode(msg_bytes.clone());
+
+        debug!(%hex_msg, %public_key, %signature, "verifying handshake");
+
+        let valid = <Curve as KeyType>::verify(public_key, &msg_bytes, signature);
+        if !valid {
+            warn!(%msg.sender, "Invalid handshake signature for peer");
+            return Err(InstanceMessageResponse::Error {
+                code: 400,
+                message: "Invalid handshake signature".to_string(),
+            });
+        }
+
+        trace!(%msg.sender, "Handshake signature verified successfully");
+        Ok(())
+    }
+
+    /// Handle a handshake message, verifying the peer on success
+    ///
+    /// # Errors
+    ///
+    /// See [`Self::verify_handshake()`]
+    pub fn handle_handshake(
+        &self,
+        msg: &HandshakeMessage,
+        public_key: &InstanceMsgPublicKey,
+        signature: &InstanceSignedMsgSignature,
+    ) -> Result<(), InstanceMessageResponse> {
+        self.verify_handshake(msg, public_key, signature)?;
+        self.peer_manager
+            .add_peer_id_to_public_key(&msg.sender, public_key);
+
+        Ok(())
+    }
+
+    /// Handle a failed handshake with a peer
+    pub fn handle_handshake_failure(&self, peer: &PeerId, reason: &str) {
+        // Update peer info and potentially ban peer
+        if let Some(mut peer_info) = self.peer_manager.get_peer_info(peer) {
+            peer_info.failures += 1;
+            self.peer_manager.update_peer(*peer, peer_info.clone());
+
+            // Ban peer if too many failures
+            if peer_info.failures >= 3 {
+                self.peer_manager
+                    .ban_peer(*peer, reason, Some(Duration::from_secs(300)));
+            }
+        }
+    }
+
+    pub fn handle_gossipsub_event(&mut self, event: gossipsub::Event) {
+        match event {
+            gossipsub::Event::Message {
+                propagation_source,
+                message_id: _,
+                message,
+            } => {
+                // Only accept gossip from verified peers
+                if !self.peer_manager.is_peer_verified(&propagation_source) {
+                    warn!(%propagation_source, "Received gossip from unverified peer");
+                    return;
+                }
+
+                debug!(%propagation_source, "Received gossip message");
+
+                // Deserialize the protocol message
+                let Ok(protocol_message) = bincode::deserialize::<ProtocolMessage>(&message.data)
+                else {
+                    warn!(%propagation_source, "Failed to deserialize gossip message");
+                    return;
+                };
+
+                debug!(%propagation_source, %protocol_message, "Forwarding gossip message to protocol handler");
+                if let Err(e) = self.protocol_message_sender.send(protocol_message) {
+                    warn!(%propagation_source, "Failed to forward gossip message: {e}");
+                }
+            }
+            gossipsub::Event::Subscribed { peer_id, topic } => {
+                debug!(%peer_id, %topic, "Peer subscribed to topic");
+            }
+            gossipsub::Event::Unsubscribed { peer_id, topic } => {
+                debug!(%peer_id, %topic, "Peer unsubscribed from topic");
+            }
+            _ => {}
+        }
+    }
+}
+
+impl NetworkBehaviour for BlueprintProtocolBehaviour {
+    type ConnectionHandler =
+        <DerivedBlueprintProtocolBehaviour as NetworkBehaviour>::ConnectionHandler;
+
+    type ToSwarm = BlueprintProtocolEvent;
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &libp2p::Multiaddr,
+        remote_addr: &libp2p::Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.blueprint_protocol
+            .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: libp2p::core::Endpoint,
+        port_use: PortUse,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        self.blueprint_protocol
+            .handle_established_outbound_connection(
+                connection_id,
+                peer,
+                addr,
+                role_override,
+                port_use,
+            )
+    }
+
+    fn handle_pending_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        local_addr: &libp2p::Multiaddr,
+        remote_addr: &libp2p::Multiaddr,
+    ) -> Result<(), ConnectionDenied> {
+        self.blueprint_protocol.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: &[libp2p::Multiaddr],
+        effective_role: libp2p::core::Endpoint,
+    ) -> Result<Vec<libp2p::Multiaddr>, ConnectionDenied> {
+        self.blueprint_protocol.handle_pending_outbound_connection(
+            connection_id,
+            maybe_peer,
+            addresses,
+            effective_role,
+        )
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection_id: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        self.blueprint_protocol
+            .on_connection_handler_event(peer_id, connection_id, event);
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
+        match &event {
+            FromSwarm::ConnectionEstablished(e) if e.other_established == 0 => {
+                // Start handshake if this peer is not verified
+                if !self.peer_manager.is_peer_verified(&e.peer_id) {
+                    debug!(
+                        "Established connection with unverified peer {:?}, sending handshake",
+                        e.peer_id
+                    );
+                    let mut key_pair = self.instance_key_pair.clone();
+
+                    let handshake_msg = HandshakeMessage::new(self.local_peer_id);
+                    let Some(signature) =
+                        self.sign_handshake(&mut key_pair, &e.peer_id, &handshake_msg)
+                    else {
+                        return;
+                    };
+
+                    self.send_request(
+                        &e.peer_id,
+                        InstanceMessageRequest::Handshake {
+                            public_key: key_pair.public(),
+                            signature,
+                            msg: handshake_msg,
+                        },
+                    );
+                    self.outbound_handshakes.insert(e.peer_id, Instant::now());
+                    info!(
+                        "Established connection to {:?}, sending handshake",
+                        e.peer_id
+                    );
+                }
+
+                self.blueprint_protocol
+                    .gossipsub
+                    .add_explicit_peer(&e.peer_id);
+            }
+            FromSwarm::ConnectionClosed(e) if e.remaining_established == 0 => {
+                if self.inbound_handshakes.contains_key(&e.peer_id) {
+                    self.inbound_handshakes.remove(&e.peer_id);
+                }
+
+                if self.outbound_handshakes.contains_key(&e.peer_id) {
+                    self.outbound_handshakes.remove(&e.peer_id);
+                }
+
+                if self.peer_manager.is_peer_verified(&e.peer_id) {
+                    self.peer_manager
+                        .remove_peer(&e.peer_id, "connection closed");
+                }
+
+                self.blueprint_protocol
+                    .gossipsub
+                    .remove_explicit_peer(&e.peer_id);
+
+                self.peer_manager.remove_peer_id_from_public_key(&e.peer_id);
+            }
+
+            _ => {}
+        }
+
+        self.blueprint_protocol.on_swarm_event(event);
+    }
+
+    fn poll(
+        &mut self,
+        cx: &mut std::task::Context<'_>,
+    ) -> Poll<ToSwarm<Self::ToSwarm, THandlerInEvent<Self>>> {
+        while let Poll::Ready(ev) = self.blueprint_protocol.poll(cx) {
+            match ev {
+                ToSwarm::GenerateEvent(ev) => match ev {
+                    DerivedBlueprintProtocolBehaviourEvent::RequestResponse(
+                        blueprint_protocol_event,
+                    ) => self.handle_request_response_event(blueprint_protocol_event),
+                    DerivedBlueprintProtocolBehaviourEvent::Gossipsub(gossip_event) => {
+                        self.handle_gossipsub_event(gossip_event);
+                    }
+                },
+                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::CloseConnection {
+                    peer_id,
+                    connection,
+                } => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+                ToSwarm::ListenOn { opts } => return Poll::Ready(ToSwarm::ListenOn { opts }),
+                ToSwarm::RemoveListener { id } => {
+                    return Poll::Ready(ToSwarm::RemoveListener { id })
+                }
+                ToSwarm::NewExternalAddrCandidate(addr) => {
+                    return Poll::Ready(ToSwarm::NewExternalAddrCandidate(addr))
+                }
+                ToSwarm::ExternalAddrConfirmed(addr) => {
+                    return Poll::Ready(ToSwarm::ExternalAddrConfirmed(addr))
+                }
+                ToSwarm::ExternalAddrExpired(addr) => {
+                    return Poll::Ready(ToSwarm::ExternalAddrExpired(addr))
+                }
+                _ => {}
+            }
+        }
+        Poll::Pending
+    }
+}
diff --git a/crates/networking/src/blueprint_protocol/handler.rs b/crates/networking/src/blueprint_protocol/handler.rs
new file mode 100644
index 000000000..2d1fabe2c
--- /dev/null
+++ b/crates/networking/src/blueprint_protocol/handler.rs
@@ -0,0 +1,268 @@
+use std::time::{Duration, Instant};
+
+use libp2p::{request_response, PeerId};
+use tracing::{debug, warn};
+
+use crate::blueprint_protocol::HandshakeMessage;
+use crate::{key_types::InstanceMsgPublicKey, types::ProtocolMessage};
+
+use super::{BlueprintProtocolBehaviour, InstanceMessageRequest, InstanceMessageResponse};
+
+const INBOUND_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
+const OUTBOUND_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
+
+impl BlueprintProtocolBehaviour {
+    #[allow(clippy::too_many_lines)]
+    pub fn handle_request_response_event(
+        &mut self,
+        event: request_response::Event<InstanceMessageRequest, InstanceMessageResponse>,
+    ) {
+        match event {
+            request_response::Event::Message {
+                peer,
+                message:
+                    request_response::Message::Request {
+                        request:
+                            InstanceMessageRequest::Handshake {
+                                public_key,
+                                signature,
+                                msg,
+                            },
+                        channel,
+                        ..
+                    },
+                ..
+            } => {
+                debug!(%peer, "Received handshake request");
+
+                // Check if we already sent a handshake request to this peer
+                if self.outbound_handshakes.contains_key(&peer) {
+                    // If we have an outbound handshake pending, we should still respond to their request
+                    // This ensures both sides complete their handshakes even if messages cross on the wire
+                    debug!(%peer, "Responding to inbound handshake request while outbound is pending");
+                }
+
+                if !self.peer_manager.is_key_whitelisted(&public_key) {
+                    warn!(%peer, %public_key, "Received handshake response from unwhitelisted peer");
+                    self.peer_manager.handle_nonwhitelisted_peer(&peer);
+                    return;
+                }
+
+                // Verify the handshake
+                match self.verify_handshake(&msg, &public_key, &signature) {
+                    Ok(()) => {
+                        // Store the handshake request
+                        self.inbound_handshakes.insert(peer, Instant::now());
+                        self.peer_manager
+                            .add_peer_id_to_public_key(&peer, &public_key);
+
+                        // Send handshake response
+                        let mut key_pair = self.instance_key_pair.clone();
+
+                        let handshake_msg = HandshakeMessage::new(self.local_peer_id);
+                        let Some(signature) =
+                            self.sign_handshake(&mut key_pair, &peer, &handshake_msg)
+                        else {
+                            return;
+                        };
+
+                        let response = InstanceMessageResponse::Handshake {
+                            public_key: key_pair.public(),
+                            signature,
+                            msg: handshake_msg,
+                        };
+
+                        if let Err(e) = self.send_response(channel, response) {
+                            warn!(%peer, "Failed to send handshake response: {:?}", e);
+                            return;
+                        }
+                    }
+                    Err(e) => {
+                        warn!(%peer, "Invalid handshake request: {:?}", e);
+                        let response = InstanceMessageResponse::Error {
+                            code: 400,
+                            message: format!("Invalid handshake: {:?}", e),
+                        };
+                        if let Err(e) = self.send_response(channel, response) {
+                            warn!(%peer, "Failed to send error response: {:?}", e);
+                        }
+                    }
+                }
+            }
+            request_response::Event::Message {
+                peer,
+                message:
+                    request_response::Message::Response {
+                        response:
+                            InstanceMessageResponse::Handshake {
+                                public_key,
+                                signature,
+                                msg,
+                            },
+                        ..
+                    },
+                ..
+            } => {
+                debug!(%peer, "Received handshake response");
+
+                // Verify we have a pending outbound handshake
+                if !self.outbound_handshakes.contains_key(&peer) {
+                    warn!(%peer, "Received unexpected handshake response");
+                    return;
+                }
+
+                if !self.peer_manager.is_key_whitelisted(&public_key) {
+                    warn!(%peer, "Received handshake response from unwhitelisted peer");
+                    self.peer_manager.handle_nonwhitelisted_peer(&peer);
+                    return;
+                }
+
+                // Verify the handshake
+                match self.verify_handshake(&msg, &public_key, &signature) {
+                    Ok(()) => {
+                        // Mark handshake as completed
+                        self.complete_handshake(&peer, &public_key);
+                    }
+                    Err(e) => {
+                        warn!(%peer, "Invalid handshake verification: {:?}", e);
+                        self.outbound_handshakes.remove(&peer);
+                        self.handle_handshake_failure(&peer, "Invalid handshake verification");
+                    }
+                }
+            }
+            request_response::Event::Message {
+                peer,
+                message:
+                    request_response::Message::Request {
+                        request:
+                            InstanceMessageRequest::Protocol {
+                                protocol,
+                                payload,
+                                metadata: _,
+                            },
+                        channel,
+                        ..
+                    },
+                ..
+            } => {
+                // Reject messages from self
+                if peer == self.local_peer_id {
+                    return;
+                }
+
+                // Only accept protocol messages from peers we've completed handshakes with
+                if !self.peer_manager.is_peer_verified(&peer) {
+                    warn!(%peer, "Received protocol message from unverified peer");
+                    let response = InstanceMessageResponse::Error {
+                        code: 403,
+                        message: "Handshake required".to_string(),
+                    };
+                    if let Err(e) = self.send_response(channel, response) {
+                        warn!(%peer, "Failed to send error response: {:?}", e);
+                    }
+                    return;
+                }
+
+                let protocol_message: ProtocolMessage = match bincode::deserialize(&payload) {
+                    Ok(message) => message,
+                    Err(e) => {
+                        warn!(%peer, "Failed to deserialize protocol message: {:?}", e);
+                        let response = InstanceMessageResponse::Error {
+                            code: 400,
+                            message: format!("Invalid protocol message: {:?}", e),
+                        };
+                        if let Err(e) = self.send_response(channel, response) {
+                            warn!(%peer, "Failed to send error response: {:?}", e);
+                        }
+                        return;
+                    }
+                };
+
+                debug!(%peer, %protocol, %protocol_message, "Received protocol request");
+                if let Err(e) = self.protocol_message_sender.send(protocol_message) {
+                    warn!(%peer, "Failed to send protocol message: {:?}", e);
+                }
+            }
+            request_response::Event::Message {
+                peer,
+                message:
+                    request_response::Message::Response {
+                        response: InstanceMessageResponse::Error { code, message },
+                        ..
+                    },
+                ..
+            } => {
+                if !self.peer_manager.is_peer_verified(&peer) {
+                    warn!(%peer, code, %message, "Received error response from unverified peer");
+                    return;
+                }
+            }
+            request_response::Event::Message {
+                peer,
+                message:
+                    request_response::Message::Response {
+                        response: InstanceMessageResponse::Success { protocol, data: _ },
+                        ..
+                    },
+                ..
+            } => {
+                debug!(%peer, %protocol, "Received successful protocol response");
+            }
+            _ => {}
+        }
+
+        // Check for expired handshakes
+        self.check_expired_handshakes();
+    }
+
+    /// Check for and remove expired handshakes
+    fn check_expired_handshakes(&mut self) {
+        let now = Instant::now();
+
+        // Check inbound handshakes
+        let expired_inbound: Vec<_> = self
+            .inbound_handshakes
+            .clone()
+            .into_read_only()
+            .iter()
+            .filter(|(_, &time)| now.duration_since(time) > INBOUND_HANDSHAKE_TIMEOUT)
+            .map(|(peer_id, _)| *peer_id)
+            .collect();
+
+        for peer_id in expired_inbound {
+            self.inbound_handshakes.remove(&peer_id);
+            self.handle_handshake_failure(&peer_id, "Inbound handshake timeout");
+        }
+
+        // Check outbound handshakes
+        let expired_outbound: Vec<_> = self
+            .outbound_handshakes
+            .clone()
+            .into_read_only()
+            .iter()
+            .filter(|(_, &time)| now.duration_since(time) > OUTBOUND_HANDSHAKE_TIMEOUT)
+            .map(|(peer_id, _)| *peer_id)
+            .collect();
+
+        for peer_id in expired_outbound {
+            self.outbound_handshakes.remove(&peer_id);
+            self.handle_handshake_failure(&peer_id, "Outbound handshake timeout");
+        }
+    }
+
+    /// Complete a successful handshake
+    fn complete_handshake(&mut self, peer: &PeerId, public_key: &InstanceMsgPublicKey) {
+        debug!(%peer, "Completed handshake");
+
+        // Remove from pending handshakes
+        self.inbound_handshakes.remove(peer);
+        self.outbound_handshakes.remove(peer);
+
+        // Update peer manager
+        self.peer_manager
+            .add_peer_id_to_public_key(peer, public_key);
+
+        // Add to verified peers
+        self.peer_manager.verify_peer(peer);
+    }
+}
diff --git a/crates/networking/src/blueprint_protocol/mod.rs b/crates/networking/src/blueprint_protocol/mod.rs
new file mode 100644
index 000000000..dc1776f96
--- /dev/null
+++ b/crates/networking/src/blueprint_protocol/mod.rs
@@ -0,0 +1,114 @@
+mod behaviour;
+mod handler;
+
+pub use behaviour::{BlueprintProtocolBehaviour, BlueprintProtocolEvent};
+use libp2p::PeerId;
+
+use crate::key_types::{InstanceMsgPublicKey, InstanceSignedMsgSignature};
+use serde::{Deserialize, Serialize};
+
+/// A message sent to a specific instance or broadcast to all instances
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum InstanceMessageRequest {
+    /// Handshake request with authentication
+    Handshake {
+        /// Public key for authentication
+        public_key: InstanceMsgPublicKey,
+        /// Signature for verification
+        signature: InstanceSignedMsgSignature,
+        /// Handshake message
+        msg: HandshakeMessage,
+    },
+    /// Protocol-specific message with custom payload
+    Protocol {
+        /// Protocol identifier (e.g., "consensus/1.0.0", "sync/1.0.0")
+        protocol: String,
+        /// Protocol-specific message payload
+        payload: Vec<u8>,
+        /// Optional metadata for the protocol handler
+        metadata: Option<Vec<u8>>,
+    },
+}
+
+/// Response to an instance message
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum InstanceMessageResponse {
+    /// Handshake response with authentication
+    Handshake {
+        /// Public key for authentication
+        public_key: InstanceMsgPublicKey,
+        /// Signature for verification
+        signature: InstanceSignedMsgSignature,
+        /// Handshake message
+        msg: HandshakeMessage,
+    },
+    /// Success response with optional data
+    Success {
+        /// Protocol identifier (e.g., "consensus/1.0.0", "sync/1.0.0")
+        protocol: String,
+        /// Response data specific to the protocol
+        data: Option<Vec<u8>>,
+    },
+    /// Error response with details
+    Error {
+        /// Error code
+        code: u16,
+        /// Error message
+        message: String,
+    },
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct HandshakeMessage {
+    /// Sender [`PeerId`]
+    pub sender: PeerId,
+    /// A Unix timestamp in milliseconds
+    pub timestamp: u128,
+}
+
+impl HandshakeMessage {
+    /// Maximum age for a handshake message in milliseconds
+    pub const MAX_AGE: u128 = 30_000;
+
+    /// Creates a new handshake message
+    ///
+    /// # Panics
+    /// - If the system time is before the Unix epoch
+    #[must_use]
+    pub fn new(sender: PeerId) -> Self {
+        let timestamp = std::time::SystemTime::now()
+            .duration_since(std::time::UNIX_EPOCH)
+            .expect("time went backwards")
+            .as_millis();
+        Self { sender, timestamp }
+    }
+
+    /// Checks if the handshake message is expired
+    ///
+    /// # Arguments
+    /// - `max_age`: Maximum age in milliseconds
+    ///
+    /// # Returns
+    /// - `true` if the message is expired, `false` otherwise
+    ///
+    /// # Panics
+    /// - If the system time is before the Unix epoch
+    #[must_use]
+    pub fn is_expired(&self, max_age: u128) -> bool {
+        let now = std::time::SystemTime::now()
+            .duration_since(std::time::UNIX_EPOCH)
+            .expect("time went backwards")
+            .as_millis();
+        now.saturating_sub(self.timestamp) > max_age
+    }
+
+    /// Converts the handshake message to a byte array
+    #[must_use]
+    pub fn to_bytes(&self, other_peer_id: &PeerId) -> Vec<u8> {
+        let mut bytes = Vec::new();
+        bytes.extend(&self.sender.to_bytes());
+        bytes.extend(other_peer_id.to_bytes());
+        bytes.extend(&self.timestamp.to_be_bytes());
+        bytes
+    }
+}
diff --git a/crates/networking/src/discovery/behaviour.rs b/crates/networking/src/discovery/behaviour.rs
new file mode 100644
index 000000000..1ea3a189b
--- /dev/null
+++ b/crates/networking/src/discovery/behaviour.rs
@@ -0,0 +1,398 @@
+use std::{
+    cmp,
+    collections::{HashMap, HashSet, VecDeque},
+    task::{Context, Poll},
+    time::Duration,
+};
+
+use crate::error::{Error, Result as NetworkingResult};
+use libp2p::{
+    autonat,
+    core::Multiaddr,
+    identify,
+    identity::PeerId,
+    kad::{self, store::MemoryStore},
+    mdns::{self, Event as MdnsEvent},
+    relay,
+    swarm::{
+        behaviour::toggle::Toggle, derive_prelude::*, dial_opts::DialOpts, NetworkBehaviour,
+        ToSwarm,
+    },
+    upnp,
+};
+use tokio::time::Interval;
+use tracing::trace;
+use tracing::{debug, info};
+
+use super::PeerInfo;
+
+#[derive(NetworkBehaviour)]
+pub struct DerivedDiscoveryBehaviour {
+    /// Kademlia discovery
+    pub kademlia: Toggle<kad::Behaviour<MemoryStore>>,
+    /// Local network discovery via mDNS
+    pub mdns: Toggle<mdns::tokio::Behaviour>,
+    /// Identify protocol for peer information exchange
+    pub identify: identify::Behaviour,
+    /// NAT traversal
+    pub autonat: autonat::Behaviour,
+    /// `UPnP` port mapping
+    pub upnp: Toggle<upnp::tokio::Behaviour>,
+    /// Circuit relay for NAT traversal
+    pub relay: Toggle<relay::Behaviour>,
+}
+
+/// Event generated by the `DiscoveryBehaviour`.
+#[derive(Debug)]
+pub enum DiscoveryEvent {
+    /// Event that notifies that we connected to the node with the given peer
+    /// id.
+    PeerConnected(PeerId),
+
+    /// Event that notifies that we disconnected with the node with the given
+    /// peer id.
+    PeerDisconnected(PeerId),
+
+    /// Discovery event
+    Discovery(Box<DerivedDiscoveryBehaviourEvent>),
+}
+
+pub struct DiscoveryBehaviour {
+    /// Discovery behaviour
+    pub discovery: DerivedDiscoveryBehaviour,
+    /// Stream that fires when we need to perform the next random Kademlia
+    /// query.
+    pub next_kad_random_query: Interval,
+    /// After `next_kad_random_query` triggers, the next one triggers after this
+    /// duration.
+    pub duration_to_next_kad: Duration,
+    /// Events to return in priority when polled.
+    pub pending_events: VecDeque<DiscoveryEvent>,
+    /// Number of nodes we're currently connected to.
+    pub n_node_connected: u32,
+    /// Peers
+    pub peers: HashSet<PeerId>,
+    /// Peer info
+    pub peer_info: HashMap<PeerId, PeerInfo>,
+    /// Target peer count
+    pub target_peer_count: u32,
+    /// Options to configure dials to known peers.
+    pub pending_dial_opts: VecDeque<DialOpts>,
+}
+
+impl DiscoveryBehaviour {
+    /// Bootstrap Kademlia network
+    ///
+    /// # Errors
+    ///
+    /// * If Kademlia is not activated
+    pub fn bootstrap(&mut self) -> NetworkingResult<kad::QueryId> {
+        if let Some(active_kad) = self.discovery.kademlia.as_mut() {
+            active_kad.bootstrap().map_err(Into::into)
+        } else {
+            Err(Error::KademliaNotActivated)
+        }
+    }
+
+    #[must_use]
+    pub fn get_peers(&self) -> &HashSet<PeerId> {
+        &self.peers
+    }
+
+    #[must_use]
+    pub fn get_peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> {
+        self.peer_info.get(peer_id)
+    }
+
+    #[must_use]
+    pub fn nat_status(&self) -> autonat::NatStatus {
+        self.discovery.autonat.nat_status()
+    }
+
+    #[must_use]
+    pub fn get_peer_addresses(&self) -> HashMap<PeerId, HashSet<Multiaddr>> {
+        self.peer_info
+            .iter()
+            .map(|(peer_id, info)| (*peer_id, info.addresses.clone()))
+            .collect()
+    }
+}
+
+impl NetworkBehaviour for DiscoveryBehaviour {
+    type ConnectionHandler = <DerivedDiscoveryBehaviour as NetworkBehaviour>::ConnectionHandler;
+    type ToSwarm = DiscoveryEvent;
+
+    fn handle_established_inbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        local_addr: &Multiaddr,
+        remote_addr: &Multiaddr,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        debug!(%peer, "Handling inbound connection");
+        self.discovery.handle_established_inbound_connection(
+            connection_id,
+            peer,
+            local_addr,
+            remote_addr,
+        )
+    }
+
+    fn handle_established_outbound_connection(
+        &mut self,
+        connection_id: ConnectionId,
+        peer: PeerId,
+        addr: &libp2p::Multiaddr,
+        role_override: libp2p::core::Endpoint,
+        port_use: PortUse,
+    ) -> Result<THandler<Self>, ConnectionDenied> {
+        debug!(%peer, "Handling outbound connection");
+        self.peer_info
+            .entry(peer)
+            .or_insert_with(|| {
+                debug!(%peer, "Creating new peer info for outbound connection");
+                PeerInfo {
+                    addresses: HashSet::new(),
+                    identify_info: None,
+                    last_seen: std::time::SystemTime::now(),
+                    ping_latency: None,
+                    successes: 0,
+                    failures: 0,
+                    average_response_time: None,
+                }
+            })
+            .addresses
+            .insert(addr.clone());
+        self.discovery.handle_established_outbound_connection(
+            connection_id,
+            peer,
+            addr,
+            role_override,
+            port_use,
+        )
+    }
+
+    fn on_swarm_event(&mut self, event: FromSwarm<'_>) {
+        match &event {
+            FromSwarm::ConnectionEstablished(e) => {
+                if e.other_established == 0 {
+                    debug!(%e.peer_id, "First connection established with peer");
+                    self.n_node_connected += 1;
+                    self.peers.insert(e.peer_id);
+                    self.pending_events
+                        .push_back(DiscoveryEvent::PeerConnected(e.peer_id));
+                }
+            }
+            FromSwarm::ConnectionClosed(e) => {
+                if e.remaining_established == 0 {
+                    debug!(%e.peer_id, "Last connection closed with peer");
+                    self.n_node_connected -= 1;
+                    self.peers.remove(&e.peer_id);
+                    self.peer_info.remove(&e.peer_id);
+                    self.pending_events
+                        .push_back(DiscoveryEvent::PeerDisconnected(e.peer_id));
+                }
+            }
+            _ => {}
+        }
+        self.discovery.on_swarm_event(event);
+    }
+
+    fn on_connection_handler_event(
+        &mut self,
+        peer_id: PeerId,
+        connection: ConnectionId,
+        event: THandlerOutEvent<Self>,
+    ) {
+        self.discovery
+            .on_connection_handler_event(peer_id, connection, event);
+    }
+
+    #[allow(clippy::type_complexity, clippy::too_many_lines)]
+    fn poll(
+        &mut self,
+        cx: &mut Context<'_>,
+    ) -> Poll<ToSwarm<Self::ToSwarm, libp2p::swarm::THandlerInEvent<Self>>> {
+        // Immediately process the content of `discovered`.
+        if let Some(ev) = self.pending_events.pop_front() {
+            return Poll::Ready(ToSwarm::GenerateEvent(ev));
+        }
+
+        // Dial to peers
+        if let Some(opts) = self.pending_dial_opts.pop_front() {
+            return Poll::Ready(ToSwarm::Dial { opts });
+        }
+
+        // Poll the stream that fires when we need to start a random Kademlia query.
+        while self.next_kad_random_query.poll_tick(cx).is_ready() {
+            if self.n_node_connected < self.target_peer_count {
+                // We still have not hit the discovery max, send random request for peers.
+                let random_peer_id = PeerId::random();
+                debug!(
+                    "Libp2p <= Starting random Kademlia request for {:?}",
+                    random_peer_id
+                );
+                if let Some(kademlia) = self.discovery.kademlia.as_mut() {
+                    kademlia.get_closest_peers(random_peer_id);
+                }
+            }
+
+            // Schedule the next random query with exponentially increasing delay,
+            // capped at 60 seconds.
+            self.next_kad_random_query = tokio::time::interval(self.duration_to_next_kad);
+            // we need to reset the interval, otherwise the next tick completes immediately.
+            self.next_kad_random_query.reset();
+
+            self.duration_to_next_kad =
+                cmp::min(self.duration_to_next_kad * 2, Duration::from_secs(60));
+        }
+
+        // Poll discovery events.
+        while let Poll::Ready(ev) = self.discovery.poll(cx) {
+            match ev {
+                ToSwarm::GenerateEvent(ev) => {
+                    match &ev {
+                        DerivedDiscoveryBehaviourEvent::Identify(identify::Event::Received {
+                            peer_id,
+                            info,
+                            connection_id,
+                        }) => {
+                            debug!(%peer_id, "Received identify event in discovery behaviour");
+                            self.peer_info.entry(*peer_id).or_default().identify_info =
+                                Some(info.clone());
+                            if let Some(kademlia) = self.discovery.kademlia.as_mut() {
+                                for address in &info.listen_addrs {
+                                    kademlia.add_address(peer_id, address.clone());
+                                }
+                            }
+                            self.pending_events
+                                .push_back(DiscoveryEvent::Discovery(Box::new(
+                                    DerivedDiscoveryBehaviourEvent::Identify(
+                                        identify::Event::Received {
+                                            peer_id: *peer_id,
+                                            info: info.clone(),
+                                            connection_id: *connection_id,
+                                        },
+                                    ),
+                                )));
+                        }
+                        DerivedDiscoveryBehaviourEvent::Identify(identify::Event::Sent {
+                            ..
+                        }) => {
+                            debug!("Identify event sent");
+                        }
+                        DerivedDiscoveryBehaviourEvent::Identify(identify::Event::Pushed {
+                            ..
+                        }) => {
+                            debug!("Identify event pushed");
+                        }
+                        DerivedDiscoveryBehaviourEvent::Identify(identify::Event::Error {
+                            ..
+                        }) => {
+                            debug!("Identify event error");
+                        }
+
+                        DerivedDiscoveryBehaviourEvent::Autonat(_) => {}
+                        DerivedDiscoveryBehaviourEvent::Upnp(ev) => match ev {
+                            upnp::Event::NewExternalAddr(addr) => {
+                                info!("UPnP NewExternalAddr: {addr}");
+                            }
+                            upnp::Event::ExpiredExternalAddr(addr) => {
+                                info!("UPnP ExpiredExternalAddr: {addr}");
+                            }
+                            upnp::Event::GatewayNotFound => {
+                                info!("UPnP GatewayNotFound");
+                            }
+                            upnp::Event::NonRoutableGateway => {
+                                info!("UPnP NonRoutableGateway");
+                            }
+                        },
+                        DerivedDiscoveryBehaviourEvent::Kademlia(ev) => match ev {
+                            // Adding to Kademlia buckets is automatic with our config,
+                            // no need to do manually.
+                            kad::Event::RoutingUpdated { .. }
+                            | kad::Event::RoutablePeer { .. }
+                            | kad::Event::PendingRoutablePeer { .. } => {
+                                // Intentionally ignore
+                            }
+                            other => {
+                                trace!("Libp2p => Unhandled Kademlia event: {:?}", other);
+                            }
+                        },
+                        DerivedDiscoveryBehaviourEvent::Mdns(ev) => match ev {
+                            MdnsEvent::Discovered(list) => {
+                                if self.n_node_connected >= self.target_peer_count {
+                                    // Already over discovery max, don't add discovered peers.
+                                    // We could potentially buffer these addresses to be added later,
+                                    // but mdns is not an important use case and may be removed in future.
+                                    continue;
+                                }
+
+                                // Add any discovered peers to Kademlia
+                                for (peer_id, multiaddr) in list {
+                                    if let Some(kad) = self.discovery.kademlia.as_mut() {
+                                        kad.add_address(peer_id, multiaddr.clone());
+                                    }
+                                }
+                            }
+                            MdnsEvent::Expired(_) => {}
+                        },
+                        DerivedDiscoveryBehaviourEvent::Relay(relay_event) => match relay_event {
+                            relay::Event::ReservationReqAccepted { src_peer_id, .. } => {
+                                debug!("Relay accepted reservation request from: {src_peer_id:#?}");
+                            }
+                            relay::Event::ReservationReqDenied { src_peer_id } => {
+                                debug!("Reservation request was denied for: {src_peer_id:#?}");
+                            }
+                            relay::Event::ReservationTimedOut { src_peer_id } => {
+                                debug!("Reservation timed out for: {src_peer_id:#?}");
+                            }
+                            _ => {}
+                        },
+                    }
+                    self.pending_events
+                        .push_back(DiscoveryEvent::Discovery(Box::new(ev)));
+                }
+                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::CloseConnection {
+                    peer_id,
+                    connection,
+                } => {
+                    return Poll::Ready(ToSwarm::CloseConnection {
+                        peer_id,
+                        connection,
+                    })
+                }
+                ToSwarm::ListenOn { opts } => return Poll::Ready(ToSwarm::ListenOn { opts }),
+                ToSwarm::RemoveListener { id } => {
+                    return Poll::Ready(ToSwarm::RemoveListener { id })
+                }
+                ToSwarm::NewExternalAddrCandidate(addr) => {
+                    return Poll::Ready(ToSwarm::NewExternalAddrCandidate(addr))
+                }
+                ToSwarm::ExternalAddrConfirmed(addr) => {
+                    return Poll::Ready(ToSwarm::ExternalAddrConfirmed(addr))
+                }
+                ToSwarm::ExternalAddrExpired(addr) => {
+                    return Poll::Ready(ToSwarm::ExternalAddrExpired(addr))
+                }
+                _ => {}
+            }
+        }
+
+        Poll::Pending
+    }
+}
diff --git a/crates/networking/src/discovery/config.rs b/crates/networking/src/discovery/config.rs
new file mode 100644
index 000000000..dc3b29ce9
--- /dev/null
+++ b/crates/networking/src/discovery/config.rs
@@ -0,0 +1,188 @@
+use super::{
+    behaviour::{DerivedDiscoveryBehaviour, DiscoveryBehaviour},
+    new_kademlia,
+};
+use crate::error::Result;
+use libp2p::{
+    autonat, identify, identity::PublicKey, mdns, relay, upnp, Multiaddr, PeerId, StreamProtocol,
+};
+use std::{
+    collections::{HashMap, HashSet, VecDeque},
+    time::Duration,
+};
+use tracing::warn;
+
+pub struct DiscoveryConfig {
+    /// The local peer ID.
+    local_peer_id: PeerId,
+    /// The local public key.
+    local_public_key: PublicKey,
+    /// The bootstrap peers.
+    bootstrap_peers: Vec<(PeerId, Multiaddr)>,
+    /// The relay nodes.
+    relay_nodes: Vec<(PeerId, Multiaddr)>,
+    /// The number of peers to connect to.
+    target_peer_count: u32,
+    /// Enable mDNS discovery.
+    enable_mdns: bool,
+    /// Enable Kademlia discovery.
+    enable_kademlia: bool,
+    /// Enable `UPnP` discovery.
+    enable_upnp: bool,
+    /// Enable relay nodes.
+    enable_relay: bool,
+    /// The name of the network.
+    network_name: String,
+    /// Protocol version string that uniquely identifies your P2P service.
+    protocol_version: String,
+}
+
+impl DiscoveryConfig {
+    #[must_use]
+    pub fn new(local_public_key: PublicKey, network_name: impl Into<String>) -> Self {
+        Self {
+            local_peer_id: local_public_key.to_peer_id(),
+            local_public_key,
+            bootstrap_peers: Vec::new(),
+            relay_nodes: Vec::new(),
+            target_peer_count: 25, // Reasonable default
+            enable_mdns: true,     // Enable by default for local development
+            enable_kademlia: true, // Enable by default for production
+            enable_upnp: true,     // Enable by default for better connectivity
+            enable_relay: true,    // Enable by default for relay functionality
+            network_name: network_name.into(),
+            protocol_version: String::from("gadget/1.0.0"), // Default version
+        }
+    }
+
+    /// Set the protocol version that uniquely identifies your P2P service.
+    ///
+    /// This should be unique to your application to avoid conflicts with other P2P networks.
+    ///
+    /// * Format recommendation: `<service-name>/<version>`
+    /// * Example: `my-blockchain/1.0.0` or `my-chat-app/0.1.0`
+    #[must_use]
+    pub fn protocol_version(mut self, version: impl Into<String>) -> Self {
+        self.protocol_version = version.into();
+        self
+    }
+
+    #[must_use]
+    pub fn bootstrap_peers(mut self, peers: Vec<(PeerId, Multiaddr)>) -> Self {
+        self.bootstrap_peers = peers;
+        self
+    }
+
+    #[must_use]
+    pub fn relay_nodes(mut self, nodes: Vec<(PeerId, Multiaddr)>) -> Self {
+        self.relay_nodes = nodes;
+        self
+    }
+
+    #[must_use]
+    pub fn target_peer_count(mut self, count: u32) -> Self {
+        self.target_peer_count = count;
+        self
+    }
+
+    #[must_use]
+    pub fn mdns(mut self, enable: bool) -> Self {
+        self.enable_mdns = enable;
+        self
+    }
+
+    #[must_use]
+    pub fn kademlia(mut self, enable: bool) -> Self {
+        self.enable_kademlia = enable;
+        self
+    }
+
+    #[must_use]
+    pub fn upnp(mut self, enable: bool) -> Self {
+        self.enable_upnp = enable;
+        self
+    }
+
+    #[must_use]
+    pub fn relay(mut self, enable: bool) -> Self {
+        self.enable_relay = enable;
+        self
+    }
+
+    /// Construct this [`DiscoveryConfig`] into a [`DiscoveryBehaviour`]
+    ///
+    /// # Errors
+    ///
+    /// If `mdns` is enabled, see [`mdns::Behaviour::new`]
+    pub fn build(self) -> Result<DiscoveryBehaviour> {
+        let kademlia_opt = if self.enable_kademlia {
+            let protocol = StreamProtocol::try_from_owned(format!(
+                "/gadget/kad/{}/kad/1.0.0",
+                self.network_name
+            ))?;
+
+            let mut kademlia = new_kademlia(self.local_peer_id, protocol);
+
+            // Add bootstrap peers
+            for (peer_id, addr) in &self.bootstrap_peers {
+                kademlia.add_address(peer_id, addr.clone());
+            }
+
+            // Start bootstrap process
+            if let Err(e) = kademlia.bootstrap() {
+                warn!("Kademlia bootstrap failed: {}", e);
+            }
+
+            Some(kademlia)
+        } else {
+            None
+        };
+
+        let mdns_opt = if self.enable_mdns {
+            Some(mdns::Behaviour::new(
+                mdns::Config::default(),
+                self.local_peer_id,
+            )?)
+        } else {
+            None
+        };
+
+        let upnp_opt = if self.enable_upnp {
+            Some(upnp::tokio::Behaviour::default())
+        } else {
+            None
+        };
+
+        let relay_opt = if self.enable_relay {
+            let relay = relay::Behaviour::new(self.local_peer_id, relay::Config::default());
+            Some(relay)
+        } else {
+            None
+        };
+
+        let behaviour = DerivedDiscoveryBehaviour {
+            kademlia: kademlia_opt.into(),
+            mdns: mdns_opt.into(),
+            identify: identify::Behaviour::new(
+                identify::Config::new(self.protocol_version, self.local_public_key)
+                    .with_agent_version(format!("gadget-{}", env!("CARGO_PKG_VERSION")))
+                    .with_push_listen_addr_updates(true),
+            ),
+            autonat: autonat::Behaviour::new(self.local_peer_id, autonat::Config::default()),
+            upnp: upnp_opt.into(),
+            relay: relay_opt.into(),
+        };
+
+        Ok(DiscoveryBehaviour {
+            discovery: behaviour,
+            peers: HashSet::new(),
+            peer_info: HashMap::new(),
+            target_peer_count: self.target_peer_count,
+            next_kad_random_query: tokio::time::interval(Duration::from_secs(1)),
+            duration_to_next_kad: Duration::from_secs(1),
+            pending_events: VecDeque::new(),
+            n_node_connected: 0,
+            pending_dial_opts: VecDeque::new(),
+        })
+    }
+}
diff --git a/crates/networking/src/discovery/mod.rs b/crates/networking/src/discovery/mod.rs
new file mode 100644
index 000000000..2b63a01e7
--- /dev/null
+++ b/crates/networking/src/discovery/mod.rs
@@ -0,0 +1,32 @@
+use std::{num::NonZero, time::Duration};
+
+use libp2p::{
+    kad::{self, store::MemoryStore},
+    PeerId, StreamProtocol,
+};
+
+pub mod behaviour;
+pub mod config;
+pub mod peers;
+
+pub use peers::{PeerEvent, PeerInfo, PeerManager};
+
+#[must_use]
+#[allow(clippy::missing_panics_doc)]
+pub fn new_kademlia(peer_id: PeerId, protocol: StreamProtocol) -> kad::Behaviour<MemoryStore> {
+    let store = kad::store::MemoryStore::new(peer_id);
+    let mut config = kad::Config::new(protocol);
+
+    // Optimize Kademlia configuration
+    config
+        .set_query_timeout(Duration::from_secs(60))
+        .set_replication_factor(NonZero::new(3).unwrap())
+        .set_publication_interval(Some(Duration::from_secs(120)))
+        .set_provider_record_ttl(Some(Duration::from_secs(24 * 60 * 60)))
+        .set_record_ttl(Some(Duration::from_secs(24 * 60 * 60)))
+        .set_parallelism(NonZero::new(5).unwrap());
+
+    let mut kademlia = kad::Behaviour::with_config(peer_id, store, config);
+    kademlia.set_mode(Some(kad::Mode::Server));
+    kademlia
+}
diff --git a/crates/networking/src/discovery/peers.rs b/crates/networking/src/discovery/peers.rs
new file mode 100644
index 000000000..5814a9d14
--- /dev/null
+++ b/crates/networking/src/discovery/peers.rs
@@ -0,0 +1,292 @@
+use std::{
+    collections::HashSet,
+    sync::Arc,
+    time::{Duration, Instant, SystemTime},
+};
+
+use crate::InstanceMsgPublicKey;
+use dashmap::{DashMap, DashSet};
+use libp2p::{core::Multiaddr, identify, PeerId};
+use tokio::sync::broadcast;
+use tracing::debug;
+
+/// Information about a peer's connection and behavior
+#[derive(Clone, Debug)]
+pub struct PeerInfo {
+    /// Known addresses for the peer
+    pub addresses: HashSet<Multiaddr>,
+    /// Information from the identify protocol
+    pub identify_info: Option<identify::Info>,
+    /// When the peer was last seen
+    pub last_seen: SystemTime,
+    /// Latest ping latency
+    pub ping_latency: Option<Duration>,
+    /// Number of successful protocol interactions
+    pub successes: u32,
+    /// Number of failed protocol interactions
+    pub failures: u32,
+    /// Average response time for protocol requests
+    pub average_response_time: Option<Duration>,
+}
+
+impl Default for PeerInfo {
+    fn default() -> Self {
+        Self {
+            addresses: HashSet::new(),
+            identify_info: None,
+            last_seen: SystemTime::now(),
+            ping_latency: None,
+            successes: 0,
+            failures: 0,
+            average_response_time: None,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum PeerEvent {
+    /// A peer was added or updated
+    PeerUpdated {
+        peer_id: PeerId,
+        info: Box<PeerInfo>,
+    },
+    /// A peer was removed
+    PeerRemoved { peer_id: PeerId, reason: String },
+    /// A peer was banned
+    PeerBanned {
+        peer_id: PeerId,
+        reason: String,
+        expires_at: Option<Instant>,
+    },
+    /// A peer was unbanned
+    PeerUnbanned { peer_id: PeerId },
+}
+
+pub struct PeerManager {
+    /// Active peers and their information
+    peers: DashMap<PeerId, PeerInfo>,
+    /// Verified peers from completed handshakes
+    verified_peers: DashSet<PeerId>,
+    /// Handshake keys to peer ids
+    public_keys_to_peer_ids: Arc<DashMap<InstanceMsgPublicKey, PeerId>>,
+    /// Banned peers with optional expiration time
+    banned_peers: DashMap<PeerId, Option<Instant>>,
+    /// Allowed public keys
+    whitelisted_keys: DashSet<InstanceMsgPublicKey>,
+    /// Event sender for peer updates
+    event_tx: broadcast::Sender<PeerEvent>,
+}
+
+impl Default for PeerManager {
+    fn default() -> Self {
+        Self::new(HashSet::default())
+    }
+}
+
+impl PeerManager {
+    #[must_use]
+    pub fn new(whitelisted_keys: HashSet<InstanceMsgPublicKey>) -> Self {
+        let (event_tx, _) = broadcast::channel(100);
+        Self {
+            peers: DashMap::default(),
+            banned_peers: DashMap::default(),
+            verified_peers: DashSet::default(),
+            public_keys_to_peer_ids: Arc::new(DashMap::default()),
+            whitelisted_keys: DashSet::from_iter(whitelisted_keys),
+            event_tx,
+        }
+    }
+
+    pub fn update_whitelisted_keys(&self, keys: HashSet<InstanceMsgPublicKey>) {
+        self.whitelisted_keys.clear();
+        for key in keys {
+            self.whitelisted_keys.insert(key);
+        }
+    }
+
+    #[must_use]
+    pub fn is_key_whitelisted(&self, key: &InstanceMsgPublicKey) -> bool {
+        self.whitelisted_keys.contains(key)
+    }
+
+    pub fn handle_nonwhitelisted_peer(&self, peer: &PeerId) {
+        self.remove_peer(peer, "non-whitelisted");
+        self.ban_peer(*peer, "non-whitelisted", None);
+    }
+
+    /// Get a subscription to peer events
+    #[must_use]
+    pub fn subscribe(&self) -> broadcast::Receiver<PeerEvent> {
+        self.event_tx.subscribe()
+    }
+
+    /// Update or add peer information
+    pub fn update_peer(&self, peer_id: PeerId, mut info: PeerInfo) {
+        // Update last seen time
+        info.last_seen = SystemTime::now();
+
+        // Insert or update peer info
+        self.peers.insert(peer_id, info.clone());
+
+        // Emit event
+        let _ = self.event_tx.send(PeerEvent::PeerUpdated {
+            peer_id,
+            info: Box::new(info),
+        });
+    }
+
+    /// Remove a peer
+    pub fn remove_peer(&self, peer_id: &PeerId, reason: impl Into<String>) {
+        if self.peers.remove(peer_id).is_some() {
+            let reason = reason.into();
+            debug!(%peer_id, %reason, "removed peer");
+            let _ = self.event_tx.send(PeerEvent::PeerRemoved {
+                peer_id: *peer_id,
+                reason,
+            });
+        }
+    }
+
+    /// Verify a peer
+    pub fn verify_peer(&self, peer_id: &PeerId) {
+        self.verified_peers.insert(*peer_id);
+    }
+
+    /// Check if a peer is verified
+    #[must_use]
+    pub fn is_peer_verified(&self, peer_id: &PeerId) -> bool {
+        self.verified_peers.contains(peer_id)
+    }
+
+    /// Ban a peer with optional expiration
+    pub fn ban_peer(&self, peer_id: PeerId, reason: impl Into<String>, duration: Option<Duration>) {
+        let expires_at = duration.map(|d| Instant::now() + d);
+
+        // Remove from active peers
+        self.remove_peer(&peer_id, "banned");
+
+        // Add to banned peers
+        self.banned_peers.insert(peer_id, expires_at);
+
+        let reason = reason.into();
+        debug!(%peer_id, %reason, "banned peer");
+        let _ = self.event_tx.send(PeerEvent::PeerBanned {
+            peer_id,
+            reason,
+            expires_at,
+        });
+    }
+
+    /// Bans a peer with the default duration(`1h`)
+    pub fn ban_peer_with_default_duration(&self, peer: PeerId, reason: impl Into<String>) {
+        const BAN_PEER_DURATION: Duration = Duration::from_secs(60 * 60); //1h
+        self.ban_peer(peer, reason, Some(BAN_PEER_DURATION));
+    }
+
+    /// Unban a peer
+    pub fn unban_peer(&self, peer_id: &PeerId) {
+        if self.banned_peers.remove(peer_id).is_some() {
+            debug!(%peer_id, "unbanned peer");
+            let _ = self
+                .event_tx
+                .send(PeerEvent::PeerUnbanned { peer_id: *peer_id });
+        }
+    }
+
+    /// Check if a peer is banned
+    #[must_use]
+    pub fn is_banned(&self, peer_id: &PeerId) -> bool {
+        self.banned_peers.contains_key(peer_id)
+    }
+
+    /// Log a successful interaction with a peer
+    pub fn log_success(&self, peer_id: &PeerId, duration: Duration) {
+        if let Some(mut info) = self.peers.get_mut(peer_id) {
+            info.successes += 1;
+            update_average_time(&mut info, duration);
+            self.update_peer(*peer_id, info.clone());
+        }
+    }
+
+    /// Log a failed interaction with a peer
+    pub fn log_failure(&self, peer_id: &PeerId, duration: Duration) {
+        if let Some(mut info) = self.peers.get_mut(peer_id) {
+            info.failures += 1;
+            update_average_time(&mut info, duration);
+            self.update_peer(*peer_id, info.clone());
+        }
+    }
+
+    /// Get peer information
+    #[must_use]
+    pub fn get_peer_info(&self, peer_id: &PeerId) -> Option<PeerInfo> {
+        self.peers.get(peer_id).map(|info| info.value().clone())
+    }
+
+    /// Get all active peers
+    #[must_use]
+    pub fn get_peers(&self) -> DashMap<PeerId, PeerInfo> {
+        self.peers.clone()
+    }
+
+    /// Get number of active peers
+    #[must_use]
+    pub fn peer_count(&self) -> usize {
+        self.peers.len()
+    }
+
+    /// Start the background task to clean up expired bans
+    pub async fn run_ban_cleanup(self: Arc<Self>) {
+        loop {
+            let now = Instant::now();
+            let mut to_unban = Vec::new();
+
+            // Find expired bans
+            let banned_peers = self.banned_peers.clone().into_read_only();
+            for (peer_id, expires_at) in banned_peers.iter() {
+                if let Some(expiry) = expires_at {
+                    if now >= *expiry {
+                        to_unban.push(*peer_id);
+                    }
+                }
+            }
+
+            // Unban expired peers
+            for peer_id in to_unban {
+                self.unban_peer(&peer_id);
+            }
+
+            tokio::time::sleep(Duration::from_secs(60)).await;
+        }
+    }
+
+    /// Add a peer id to the public key to peer id map after verifying handshake
+    pub fn add_peer_id_to_public_key(&self, peer_id: &PeerId, public_key: &InstanceMsgPublicKey) {
+        self.public_keys_to_peer_ids.insert(*public_key, *peer_id);
+    }
+
+    /// Remove a peer id from the public key to peer id map
+    pub fn remove_peer_id_from_public_key(&self, peer_id: &PeerId) {
+        self.public_keys_to_peer_ids.retain(|_, id| id != peer_id);
+    }
+
+    #[must_use]
+    pub fn get_peer_id_from_public_key(&self, public_key: &InstanceMsgPublicKey) -> Option<PeerId> {
+        self.public_keys_to_peer_ids.get(public_key).map(|id| *id)
+    }
+}
+
+/// Update the average response time for a peer
+fn update_average_time(info: &mut PeerInfo, duration: Duration) {
+    const ALPHA: u32 = 5; // Smoothing factor for the moving average
+
+    if info.average_response_time.is_none() {
+        info.average_response_time = Some(duration);
+    } else if duration < info.average_response_time.unwrap() {
+        let delta = (info.average_response_time.unwrap() - duration) / ALPHA;
+        info.average_response_time = Some(info.average_response_time.unwrap() - delta);
+    } else {
+        let delta = (duration - info.average_response_time.unwrap()) / ALPHA;
+        info.average_response_time = Some(info.average_response_time.unwrap() + delta);
+    }
+}
diff --git a/crates/networking/src/error.rs b/crates/networking/src/error.rs
index 9c4d13ead..b424d28f0 100644
--- a/crates/networking/src/error.rs
+++ b/crates/networking/src/error.rs
@@ -1,3 +1,7 @@
+use crate::NetworkEvent;
+
+pub type Result<T> = core::result::Result<T, Error>;
+
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
     #[error("Network error: {0}")]
@@ -32,6 +36,9 @@ pub enum Error {
     #[error("No network found")]
     NoNetworkFound,
 
+    #[error("Kademlia is not activated")]
+    KademliaNotActivated,
+
     #[error("Other error: {0}")]
     Other(String),
 
@@ -40,16 +47,33 @@ pub enum Error {
     Io(#[from] std::io::Error),
 
     // libp2p compat
+    #[error(transparent)]
+    InvalidProtocol(#[from] libp2p::swarm::InvalidProtocol),
+
+    #[error(transparent)]
+    NoKnownPeers(#[from] libp2p::kad::NoKnownPeers),
+
     #[error(transparent)]
     Dial(#[from] libp2p::swarm::DialError),
+
     #[error(transparent)]
     Noise(#[from] libp2p::noise::Error),
+
     #[error(transparent)]
     Behaviour(#[from] libp2p::BehaviourBuilderError),
+
     #[error(transparent)]
     Subscription(#[from] libp2p::gossipsub::SubscriptionError),
+
     #[error(transparent)]
     TransportIo(#[from] libp2p::TransportError<std::io::Error>),
+
     #[error(transparent)]
     Multiaddr(#[from] libp2p::multiaddr::Error),
+
+    #[error(transparent)]
+    TokioSendError(#[from] tokio::sync::mpsc::error::SendError<NetworkEvent>),
+
+    #[error(transparent)]
+    CrossbeamSendError(#[from] crossbeam_channel::SendError<NetworkEvent>),
 }
diff --git a/crates/networking/src/gossip.rs b/crates/networking/src/gossip.rs
deleted file mode 100644
index f63465c41..000000000
--- a/crates/networking/src/gossip.rs
+++ /dev/null
@@ -1,430 +0,0 @@
-#![allow(
-    missing_debug_implementations,
-    unused_results,
-    clippy::module_name_repetitions,
-    clippy::exhaustive_enums
-)]
-
-use crate::error::Error;
-use crate::key_types::{GossipMsgKeyPair, GossipMsgPublicKey, GossipSignedMsgSignature};
-use async_trait::async_trait;
-use gadget_crypto::hashing::blake3_256;
-use gadget_std::collections::BTreeMap;
-use gadget_std::string::ToString;
-use gadget_std::sync::atomic::AtomicUsize;
-use gadget_std::sync::Arc;
-use libp2p::gossipsub::IdentTopic;
-use libp2p::kad::store::MemoryStore;
-use libp2p::{
-    gossipsub, mdns, request_response, swarm::NetworkBehaviour, swarm::SwarmEvent, PeerId,
-};
-use lru_mem::LruCache;
-use serde::{Deserialize, Serialize};
-use tokio::sync::mpsc::UnboundedSender;
-use tokio::sync::{Mutex, RwLock};
-
-use crate::networking::{Network, ParticipantInfo, ProtocolMessage};
-use gadget_std as std;
-use gadget_std::{boxed::Box, format, string::String, vec::Vec};
-use std::vec;
-
-/// 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)]
-pub struct MyBehaviour {
-    pub gossipsub: gossipsub::Behaviour,
-    pub mdns: mdns::tokio::Behaviour,
-    pub p2p: request_response::cbor::Behaviour<MyBehaviourRequest, MyBehaviourResponse>,
-    pub identify: libp2p::identify::Behaviour,
-    pub kadmelia: libp2p::kad::Behaviour<MemoryStore>,
-    pub dcutr: libp2p::dcutr::Behaviour,
-    pub relay: libp2p::relay::Behaviour,
-    pub ping: libp2p::ping::Behaviour,
-}
-
-pub type InboundMapping = (IdentTopic, UnboundedSender<Vec<u8>>, Arc<AtomicUsize>);
-
-pub struct NetworkServiceWithoutSwarm<'a> {
-    pub inbound_mapping: &'a [InboundMapping],
-    pub public_key_to_libp2p_id: Arc<RwLock<BTreeMap<GossipMsgPublicKey, PeerId>>>,
-    pub secret_key: &'a GossipMsgKeyPair,
-    pub connected_peers: Arc<AtomicUsize>,
-    pub span: tracing::Span,
-    pub my_id: PeerId,
-}
-
-impl<'a> NetworkServiceWithoutSwarm<'a> {
-    pub(crate) fn with_swarm(
-        &'a self,
-        swarm: &'a mut libp2p::Swarm<MyBehaviour>,
-    ) -> NetworkService<'a> {
-        NetworkService {
-            swarm,
-            inbound_mapping: self.inbound_mapping,
-            public_key_to_libp2p_id: &self.public_key_to_libp2p_id,
-            secret_key: self.secret_key,
-            connected_peers: self.connected_peers.clone(),
-            span: &self.span,
-            my_id: self.my_id,
-        }
-    }
-}
-
-pub struct NetworkService<'a> {
-    pub swarm: &'a mut libp2p::Swarm<MyBehaviour>,
-    pub inbound_mapping: &'a [InboundMapping],
-    pub public_key_to_libp2p_id: &'a Arc<RwLock<BTreeMap<GossipMsgPublicKey, PeerId>>>,
-    pub connected_peers: Arc<AtomicUsize>,
-    pub secret_key: &'a GossipMsgKeyPair,
-    pub span: &'a tracing::Span,
-    pub my_id: PeerId,
-}
-
-impl NetworkService<'_> {
-    /// Handle local requests that are meant to be sent to the network.
-    pub(crate) 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 = bincode::serialize(&payload).expect("Should serialize");
-                if let Err(e) = self
-                    .swarm
-                    .behaviour_mut()
-                    .gossipsub
-                    .publish(msg.topic, gossip_message)
-                {
-                    gadget_logging::error!("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(_)) => {
-                gadget_logging::error!("Broadcasting a request is not supported");
-            }
-            (MessageType::Broadcast, GossipOrRequestResponse::Response(_)) => {
-                gadget_logging::error!("Broadcasting a response is not supported");
-            }
-            (MessageType::P2P(_), GossipOrRequestResponse::Gossip(_)) => {
-                gadget_logging::error!("P2P message should be a request or response");
-            }
-            (MessageType::P2P(_), GossipOrRequestResponse::Response(_)) => {
-                // TODO: Send the response to the peer.
-            }
-        }
-    }
-
-    /// Handle inbound events from the networking layer
-    #[allow(clippy::too_many_lines)]
-    pub(crate) async fn handle_swarm_event(&mut self, event: SwarmEvent<MyBehaviourEvent>) {
-        use MyBehaviourEvent::{Dcutr, Gossipsub, Identify, Kadmelia, Mdns, P2p, Ping, Relay};
-        use SwarmEvent::{
-            Behaviour, ConnectionClosed, ConnectionEstablished, Dialing, ExpiredListenAddr,
-            ExternalAddrConfirmed, ExternalAddrExpired, IncomingConnection,
-            IncomingConnectionError, ListenerClosed, ListenerError, NewExternalAddrCandidate,
-            NewExternalAddrOfPeer, NewListenAddr, OutgoingConnectionError,
-        };
-        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)) => {
-                gadget_logging::trace!("Kadmelia event: {event:?}");
-            }
-            Behaviour(Dcutr(event)) => {
-                self.handle_dcutr_event(event).await;
-            }
-            Behaviour(Relay(event)) => {
-                self.handle_relay_event(event).await;
-            }
-            Behaviour(Ping(event)) => {
-                self.handle_ping_event(event).await;
-            }
-
-            NewListenAddr {
-                address,
-                listener_id,
-            } => {
-                gadget_logging::trace!("{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,
-            } => {
-                gadget_logging::trace!("{listener_id} has an expired address: {address}");
-            }
-            ListenerClosed {
-                listener_id,
-                addresses,
-                reason,
-            } => {
-                gadget_logging::trace!(
-                    "{listener_id} on {addresses:?} has been closed: {reason:?}"
-                );
-            }
-            ListenerError { listener_id, error } => {
-                gadget_logging::error!("{listener_id} has an error: {error}");
-            }
-            Dialing {
-                peer_id,
-                connection_id,
-            } => {
-                gadget_logging::trace!(
-                    "Dialing peer: {peer_id:?} with connection_id: {connection_id}"
-                );
-            }
-            NewExternalAddrCandidate { address } => {
-                gadget_logging::trace!("New external address candidate: {address}");
-            }
-            ExternalAddrConfirmed { address } => {
-                gadget_logging::trace!("External address confirmed: {address}");
-            }
-            ExternalAddrExpired { address } => {
-                gadget_logging::trace!("External address expired: {address}");
-            }
-            NewExternalAddrOfPeer { peer_id, address } => {
-                gadget_logging::trace!(
-                    "New external address of peer: {peer_id} with address: {address}"
-                );
-            }
-            unknown => {
-                gadget_logging::warn!("Unknown swarm event: {unknown:?}");
-            }
-        }
-    }
-}
-
-pub struct GossipHandle {
-    pub topic: IdentTopic,
-    pub tx_to_outbound: UnboundedSender<IntraNodePayload>,
-    pub rx_from_inbound: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>>>,
-    pub connected_peers: Arc<AtomicUsize>,
-    pub public_key_to_libp2p_id: Arc<RwLock<BTreeMap<GossipMsgPublicKey, PeerId>>>,
-    pub recent_messages: parking_lot::Mutex<LruCache<[u8; 32], ()>>,
-    pub my_id: GossipMsgPublicKey,
-}
-
-impl GossipHandle {
-    #[must_use]
-    pub fn connected_peers(&self) -> usize {
-        self.connected_peers
-            .load(gadget_std::sync::atomic::Ordering::Relaxed)
-    }
-
-    #[must_use]
-    pub fn topic(&self) -> IdentTopic {
-        self.topic.clone()
-    }
-
-    /// Returns an ordered vector of public keys of the peers that are connected to the gossipsub topic.
-    pub async fn peers(&self) -> Vec<GossipMsgPublicKey> {
-        self.public_key_to_libp2p_id
-            .read()
-            .await
-            .keys()
-            .copied()
-            .collect()
-    }
-}
-
-pub struct IntraNodePayload {
-    topic: IdentTopic,
-    payload: GossipOrRequestResponse,
-    message_type: MessageType,
-}
-
-impl gadget_std::fmt::Debug for IntraNodePayload {
-    fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
-        f.debug_struct("IntraNodePayload")
-            .field("topic", &self.topic)
-            .finish_non_exhaustive()
-    }
-}
-
-#[non_exhaustive]
-#[derive(Serialize, Deserialize, Debug)]
-pub enum GossipOrRequestResponse {
-    Gossip(GossipMessage),
-    Request(MyBehaviourRequest),
-    Response(MyBehaviourResponse),
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-pub struct GossipMessage {
-    pub topic: String,
-    pub raw_payload: Vec<u8>,
-}
-
-#[non_exhaustive]
-#[derive(Serialize, Deserialize, Debug)]
-pub enum MyBehaviourRequest {
-    Handshake {
-        public_key: GossipMsgPublicKey,
-        signature: GossipSignedMsgSignature,
-    },
-    Message {
-        topic: String,
-        raw_payload: Vec<u8>,
-    },
-}
-
-#[non_exhaustive]
-#[derive(Serialize, Deserialize, Debug)]
-pub enum MyBehaviourResponse {
-    Handshaked {
-        public_key: GossipMsgPublicKey,
-        signature: GossipSignedMsgSignature,
-    },
-    MessageHandled,
-}
-
-enum MessageType {
-    Broadcast,
-    P2P(PeerId),
-}
-
-#[async_trait]
-impl Network for GossipHandle {
-    async fn next_message(&self) -> Option<ProtocolMessage> {
-        loop {
-            let mut lock = self
-                .rx_from_inbound
-                .try_lock()
-                .expect("There should be only a single caller for `next_message`");
-
-            let message_bytes = lock.recv().await?;
-            drop(lock);
-            match bincode::deserialize::<ProtocolMessage>(&message_bytes) {
-                Ok(message) => {
-                    let hash = blake3_256(&message_bytes);
-                    let mut map = self.recent_messages.lock();
-                    if map
-                        .insert(hash, ())
-                        .expect("Should not exceed memory limit (rx)")
-                        .is_none()
-                    {
-                        return Some(message);
-                    }
-                }
-                Err(e) => {
-                    gadget_logging::error!("Failed to deserialize message (gossip): {e}");
-                }
-            }
-        }
-    }
-
-    async fn send_message(&self, mut message: ProtocolMessage) -> Result<(), Error> {
-        message.sender.public_key = Some(self.my_id);
-        let message_type = if let Some(ParticipantInfo {
-            public_key: Some(to),
-            ..
-        }) = message.recipient
-        {
-            let pub_key_to_libp2p_id = self.public_key_to_libp2p_id.read().await;
-            gadget_logging::trace!("Handshake count: {}", pub_key_to_libp2p_id.len());
-            let libp2p_id = pub_key_to_libp2p_id
-                .get(&to)
-                .copied()
-                .ok_or_else(|| {
-                    Error::NetworkError(format!(
-                        "No libp2p ID found for crypto public key: {:?}. No handshake happened? Total handshakes: {}",
-                        to, pub_key_to_libp2p_id.len(),
-                    ))
-                })?;
-
-            MessageType::P2P(libp2p_id)
-        } else {
-            MessageType::Broadcast
-        };
-
-        let raw_payload =
-            bincode::serialize(&message).map_err(|err| Error::MessagingError(err.to_string()))?;
-        let payload_inner = match message_type {
-            MessageType::Broadcast => GossipOrRequestResponse::Gossip(GossipMessage {
-                topic: self.topic.to_string(),
-                raw_payload,
-            }),
-            MessageType::P2P(_) => GossipOrRequestResponse::Request(MyBehaviourRequest::Message {
-                topic: self.topic.to_string(),
-                raw_payload,
-            }),
-        };
-
-        let payload = IntraNodePayload {
-            topic: self.topic.clone(),
-            payload: payload_inner,
-            message_type,
-        };
-
-        self.tx_to_outbound
-            .send(payload)
-            .map_err(|e| Error::NetworkError(format!("Failed to send intra-node payload: {e}")))
-    }
-
-    fn public_id(&self) -> GossipMsgPublicKey {
-        self.my_id
-    }
-}
diff --git a/crates/crypto/sp-core/src/sp_core_util.rs b/crates/networking/src/handlers/blueprint_protocol.rs
similarity index 100%
rename from crates/crypto/sp-core/src/sp_core_util.rs
rename to crates/networking/src/handlers/blueprint_protocol.rs
diff --git a/crates/networking/src/handlers/connections.rs b/crates/networking/src/handlers/connections.rs
deleted file mode 100644
index 0f686c31e..000000000
--- a/crates/networking/src/handlers/connections.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-#![allow(unused_results, clippy::used_underscore_binding)]
-
-use crate::gossip::{MyBehaviourRequest, NetworkService};
-use crate::key_types::Curve;
-use gadget_crypto::KeyType;
-use gadget_std as std;
-use itertools::Itertools;
-use libp2p::PeerId;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self))]
-    pub(crate) async fn handle_connection_established(
-        &mut self,
-        peer_id: PeerId,
-        _num_established: u32,
-    ) {
-        gadget_logging::debug!("Connection established");
-        if !self
-            .public_key_to_libp2p_id
-            .read()
-            .await
-            .iter()
-            .any(|(_, id)| id == &peer_id)
-        {
-            let my_peer_id = *self.swarm.local_peer_id();
-            let msg = my_peer_id.to_bytes();
-            match <Curve as KeyType>::sign_with_secret(&mut self.secret_key.clone(), &msg) {
-                Ok(signature) => {
-                    let handshake = MyBehaviourRequest::Handshake {
-                        public_key: self.secret_key.public(),
-                        signature,
-                    };
-                    self.swarm
-                        .behaviour_mut()
-                        .p2p
-                        .send_request(&peer_id, handshake);
-                    self.swarm
-                        .behaviour_mut()
-                        .gossipsub
-                        .add_explicit_peer(&peer_id);
-                    gadget_logging::info!("Sent handshake from {my_peer_id} to {peer_id}");
-                }
-                Err(e) => {
-                    gadget_logging::error!("Failed to sign handshake: {e}");
-                }
-            }
-        }
-    }
-
-    #[tracing::instrument(skip(self))]
-    pub(crate) async fn handle_connection_closed(
-        &mut self,
-        peer_id: PeerId,
-        num_established: u32,
-        _cause: Option<libp2p::swarm::ConnectionError>,
-    ) {
-        gadget_logging::trace!("Connection closed");
-        if num_established == 0 {
-            self.swarm
-                .behaviour_mut()
-                .gossipsub
-                .remove_explicit_peer(&peer_id);
-            let mut pub_key_to_libp2p_id = self.public_key_to_libp2p_id.write().await;
-            let len_initial = 0;
-            pub_key_to_libp2p_id.retain(|_, id| *id != peer_id);
-            if pub_key_to_libp2p_id.len() == len_initial + 1 {
-                self.connected_peers
-                    .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
-            }
-        }
-    }
-
-    #[tracing::instrument(skip(self))]
-    pub(crate) async fn handle_incoming_connection(
-        &mut self,
-        _connection_id: libp2p::swarm::ConnectionId,
-        _local_addr: libp2p::Multiaddr,
-        _send_back_addr: libp2p::Multiaddr,
-    ) {
-        gadget_logging::trace!("Incoming connection");
-    }
-
-    #[tracing::instrument(skip(self))]
-    async fn handle_outgoing_connection(
-        &mut self,
-        peer_id: PeerId,
-        _connection_id: libp2p::swarm::ConnectionId,
-    ) {
-        gadget_logging::trace!("Outgoing connection to peer: {peer_id}");
-    }
-
-    #[tracing::instrument(skip(self, error))]
-    pub(crate) 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,
-    ) {
-        gadget_logging::error!("Incoming connection error: {error}");
-    }
-
-    #[tracing::instrument(skip(self, error))]
-    pub(crate) async fn handle_outgoing_connection_error(
-        &mut self,
-        _connection_id: libp2p::swarm::ConnectionId,
-        _peer_id: Option<PeerId>,
-        error: libp2p::swarm::DialError,
-    ) {
-        if let libp2p::swarm::DialError::Transport(addrs) = error {
-            let read = self.public_key_to_libp2p_id.read().await;
-            for (addr, err) in addrs {
-                if let Some(peer_id) = get_peer_id_from_multiaddr(&addr) {
-                    if !read.values().contains(&peer_id) {
-                        gadget_logging::warn!(
-                            "Outgoing connection error to peer: {peer_id} at {addr}: {err}",
-                            peer_id = peer_id,
-                            addr = addr,
-                            err = err
-                        );
-                    }
-                }
-            }
-        } else {
-            gadget_logging::error!("Outgoing connection error to peer: {error}");
-        }
-    }
-}
-
-fn get_peer_id_from_multiaddr(addr: &libp2p::Multiaddr) -> Option<PeerId> {
-    addr.iter()
-        .find_map(|proto| {
-            if let libp2p::multiaddr::Protocol::P2p(peer_id) = proto {
-                Some(Some(peer_id))
-            } else {
-                None
-            }
-        })
-        .flatten()
-}
diff --git a/crates/networking/src/handlers/dcutr.rs b/crates/networking/src/handlers/dcutr.rs
deleted file mode 100644
index f0fd3497f..000000000
--- a/crates/networking/src/handlers/dcutr.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-use crate::gossip::NetworkService;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub async fn handle_dcutr_event(&mut self, event: libp2p::dcutr::Event) {
-        gadget_logging::trace!("DCUTR event: {event:?}");
-    }
-}
diff --git a/crates/networking/src/handlers/discovery.rs b/crates/networking/src/handlers/discovery.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/crates/networking/src/handlers/discovery.rs
@@ -0,0 +1,1 @@
+
diff --git a/crates/networking/src/handlers/gossip.rs b/crates/networking/src/handlers/gossip.rs
deleted file mode 100644
index ae4c7d559..000000000
--- a/crates/networking/src/handlers/gossip.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-#![allow(unused_results)]
-
-use crate::gossip::{GossipMessage, NetworkService};
-use gadget_std::string::ToString;
-use gadget_std::sync::atomic::AtomicUsize;
-use gadget_std::sync::Arc;
-use libp2p::gossipsub::{Event, TopicHash};
-use libp2p::{gossipsub, PeerId};
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub(crate) async fn handle_gossip(&mut self, event: gossipsub::Event) {
-        let with_connected_peers = |topic: &TopicHash, f: fn(&Arc<AtomicUsize>)| {
-            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 {
-            Event::Message {
-                propagation_source,
-                message_id,
-                message,
-            } => {
-                self.handle_gossip_message(propagation_source, message_id, message)
-                    .await;
-            }
-            Event::Subscribed { peer_id, topic } => {
-                let added = with_connected_peers(&topic, |_connected_peers| {
-                    // Code commented out because each peer needs to do a request-response
-                    // direct P2P handshake, which is where the connected_peers counter is
-                    // incremented. Adding here will just add twice, which is undesirable.
-                    // connected_peers.fetch_add(1, gadget_std::sync::atomic::Ordering::Relaxed);
-                });
-                if added {
-                    gadget_logging::trace!("{peer_id} subscribed to {topic}");
-                } else {
-                    gadget_logging::error!("{peer_id} subscribed to unknown topic: {topic}");
-                }
-            }
-            Event::Unsubscribed { peer_id, topic } => {
-                let removed = with_connected_peers(&topic, |_connected_peers| {
-                    // Code commented out because each peer needs to do a request-response
-                    // direct P2P handshake, which is where the connected_peers counter is
-                    // decremented. Subbing here will just sub twice, which is undesirable.
-                    // connected_peers.fetch_sub(1, gadget_std::sync::atomic::Ordering::Relaxed);
-                });
-                if removed {
-                    gadget_logging::trace!("{peer_id} unsubscribed from {topic}");
-                } else {
-                    gadget_logging::error!("{peer_id} unsubscribed from unknown topic: {topic}");
-                }
-            }
-            Event::GossipsubNotSupported { peer_id } => {
-                gadget_logging::trace!("{peer_id} does not support gossipsub!");
-            }
-            Event::SlowPeer {
-                peer_id,
-                failed_messages: _,
-            } => {
-                gadget_logging::error!("{peer_id} wasn't able to download messages in time!");
-            }
-        }
-    }
-
-    #[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 {
-            gadget_logging::error!("Got message from unknown peer");
-            return;
-        };
-
-        // Reject messages from self
-        if origin == self.my_id {
-            return;
-        }
-
-        gadget_logging::trace!("Got message from peer: {origin}");
-        match bincode::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) {
-                        gadget_logging::warn!("Failed to send message to worker: {e}");
-                    }
-                } else {
-                    gadget_logging::error!("No registered worker for topic: {topic}!");
-                }
-            }
-            Err(e) => {
-                gadget_logging::error!("Failed to deserialize message (handlers/gossip): {e}");
-            }
-        }
-    }
-}
diff --git a/crates/networking/src/handlers/identify.rs b/crates/networking/src/handlers/identify.rs
deleted file mode 100644
index 017be564b..000000000
--- a/crates/networking/src/handlers/identify.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use crate::gossip::NetworkService;
-use gadget_std::format;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub(crate) async fn handle_identify_event(&mut self, event: libp2p::identify::Event) {
-        use libp2p::identify::Event::{Error, Pushed, Received, Sent};
-        match event {
-            Received { peer_id, info, .. } => {
-                // TODO: Verify the peer info, for example the protocol version, agent version, etc.
-                let info_lines = [
-                    format!("Protocol Version: {}", info.protocol_version),
-                    format!("Agent Version: {}", info.agent_version),
-                    format!("Supported Protocols: {:?}", info.protocols),
-                ];
-                let info_lines = info_lines.join(", ");
-                gadget_logging::trace!(
-                    "Received identify event from peer: {peer_id} with info: {info_lines}"
-                );
-                self.swarm.add_external_address(info.observed_addr);
-            }
-            Sent { peer_id, .. } => {
-                gadget_logging::trace!("Sent identify event to peer: {peer_id}");
-            }
-            Pushed { peer_id, info, .. } => {
-                let info_lines = [
-                    format!("Protocol Version: {}", info.protocol_version),
-                    format!("Agent Version: {}", info.agent_version),
-                    format!("Supported Protocols: {:?}", info.protocols),
-                ];
-                let info_lines = info_lines.join(", ");
-                gadget_logging::trace!(
-                    "Pushed identify event to peer: {peer_id} with info: {info_lines}"
-                );
-            }
-            Error { peer_id, error, .. } => {
-                gadget_logging::error!("Identify error from peer: {peer_id} with error: {error}");
-            }
-        }
-    }
-}
diff --git a/crates/networking/src/handlers/kadmelia.rs b/crates/networking/src/handlers/kadmelia.rs
deleted file mode 100644
index fec6a8a39..000000000
--- a/crates/networking/src/handlers/kadmelia.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use crate::gossip::NetworkService;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    async fn handle_kadmelia_event(&mut self, event: libp2p::kad::Event) {
-        // TODO: Handle kadmelia events
-        gadget_logging::trace!("Kadmelia event: {event:?}");
-    }
-}
diff --git a/crates/networking/src/handlers/mdns.rs b/crates/networking/src/handlers/mdns.rs
deleted file mode 100644
index 9bdd1ac8d..000000000
--- a/crates/networking/src/handlers/mdns.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use crate::gossip::NetworkService;
-use libp2p::mdns;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub(crate) async fn handle_mdns_event(&mut self, event: mdns::Event) {
-        use mdns::Event::{Discovered, Expired};
-        match event {
-            Discovered(list) => {
-                for (peer_id, multiaddr) in list {
-                    gadget_logging::trace!("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) {
-                        gadget_logging::error!("Failed to dial peer: {err}");
-                    }
-                }
-            }
-            Expired(list) => {
-                for (peer_id, multiaddr) in list {
-                    gadget_logging::trace!("discover peer has expired: {peer_id} with {multiaddr}");
-                    self.swarm
-                        .behaviour_mut()
-                        .gossipsub
-                        .remove_explicit_peer(&peer_id);
-                }
-            }
-        }
-    }
-}
diff --git a/crates/networking/src/handlers/mod.rs b/crates/networking/src/handlers/mod.rs
index 63b39123e..4e5668a5d 100644
--- a/crates/networking/src/handlers/mod.rs
+++ b/crates/networking/src/handlers/mod.rs
@@ -1,10 +1,3 @@
-#[cfg(not(target_family = "wasm"))]
-pub mod connections;
-pub mod dcutr;
-pub mod gossip;
-pub mod identify;
-pub mod kadmelia;
-pub mod mdns;
-pub mod p2p;
+pub mod blueprint_protocol;
+pub mod discovery;
 pub mod ping;
-pub mod relay;
diff --git a/crates/networking/src/handlers/p2p.rs b/crates/networking/src/handlers/p2p.rs
deleted file mode 100644
index 7dd5349a9..000000000
--- a/crates/networking/src/handlers/p2p.rs
+++ /dev/null
@@ -1,205 +0,0 @@
-#![allow(unused_results)]
-
-use crate::gossip::{MyBehaviourRequest, MyBehaviourResponse, NetworkService};
-use crate::key_types::Curve;
-use gadget_crypto::KeyType;
-use gadget_std::string::ToString;
-use gadget_std::sync::atomic::Ordering;
-use libp2p::gossipsub::IdentTopic;
-use libp2p::{request_response, PeerId};
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub(crate) async fn handle_p2p(
-        &mut self,
-        event: request_response::Event<MyBehaviourRequest, MyBehaviourResponse>,
-    ) {
-        use request_response::Event::{InboundFailure, Message, OutboundFailure, ResponseSent};
-        match event {
-            Message {
-                peer,
-                message,
-                connection_id: _,
-            } => {
-                gadget_logging::trace!("Received P2P message from: {peer}");
-                self.handle_p2p_message(peer, message).await;
-            }
-            OutboundFailure {
-                peer,
-                request_id,
-                error,
-                connection_id: _,
-            } => {
-                gadget_logging::error!("Failed to send message to peer: {peer} with request_id: {request_id} and error: {error}");
-            }
-            InboundFailure {
-                peer,
-                request_id,
-                error,
-                connection_id: _,
-            } => {
-                gadget_logging::error!("Failed to receive message from peer: {peer} with request_id: {request_id} and error: {error}");
-            }
-            ResponseSent {
-                peer,
-                request_id,
-                connection_id: _,
-            } => {
-                gadget_logging::debug!(
-                    "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::{Request, Response};
-        match message {
-            Request {
-                request,
-                channel,
-                request_id,
-            } => {
-                gadget_logging::trace!(
-                    "Received request with request_id: {request_id} from peer: {peer}"
-                );
-                self.handle_p2p_request(peer, request_id, request, channel)
-                    .await;
-            }
-            Response {
-                response,
-                request_id,
-            } => {
-                gadget_logging::trace!(
-                    "Received response from peer: {peer} with request_id: {request_id}"
-                );
-                self.handle_p2p_response(peer, request_id, response).await;
-            }
-        }
-    }
-
-    #[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 crate::gossip::MyBehaviourRequest::{Handshake, Message};
-        let result = match req {
-            Handshake {
-                public_key,
-                signature,
-            } => {
-                gadget_logging::trace!("Received handshake from peer: {peer}");
-                // Verify the signature
-                let msg = peer.to_bytes();
-                let valid = <Curve as KeyType>::verify(&public_key, &msg, &signature);
-                if !valid {
-                    gadget_logging::warn!("Invalid initial handshake signature from peer: {peer}");
-                    let _ = self.swarm.disconnect_peer_id(peer);
-                    return;
-                }
-                if self
-                    .public_key_to_libp2p_id
-                    .write()
-                    .await
-                    .insert(public_key, peer)
-                    .is_none()
-                {
-                    let _ = self.connected_peers.fetch_add(1, Ordering::Relaxed);
-                }
-                // Send response with our public key
-                let my_peer_id = self.swarm.local_peer_id();
-                let msg = my_peer_id.to_bytes();
-                match <Curve as KeyType>::sign_with_secret(&mut self.secret_key.clone(), &msg) {
-                    Ok(signature) => self.swarm.behaviour_mut().p2p.send_response(
-                        channel,
-                        MyBehaviourResponse::Handshaked {
-                            public_key: self.secret_key.public(),
-                            signature,
-                        },
-                    ),
-                    Err(e) => {
-                        gadget_logging::error!("Failed to sign message: {e}");
-                        return;
-                    }
-                }
-            }
-            Message { topic, raw_payload } => {
-                // Reject messages from self
-                if peer == self.my_id {
-                    return;
-                }
-
-                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) {
-                        gadget_logging::warn!("Failed to send message to worker: {e}");
-                    }
-                } else {
-                    gadget_logging::error!("No registered worker for topic: {topic}!");
-                }
-                self.swarm
-                    .behaviour_mut()
-                    .p2p
-                    .send_response(channel, MyBehaviourResponse::MessageHandled)
-            }
-        };
-        if result.is_err() {
-            gadget_logging::error!("Failed to send response for {request_id}");
-        }
-    }
-
-    #[tracing::instrument(skip(self, message))]
-    async fn handle_p2p_response(
-        &mut self,
-        peer: PeerId,
-        request_id: request_response::OutboundRequestId,
-        message: MyBehaviourResponse,
-    ) {
-        use crate::gossip::MyBehaviourResponse::{Handshaked, MessageHandled};
-        match message {
-            Handshaked {
-                public_key,
-                signature,
-            } => {
-                gadget_logging::trace!("Received handshake-ack message from peer: {peer}");
-                let msg = peer.to_bytes();
-                let valid = <Curve as KeyType>::verify(&public_key, &msg, &signature);
-                if !valid {
-                    gadget_logging::warn!(
-                        "Invalid handshake-acknowledgement signature from peer: {peer}"
-                    );
-                    // TODO: report this peer.
-                    self.public_key_to_libp2p_id
-                        .write()
-                        .await
-                        .remove(&public_key);
-                    let _ = self.swarm.disconnect_peer_id(peer);
-                    return;
-                }
-                if self
-                    .public_key_to_libp2p_id
-                    .write()
-                    .await
-                    .insert(public_key, peer)
-                    .is_none()
-                {
-                    let _ = self.connected_peers.fetch_add(1, Ordering::Relaxed);
-                }
-            }
-            MessageHandled => {}
-        }
-    }
-}
diff --git a/crates/networking/src/handlers/ping.rs b/crates/networking/src/handlers/ping.rs
index b669bc98a..8b1378917 100644
--- a/crates/networking/src/handlers/ping.rs
+++ b/crates/networking/src/handlers/ping.rs
@@ -1,8 +1,1 @@
-use crate::gossip::NetworkService;
 
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub async fn handle_ping_event(&mut self, event: libp2p::ping::Event) {
-        gadget_logging::trace!("Ping event: {event:?}");
-    }
-}
diff --git a/crates/networking/src/handlers/relay.rs b/crates/networking/src/handlers/relay.rs
deleted file mode 100644
index 320132086..000000000
--- a/crates/networking/src/handlers/relay.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-use crate::gossip::NetworkService;
-
-impl NetworkService<'_> {
-    #[tracing::instrument(skip(self, event))]
-    pub async fn handle_relay_event(&mut self, event: libp2p::relay::Event) {
-        gadget_logging::trace!("Relay event: {event:?}");
-    }
-}
diff --git a/crates/networking/src/lib.rs b/crates/networking/src/lib.rs
index 8e9120a17..3656cbd36 100644
--- a/crates/networking/src/lib.rs
+++ b/crates/networking/src/lib.rs
@@ -1,21 +1,20 @@
 #![cfg_attr(docsrs, feature(doc_auto_cfg))]
 
-pub mod gossip;
-pub mod handlers;
-pub mod messaging;
-pub mod networking;
-#[cfg(feature = "round-based-compat")]
-pub mod round_based_compat;
-#[cfg(feature = "round-based-compat")]
-pub use round_based;
-
+pub mod behaviours;
+pub mod blueprint_protocol;
+pub mod discovery;
 pub mod error;
-pub mod setup;
+pub mod handlers;
+pub mod service;
+pub mod service_handle;
+pub mod types;
 
-/// Unique identifier for a party
-pub type UserID = u16;
+#[cfg(test)]
+mod tests;
 
+pub use gadget_crypto::KeyType;
 pub use key_types::*;
+pub use service::{NetworkConfig, NetworkEvent, NetworkService};
 
 #[cfg(all(
     feature = "sp-core-ecdsa",
@@ -24,8 +23,8 @@ pub use key_types::*;
 ))]
 pub mod key_types {
     pub use gadget_crypto::sp_core::{
-        SpEcdsa as Curve, SpEcdsaPair as GossipMsgKeyPair, SpEcdsaPublic as GossipMsgPublicKey,
-        SpEcdsaSignature as GossipSignedMsgSignature,
+        SpEcdsa as Curve, SpEcdsaPair as InstanceMsgKeyPair, SpEcdsaPublic as InstanceMsgPublicKey,
+        SpEcdsaSignature as InstanceSignedMsgSignature,
     };
 }
 
@@ -36,8 +35,8 @@ pub mod key_types {
 ))]
 pub mod key_types {
     pub use gadget_crypto::sp_core::{
-        SpSr25519 as Curve, SpSr25519Pair as GossipMsgKeyPair,
-        SpSr25519Public as GossipMsgPublicKey, SpSr25519Signature as GossipSignedMsgSignature,
+        SpSr25519 as Curve, SpSr25519Pair as InstanceMsgKeyPair,
+        SpSr25519Public as InstanceMsgPublicKey, SpSr25519Signature as InstanceSignedMsgSignature,
     };
 }
 
@@ -48,8 +47,8 @@ pub mod key_types {
 ))]
 pub mod key_types {
     pub use gadget_crypto::sp_core::{
-        SpEd25519 as Curve, SpEd25519Pair as GossipMsgKeyPair,
-        SpEd25519Public as GossipMsgPublicKey, SpEd25519Signature as GossipSignedMsgSignature,
+        SpEd25519 as Curve, SpEd25519Pair as InstanceMsgKeyPair,
+        SpEd25519Public as InstanceMsgPublicKey, SpEd25519Signature as InstanceSignedMsgSignature,
     };
 }
 
@@ -61,8 +60,8 @@ pub mod key_types {
 pub mod key_types {
     // Default to k256 ECDSA implementation
     pub use gadget_crypto::k256::{
-        K256Ecdsa as Curve, K256Signature as GossipSignedMsgSignature,
-        K256SigningKey as GossipMsgKeyPair, K256VerifyingKey as GossipMsgPublicKey,
+        K256Ecdsa as Curve, K256Signature as InstanceSignedMsgSignature,
+        K256SigningKey as InstanceMsgKeyPair, K256VerifyingKey as InstanceMsgPublicKey,
     };
 }
 
diff --git a/crates/networking/src/messaging.rs b/crates/networking/src/messaging.rs
deleted file mode 100644
index c4a0dd6c1..000000000
--- a/crates/networking/src/messaging.rs
+++ /dev/null
@@ -1,468 +0,0 @@
-use async_trait::async_trait;
-use gadget_std as std;
-use gadget_std::boxed::Box;
-use gadget_std::collections::HashMap;
-use gadget_std::fmt::Display;
-use gadget_std::hash::Hash;
-use gadget_std::ops::Add;
-use gadget_std::sync::atomic::AtomicBool;
-use gadget_std::sync::Arc;
-use gadget_std::{eprintln, string::String, vec::Vec};
-use itertools::Itertools;
-use serde::{Deserialize, Serialize};
-use tokio::sync::RwLock;
-use tokio::time::{sleep, Duration};
-
-const OUTBOUND_POLL: Duration = Duration::from_millis(100);
-const INBOUND_POLL: Duration = Duration::from_millis(100);
-
-#[async_trait]
-pub trait MessageMetadata {
-    type JobId: Display + Hash + Eq + Copy + Send + Sync + 'static;
-    type PeerId: Display + Hash + Eq + Copy + Send + Sync + 'static;
-    type MessageId: Add<usize, Output = Self::MessageId>
-        + Eq
-        + PartialEq
-        + Display
-        + Hash
-        + Ord
-        + PartialOrd
-        + Copy
-        + Send
-        + Sync
-        + 'static;
-
-    fn job_id(&self) -> Self::JobId;
-    fn source_id(&self) -> Self::PeerId;
-    fn destination_id(&self) -> Self::PeerId;
-    fn message_id(&self) -> Self::MessageId;
-    fn contents(&self) -> &[u8];
-}
-
-#[async_trait]
-pub trait NetworkMessagingIO {
-    type Message: MessageMetadata + Send + Sync + 'static;
-
-    async fn next_message(&self) -> Option<Payload<Self::Message>>;
-    async fn send_message(&self, message: &Payload<Self::Message>) -> Result<(), NetworkError>;
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub enum Payload<M: MessageMetadata> {
-    Ack {
-        job_id: M::JobId,
-        from_id: M::PeerId,
-        message_id: M::MessageId,
-    },
-    Message(M),
-}
-
-#[derive(Debug)]
-pub enum NetworkError {
-    SendFailed(String),
-    ConnectionError(String),
-}
-
-#[derive(Debug)]
-pub enum BackendError {
-    StorageError(String),
-    NotFound,
-    Stopped,
-}
-
-#[derive(Debug, Copy, Clone)]
-pub enum DeliveryError {
-    NoReceiver,
-    ChannelClosed,
-}
-
-// Modified Backend trait to handle both outbound and inbound messages
-#[async_trait]
-pub trait Backend<M: MessageMetadata> {
-    async fn store_outbound(&self, message: M) -> Result<(), BackendError>;
-    async fn store_inbound(&self, message: M) -> Result<(), BackendError>;
-    async fn clear_message(
-        &self,
-        peer_id: M::PeerId,
-        job_id: M::JobId,
-        message_id: M::MessageId,
-    ) -> Result<(), BackendError>;
-    async fn get_pending_outbound(&self) -> Result<Vec<M>, BackendError>;
-    async fn get_pending_inbound(&self) -> Result<Vec<M>, BackendError>;
-}
-
-#[async_trait]
-pub trait LocalDelivery<M: MessageMetadata> {
-    async fn deliver(&self, message: M) -> Result<(), DeliveryError>;
-}
-
-// Add this new struct to track last ACKed message IDs
-pub struct MessageTracker<M: MessageMetadata> {
-    last_acked: HashMap<(M::JobId, M::PeerId), M::MessageId>,
-}
-
-impl<M: MessageMetadata> MessageTracker<M> {
-    fn new() -> Self {
-        Self {
-            last_acked: HashMap::new(),
-        }
-    }
-
-    fn update_ack(&mut self, job_id: M::JobId, peer_id: M::PeerId, msg_id: M::MessageId) {
-        let key = (job_id, peer_id);
-        match self.last_acked.get(&key) {
-            Some(last_id) => {
-                if msg_id == *last_id + 1usize {
-                    let _ = self.last_acked.insert(key, msg_id);
-                }
-            }
-            None => {
-                // For the first message in a sequence, only accept if it's the initial message
-                let _ = self.last_acked.insert(key, msg_id);
-            }
-        }
-    }
-
-    fn can_send(&self, job_id: &M::JobId, peer_id: &M::PeerId, msg_id: &M::MessageId) -> bool {
-        match self.last_acked.get(&(*job_id, *peer_id)) {
-            Some(last_id) => *msg_id == *last_id + 1usize,
-            None => true, // If there is no message for this job/peer_id combo, send it
-        }
-    }
-}
-
-pub struct MessageSystem<M, B, L, N>
-where
-    M: MessageMetadata + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
-    B: Backend<M> + Send + Sync + 'static,
-    L: LocalDelivery<M> + Send + Sync + 'static,
-    N: NetworkMessagingIO<Message = M> + Send + Sync + 'static,
-{
-    backend: Arc<B>,
-    local_delivery: Arc<L>,
-    network: Arc<N>,
-    is_running: Arc<AtomicBool>,
-    tracker: Arc<RwLock<MessageTracker<M>>>,
-}
-
-impl<M, B, L, N> Clone for MessageSystem<M, B, L, N>
-where
-    M: MessageMetadata + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
-    B: Backend<M> + Send + Sync + 'static,
-    L: LocalDelivery<M> + Send + Sync + 'static,
-    N: NetworkMessagingIO<Message = M> + Send + Sync + 'static,
-{
-    fn clone(&self) -> Self {
-        Self {
-            backend: self.backend.clone(),
-            local_delivery: self.local_delivery.clone(),
-            network: self.network.clone(),
-            is_running: self.is_running.clone(),
-            tracker: self.tracker.clone(),
-        }
-    }
-}
-
-impl<M, B, L, N> MessageSystem<M, B, L, N>
-where
-    M: MessageMetadata + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static,
-    B: Backend<M> + Send + Sync + 'static,
-    L: LocalDelivery<M> + Send + Sync + 'static,
-    N: NetworkMessagingIO<Message = M> + Send + Sync + 'static,
-{
-    pub fn new(backend: B, local_delivery: L, network: N) -> Self {
-        let this = Self {
-            backend: Arc::new(backend),
-            local_delivery: Arc::new(local_delivery),
-            network: Arc::new(network),
-            is_running: Arc::new(AtomicBool::new(true)),
-            tracker: Arc::new(RwLock::new(MessageTracker::new())),
-        };
-
-        this.spawn_background_tasks();
-
-        this
-    }
-
-    fn spawn_background_tasks(&self) {
-        // Spawn outbound processing task
-        let self_clone = self.clone();
-        let is_alive = self.is_running.clone();
-
-        let outbound_handle = tokio::spawn(async move {
-            loop {
-                self_clone.process_outbound().await;
-                sleep(OUTBOUND_POLL).await;
-            }
-        });
-
-        // Spawn inbound processing task
-        let self_clone = self.clone();
-        let inbound_handle = tokio::spawn(async move {
-            loop {
-                self_clone.process_inbound().await;
-                sleep(INBOUND_POLL).await;
-            }
-        });
-
-        // Spawn network listener task
-        let self_clone = self.clone();
-        let network_io_handle = tokio::spawn(async move {
-            self_clone.process_network_messages().await;
-        });
-
-        // Spawn a task that selects all three handles, and on any of them finishing, it will
-        // set the atomic bool to false
-        drop(tokio::spawn(async move {
-            tokio::select! {
-                _ = outbound_handle => {
-                    gadget_logging::error!("Outbound processing task prematurely ended");
-                },
-                _ = inbound_handle => {
-                    gadget_logging::error!("Inbound processing task prematurely ended");
-                },
-                _ = network_io_handle => {
-                    gadget_logging::error!("Network IO task prematurely ended");
-                },
-            }
-
-            is_alive.store(false, gadget_std::sync::atomic::Ordering::Relaxed);
-        }));
-    }
-
-    async fn process_outbound(&self) {
-        let pending_messages = match self.backend.get_pending_outbound().await {
-            Ok(messages) => messages,
-            Err(e) => {
-                eprintln!("Failed to get pending outbound messages: {:?}", e);
-                return;
-            }
-        };
-
-        // Group messages by (JobId, PeerId) pair
-        let mut grouped_messages: HashMap<(M::JobId, M::PeerId), Vec<M>> = HashMap::new();
-        for msg in pending_messages {
-            grouped_messages
-                .entry((msg.job_id(), msg.destination_id()))
-                .or_default()
-                .push(msg);
-        }
-
-        // Process each group independently
-        let tracker = self.tracker.read().await;
-        for ((job_id, peer_id), mut messages) in grouped_messages {
-            // Sort messages by MessageId
-            messages.sort_by_key(MessageMetadata::message_id);
-
-            // Find the first message we can send based on ACKs
-            if let Some(msg) = messages
-                .into_iter()
-                .find(|m| tracker.can_send(&job_id, &peer_id, &m.message_id()))
-            {
-                if let Err(e) = self.network.send_message(&Payload::Message(msg)).await {
-                    eprintln!("Failed to send message: {:?}", e);
-                }
-            }
-        }
-    }
-
-    async fn process_inbound(&self) {
-        let pending_messages = match self.backend.get_pending_inbound().await {
-            Ok(messages) => messages,
-            Err(e) => {
-                eprintln!("Failed to get pending inbound messages: {:?}", e);
-                return;
-            }
-        };
-
-        // Sort the pending messages in order by MessageID
-        let pending_messages: Vec<M> = pending_messages
-            .into_iter()
-            .sorted_by_key(MessageMetadata::message_id)
-            .collect();
-
-        for message in pending_messages {
-            match self.local_delivery.deliver(message.clone()).await {
-                Ok(()) => {
-                    // Create and send ACK
-                    if let Err(e) = self
-                        .network
-                        .send_message(&Self::create_ack_message(&message))
-                        .await
-                    {
-                        gadget_logging::error!("Failed to send ACK: {e:?}");
-                        continue;
-                    }
-
-                    // Clear delivered message from backend
-                    if let Err(e) = self
-                        .backend
-                        .clear_message(message.source_id(), message.job_id(), message.message_id())
-                        .await
-                    {
-                        gadget_logging::error!("Failed to clear delivered message: {e:?}");
-                    }
-                }
-                Err(e) => {
-                    gadget_logging::error!("Failed to deliver message: {e:?}");
-                }
-            }
-        }
-    }
-
-    // Modify process_network_messages to update the tracker
-    async fn process_network_messages(&self) {
-        loop {
-            if let Some(message) = self.network.next_message().await {
-                match message {
-                    Payload::Ack {
-                        job_id,
-                        from_id,
-                        message_id,
-                    } => {
-                        // Update the tracker with the new ACK
-                        let mut tracker = self.tracker.write().await;
-                        tracker.update_ack(job_id, from_id, message_id);
-
-                        if let Err(e) = self
-                            .backend
-                            .clear_message(from_id, job_id, message_id)
-                            .await
-                        {
-                            gadget_logging::error!("Failed to clear ACKed message: {e:?}");
-                        }
-                    }
-                    Payload::Message(msg) => {
-                        if let Err(e) = self.backend.store_inbound(msg).await {
-                            gadget_logging::error!("Failed to store inbound message: {e:?}");
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /// Send a message through the message system
-    ///
-    /// # Errors
-    ///
-    /// Returns `BackendError::Stopped` if the message system is not running.
-    /// May also return other `BackendError` variants if the backend storage operation fails.
-    pub async fn send_message(&self, message: M) -> Result<(), BackendError> {
-        if self.is_running.load(std::sync::atomic::Ordering::Relaxed) {
-            self.backend.store_outbound(message).await
-        } else {
-            Err(BackendError::Stopped)
-        }
-    }
-
-    fn create_ack_message(original_message: &M) -> Payload<M> {
-        Payload::Ack {
-            job_id: original_message.job_id(),
-            from_id: original_message.source_id(),
-            message_id: original_message.message_id(),
-        }
-    }
-}
-
-// Example InMemoryBackend implementation
-pub struct InMemoryBackend<M: MessageMetadata> {
-    outbound: Mailbox<M::JobId, M::PeerId, M::MessageId, M>,
-    inbound: Mailbox<M::JobId, M::PeerId, M::MessageId, M>,
-}
-
-type Mailbox<JobId, PeerId, MessageId, Message> =
-    Arc<RwLock<HashMap<(JobId, PeerId, MessageId), Message>>>;
-
-impl<M: MessageMetadata> InMemoryBackend<M> {
-    #[must_use]
-    pub fn new() -> Self {
-        Self {
-            outbound: Arc::new(RwLock::new(HashMap::new())),
-            inbound: Arc::new(RwLock::new(HashMap::new())),
-        }
-    }
-}
-
-impl<M: MessageMetadata> Default for InMemoryBackend<M> {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-#[async_trait]
-impl<M: MessageMetadata + Clone + Send + Sync + 'static> Backend<M> for InMemoryBackend<M> {
-    async fn store_outbound(&self, message: M) -> Result<(), BackendError> {
-        let mut outbound = self.outbound.write().await;
-        let (job_id, source_id, message_id) =
-            (message.job_id(), message.source_id(), message.message_id());
-
-        if outbound
-            .insert(
-                (
-                    message.job_id(),
-                    message.destination_id(),
-                    message.message_id(),
-                ),
-                message,
-            )
-            .is_some()
-        {
-            gadget_logging::warn!(
-                "Overwriting existing message in outbound storage jid={}/dest={}/id={}",
-                job_id,
-                source_id,
-                message_id
-            );
-        }
-        Ok(())
-    }
-
-    async fn store_inbound(&self, message: M) -> Result<(), BackendError> {
-        let mut inbound = self.inbound.write().await;
-        let (job_id, source_id, message_id) =
-            (message.job_id(), message.source_id(), message.message_id());
-
-        if inbound
-            .insert(
-                (message.job_id(), message.source_id(), message.message_id()),
-                message,
-            )
-            .is_some()
-        {
-            gadget_logging::warn!(
-                "Overwriting existing message in inbound storage jid={}/src={}/id={}",
-                job_id,
-                source_id,
-                message_id
-            );
-        }
-        Ok(())
-    }
-
-    async fn clear_message(
-        &self,
-        peer_id: M::PeerId,
-        job_id: M::JobId,
-        message_id: M::MessageId,
-    ) -> Result<(), BackendError> {
-        // Try to remove from both outbound and inbound
-        let mut outbound = self.outbound.write().await;
-        let mut inbound = self.inbound.write().await;
-
-        let _ = outbound.remove(&(job_id, peer_id, message_id));
-        let _ = inbound.remove(&(job_id, peer_id, message_id));
-
-        Ok(())
-    }
-
-    async fn get_pending_outbound(&self) -> Result<Vec<M>, BackendError> {
-        let outbound = self.outbound.read().await;
-        Ok(outbound.values().cloned().collect())
-    }
-
-    async fn get_pending_inbound(&self) -> Result<Vec<M>, BackendError> {
-        let inbound = self.inbound.read().await;
-        Ok(inbound.values().cloned().collect())
-    }
-}
diff --git a/crates/networking/src/networking.rs b/crates/networking/src/networking.rs
deleted file mode 100644
index f793f833e..000000000
--- a/crates/networking/src/networking.rs
+++ /dev/null
@@ -1,1357 +0,0 @@
-use crate::error::Error;
-use crate::key_types::GossipMsgPublicKey;
-use async_trait::async_trait;
-use dashmap::DashMap;
-use futures::{Stream, StreamExt};
-use gadget_crypto::hashing::blake3_256;
-use gadget_std as std;
-use gadget_std::boxed::Box;
-use gadget_std::cmp::Reverse;
-use gadget_std::collections::{BinaryHeap, HashMap};
-use gadget_std::fmt::Display;
-use gadget_std::format;
-use gadget_std::ops::{Deref, DerefMut};
-use gadget_std::pin::Pin;
-use gadget_std::string::ToString;
-use gadget_std::sync::Arc;
-use gadget_std::task::{Context, Poll};
-use gadget_std::vec::Vec;
-use serde::{Deserialize, Serialize};
-use tokio::sync::mpsc::UnboundedSender;
-use tokio::sync::Mutex;
-use tracing::trace;
-
-pub type UserID = u16;
-
-#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)]
-pub struct IdentifierInfo {
-    pub message_id: u64,
-    pub round_id: u16,
-}
-
-impl Display for IdentifierInfo {
-    fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
-        let message_id = format!("message_id: {}", self.message_id);
-        let round_id = format!("round_id: {}", self.round_id);
-        write!(f, "{} {}", message_id, round_id)
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
-pub struct ParticipantInfo {
-    pub user_id: u16,
-    pub public_key: Option<GossipMsgPublicKey>,
-}
-
-impl Display for ParticipantInfo {
-    fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
-        let public_key = self
-            .public_key
-            .map(|key| format!("public_key: {:?}", key))
-            .unwrap_or_default();
-        write!(f, "user_id: {}, {}", self.user_id, public_key)
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct ProtocolMessage {
-    pub identifier_info: IdentifierInfo,
-    pub sender: ParticipantInfo,
-    pub recipient: Option<ParticipantInfo>,
-    pub payload: Vec<u8>,
-}
-
-impl Display for ProtocolMessage {
-    fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
-        write!(
-            f,
-            "identifier_info: {}, sender: {}, recipient: {:?}, payload: {:?}",
-            self.identifier_info, self.sender, self.recipient, self.payload
-        )
-    }
-}
-
-#[async_trait]
-#[auto_impl::auto_impl(&, Box, Arc)]
-pub trait Network: Send + Sync + 'static {
-    async fn next_message(&self) -> Option<ProtocolMessage>;
-    async fn send_message(&self, message: ProtocolMessage) -> Result<(), Error>;
-
-    fn public_id(&self) -> GossipMsgPublicKey;
-
-    fn build_protocol_message<Payload: Serialize>(
-        &self,
-        identifier_info: IdentifierInfo,
-        from: UserID,
-        to: Option<UserID>,
-        payload: &Payload,
-        to_network_id: Option<GossipMsgPublicKey>,
-    ) -> ProtocolMessage {
-        assert!(
-            (u8::from(to.is_none()) + u8::from(to_network_id.is_none()) != 1),
-            "Either `to` must be Some AND `to_network_id` is Some, or, both None"
-        );
-
-        let sender_participant_info = ParticipantInfo {
-            user_id: from,
-            public_key: Some(self.public_id()),
-        };
-        let receiver_participant_info = to.map(|to| ParticipantInfo {
-            user_id: to,
-            public_key: to_network_id,
-        });
-        ProtocolMessage {
-            identifier_info,
-            sender: sender_participant_info,
-            recipient: receiver_participant_info,
-            payload: bincode::serialize(payload).expect("Failed to serialize message"),
-        }
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct SequencedMessage {
-    seq: u64,
-    payload: Vec<u8>,
-}
-
-#[derive(Debug)]
-struct PendingMessage {
-    seq: u64,
-    message: ProtocolMessage,
-}
-
-impl PartialEq for PendingMessage {
-    fn eq(&self, other: &Self) -> bool {
-        self.seq == other.seq
-    }
-}
-
-impl Eq for PendingMessage {}
-
-impl PartialOrd for PendingMessage {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for PendingMessage {
-    fn cmp(&self, other: &Self) -> gadget_std::cmp::Ordering {
-        self.seq.cmp(&other.seq)
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-pub struct MultiplexedMessage {
-    stream_id: StreamKey,
-    payload: SequencedMessage,
-}
-
-pub struct NetworkMultiplexer {
-    to_receiving_streams: ActiveStreams,
-    unclaimed_receiving_streams: Arc<DashMap<StreamKey, MultiplexedReceiver>>,
-    tx_to_networking_layer: MultiplexedSender,
-    sequence_numbers: Arc<DashMap<CompoundStreamKey, u64>>,
-    my_id: GossipMsgPublicKey,
-}
-
-type ActiveStreams = Arc<DashMap<StreamKey, UnboundedSender<ProtocolMessage>>>;
-
-#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Default)]
-pub struct StreamKey {
-    pub task_hash: [u8; 32],
-    pub round_id: i32,
-}
-
-impl From<IdentifierInfo> for StreamKey {
-    fn from(identifier_info: IdentifierInfo) -> Self {
-        let str_repr = identifier_info.to_string();
-        let task_hash = blake3_256(str_repr.as_bytes());
-        Self {
-            task_hash,
-            round_id: -1,
-        }
-    }
-}
-
-pub struct MultiplexedReceiver {
-    inner: tokio::sync::mpsc::UnboundedReceiver<ProtocolMessage>,
-    stream_id: StreamKey,
-    // For post-drop removal purposes
-    active_streams: ActiveStreams,
-}
-
-#[derive(Clone)]
-pub struct MultiplexedSender {
-    inner: tokio::sync::mpsc::UnboundedSender<(StreamKey, ProtocolMessage)>,
-    pub(crate) stream_id: StreamKey,
-}
-
-impl MultiplexedSender {
-    /// Sends a protocol message through the multiplexed channel.
-    ///
-    /// # Arguments
-    /// * `message` - The protocol message to send
-    ///
-    /// # Returns
-    /// * `Ok(())` - If the message was successfully sent
-    /// * `Err(Error)` - If there was an error sending the message
-    ///
-    /// # Errors
-    /// Returns an error if the receiving end of the channel has been closed,
-    /// indicating that the network connection is no longer available.
-    pub fn send(&self, message: ProtocolMessage) -> Result<(), Error> {
-        self.inner
-            .send((self.stream_id, message))
-            .map_err(|err| Error::Other(err.to_string()))
-    }
-}
-
-impl Stream for MultiplexedReceiver {
-    type Item = ProtocolMessage;
-
-    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
-        Pin::new(&mut self.get_mut().inner).poll_recv(cx)
-    }
-}
-
-impl Deref for MultiplexedReceiver {
-    type Target = tokio::sync::mpsc::UnboundedReceiver<ProtocolMessage>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.inner
-    }
-}
-
-impl DerefMut for MultiplexedReceiver {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.inner
-    }
-}
-
-impl Drop for MultiplexedReceiver {
-    fn drop(&mut self) {
-        let _ = self.active_streams.remove(&self.stream_id);
-    }
-}
-
-// Since a single stream can be used for multiple users, and, multiple users assign seq's independently,
-// we need to make a key that is unique for each (send->dest) pair and stream.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct CompoundStreamKey {
-    stream_key: StreamKey,
-    send_user: UserID,
-    recv_user: Option<UserID>,
-}
-
-impl NetworkMultiplexer {
-    /// Creates a new `NetworkMultiplexer` instance.
-    ///
-    /// # Arguments
-    /// * `network` - The underlying network implementation that implements the Network trait
-    ///
-    /// # Type Parameters
-    /// * `N` - The network type that implements the Network trait
-    ///
-    /// # Returns
-    /// * `Self` - A new `NetworkMultiplexer` instance
-    ///
-    /// # Panics
-    /// This function will panic if the internal receiver has already been taken, which should not happen.
-    #[allow(clippy::too_many_lines)]
-    pub fn new<N: Network>(network: N) -> Self {
-        let (tx_to_networking_layer, mut rx_from_substreams) =
-            tokio::sync::mpsc::unbounded_channel();
-        let my_id = network.public_id();
-        let this = NetworkMultiplexer {
-            to_receiving_streams: Arc::new(DashMap::new()),
-            unclaimed_receiving_streams: Arc::new(DashMap::new()),
-            tx_to_networking_layer: MultiplexedSender {
-                inner: tx_to_networking_layer,
-                stream_id: StreamKey::default(),
-            },
-            sequence_numbers: Arc::new(DashMap::new()),
-            my_id,
-        };
-
-        let active_streams = this.to_receiving_streams.clone();
-        let unclaimed_streams = this.unclaimed_receiving_streams.clone();
-        let tx_to_networking_layer = this.tx_to_networking_layer.clone();
-        let sequence_numbers = this.sequence_numbers.clone();
-
-        drop(tokio::spawn(async move {
-            let network_clone = &network;
-
-            let task1 = async move {
-                while let Some((stream_id, msg)) = rx_from_substreams.recv().await {
-                    let compound_key = CompoundStreamKey {
-                        stream_key: stream_id,
-                        send_user: msg.sender.user_id,
-                        recv_user: msg.recipient.as_ref().map(|p| p.user_id),
-                    };
-
-                    let mut seq = sequence_numbers.entry(compound_key).or_insert(0);
-                    let current_seq = *seq;
-                    *seq += 1;
-
-                    trace!(
-                        "SEND SEQ {current_seq} FROM {} | StreamKey: {:?}",
-                        msg.sender.user_id,
-                        hex::encode(bincode::serialize(&compound_key).unwrap())
-                    );
-
-                    let multiplexed_message = MultiplexedMessage {
-                        stream_id,
-                        payload: SequencedMessage {
-                            seq: current_seq,
-                            payload: msg.payload,
-                        },
-                    };
-
-                    let message = ProtocolMessage {
-                        identifier_info: msg.identifier_info,
-                        sender: msg.sender,
-                        recipient: msg.recipient,
-                        payload: bincode::serialize(&multiplexed_message)
-                            .expect("Failed to serialize message"),
-                    };
-
-                    if let Err(err) = network_clone.send_message(message).await {
-                        gadget_logging::error!("Failed to send message to network: {err:?}");
-                        break;
-                    }
-                }
-            };
-
-            let task2 = async move {
-                let mut pending_messages: HashMap<
-                    CompoundStreamKey,
-                    BinaryHeap<Reverse<PendingMessage>>,
-                > = HashMap::default();
-                let mut expected_seqs: HashMap<CompoundStreamKey, u64> = HashMap::default();
-
-                while let Some(mut msg) = network_clone.next_message().await {
-                    if let Some(recv) = msg.recipient.as_ref() {
-                        if let Some(recv_pk) = &recv.public_key {
-                            if recv_pk != &my_id {
-                                gadget_logging::warn!(
-                                    "Received a message not intended for the local user"
-                                );
-                            }
-                        }
-                    }
-
-                    if let Ok(multiplexed_message) =
-                        bincode::deserialize::<MultiplexedMessage>(&msg.payload)
-                    {
-                        let stream_id = multiplexed_message.stream_id;
-                        let compound_key = CompoundStreamKey {
-                            stream_key: stream_id,
-                            send_user: msg.sender.user_id,
-                            recv_user: msg.recipient.as_ref().map(|p| p.user_id),
-                        };
-                        let seq = multiplexed_message.payload.seq;
-                        msg.payload = multiplexed_message.payload.payload;
-
-                        // Get or create the pending heap for this stream
-                        let pending = pending_messages.entry(compound_key).or_default();
-                        let expected_seq = expected_seqs.entry(compound_key).or_default();
-
-                        let send_user = msg.sender.user_id;
-                        let recv_user = msg.recipient.as_ref().map_or(-1, |p| i32::from(p.user_id));
-                        let compound_key_hex =
-                            hex::encode(bincode::serialize(&compound_key).unwrap());
-                        trace!(
-                            "RECV SEQ {seq} FROM {} as user {:?} | Expecting: {} | StreamKey: {:?}",
-                            send_user,
-                            recv_user,
-                            *expected_seq,
-                            compound_key_hex,
-                        );
-
-                        // Add the message to pending
-                        pending.push(Reverse(PendingMessage { seq, message: msg }));
-
-                        // Try to deliver messages in order
-                        if let Some(active_receiver) = active_streams.get(&stream_id) {
-                            while let Some(Reverse(PendingMessage { seq, message: _ })) =
-                                pending.peek()
-                            {
-                                if *seq != *expected_seq {
-                                    break;
-                                }
-
-                                trace!("DELIVERING SEQ {seq} FROM {} as user {:?} | Expecting: {} | StreamKey: {:?}", send_user, recv_user, *expected_seq, compound_key_hex);
-
-                                *expected_seq += 1;
-
-                                let message = pending.pop().unwrap().0.message;
-
-                                if let Err(err) = active_receiver.send(message) {
-                                    gadget_logging::error!(%err, "Failed to send message to receiver");
-                                    let _ = active_streams.remove(&stream_id);
-                                    break;
-                                }
-                            }
-                        } else {
-                            let (tx, rx) = Self::create_multiplexed_stream_inner(
-                                tx_to_networking_layer.clone(),
-                                &active_streams,
-                                stream_id,
-                            );
-
-                            // Deliver any pending messages in order
-                            while let Some(Reverse(PendingMessage { seq, message: _ })) =
-                                pending.peek()
-                            {
-                                if *seq != *expected_seq {
-                                    break;
-                                }
-
-                                gadget_logging::warn!("EARLY DELIVERY SEQ {seq} FROM {} as user {:?} | Expecting: {} | StreamKey: {:?}", send_user, recv_user, *expected_seq, compound_key_hex);
-
-                                *expected_seq += 1;
-
-                                let message = pending.pop().unwrap().0.message;
-
-                                if let Err(err) = tx.send(message) {
-                                    gadget_logging::error!(%err, "Failed to send message to receiver");
-                                    break;
-                                }
-                            }
-
-                            let _ = unclaimed_streams.insert(stream_id, rx);
-                        }
-                    } else {
-                        gadget_logging::error!("Failed to deserialize message (networking)");
-                    }
-                }
-            };
-
-            tokio::select! {
-                () = task1 => {
-                    gadget_logging::error!("Task 1 exited");
-                },
-                () = task2 => {
-                    gadget_logging::error!("Task 2 exited");
-                }
-            }
-        }));
-
-        this
-    }
-
-    /// Creates a new multiplexed stream.
-    ///
-    /// # Arguments
-    /// * `id` - The ID of the stream to create
-    ///
-    /// # Returns
-    /// * `Self` - A new multiplexed stream
-    pub fn multiplex(&self, id: impl Into<StreamKey>) -> SubNetwork {
-        let id = id.into();
-        let my_id = self.my_id;
-        let mut tx_to_networking_layer = self.tx_to_networking_layer.clone();
-        if let Some(unclaimed) = self.unclaimed_receiving_streams.remove(&id) {
-            tx_to_networking_layer.stream_id = id;
-            return SubNetwork {
-                tx: tx_to_networking_layer,
-                rx: Some(unclaimed.1.into()),
-                my_id,
-            };
-        }
-
-        let (tx, rx) = Self::create_multiplexed_stream_inner(
-            tx_to_networking_layer,
-            &self.to_receiving_streams,
-            id,
-        );
-
-        SubNetwork {
-            tx,
-            rx: Some(rx.into()),
-            my_id,
-        }
-    }
-
-    /// Creates a subnetwork, and also forwards all messages to the given channel. The network cannot be used to
-    /// receive messages since the messages will be forwarded to the provided channel.
-    ///
-    /// # Panics
-    ///
-    /// This function will panic if the internal receiver has already been taken, which should not happen
-    /// under normal circumstances.
-    pub fn multiplex_with_forwarding(
-        &self,
-        id: impl Into<StreamKey>,
-        forward_tx: tokio::sync::mpsc::UnboundedSender<ProtocolMessage>,
-    ) -> SubNetwork {
-        let mut network = self.multiplex(id);
-        let rx = network.rx.take().expect("Rx from network should be Some");
-        let forwarding_task = async move {
-            let mut rx = rx.into_inner();
-            while let Some(msg) = rx.recv().await {
-                gadget_logging::info!(
-                    "Round {}: Received message from {} to {:?} (id: {})",
-                    msg.identifier_info.round_id,
-                    msg.sender.user_id,
-                    msg.recipient.as_ref().map(|p| p.user_id),
-                    msg.identifier_info.message_id,
-                );
-                if let Err(err) = forward_tx.send(msg) {
-                    gadget_logging::error!(%err, "Failed to forward message to network");
-                    // TODO: Add AtomicBool to make sending stop
-                    break;
-                }
-            }
-        };
-
-        drop(tokio::spawn(forwarding_task));
-
-        network
-    }
-
-    fn create_multiplexed_stream_inner(
-        mut tx_to_networking_layer: MultiplexedSender,
-        active_streams: &ActiveStreams,
-        stream_id: StreamKey,
-    ) -> (MultiplexedSender, MultiplexedReceiver) {
-        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
-        if active_streams.insert(stream_id, tx).is_some() {
-            gadget_logging::warn!(
-                "Stream ID {stream_id:?} already exists! Existing stream will be replaced"
-            );
-        }
-        tx_to_networking_layer.stream_id = stream_id;
-
-        (
-            tx_to_networking_layer,
-            MultiplexedReceiver {
-                inner: rx,
-                stream_id,
-                active_streams: active_streams.clone(),
-            },
-        )
-    }
-}
-
-impl<N: Network> From<N> for NetworkMultiplexer {
-    fn from(network: N) -> Self {
-        Self::new(network)
-    }
-}
-
-pub struct SubNetwork {
-    tx: MultiplexedSender,
-    rx: Option<Mutex<MultiplexedReceiver>>,
-    my_id: GossipMsgPublicKey,
-}
-
-impl SubNetwork {
-    /// Sends a protocol message through the subnetwork.
-    ///
-    /// # Arguments
-    /// * `message` - The protocol message to send
-    ///
-    /// # Returns
-    /// * `Ok(())` - If the message was successfully sent
-    /// * `Err(Error)` - If there was an error sending the message
-    ///
-    /// # Errors
-    /// * Returns an error if the underlying network connection is closed or unavailable
-    pub fn send(&self, message: ProtocolMessage) -> Result<(), Error> {
-        self.tx.send(message)
-    }
-
-    pub async fn recv(&self) -> Option<ProtocolMessage> {
-        self.rx.as_ref()?.lock().await.next().await
-    }
-}
-
-#[async_trait]
-impl Network for SubNetwork {
-    async fn next_message(&self) -> Option<ProtocolMessage> {
-        self.recv().await
-    }
-
-    async fn send_message(&self, message: ProtocolMessage) -> Result<(), Error> {
-        self.send(message)
-    }
-
-    fn public_id(&self) -> GossipMsgPublicKey {
-        self.my_id
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use self::std::time::Duration;
-    use super::*;
-    use crate::gossip::GossipHandle;
-    use futures::{stream, StreamExt};
-    use gadget_crypto::hashing::blake3_256;
-    use gadget_crypto::KeyType;
-    use gadget_logging::setup_log;
-    use gadget_std::collections::BTreeMap;
-    use gadget_std::sync::LazyLock;
-    use serde::{Deserialize, Serialize};
-    use tokio::time::sleep;
-
-    const TOPIC: &str = "/gadget/test/1.0.0";
-
-    fn deserialize<'a, T>(data: &'a [u8]) -> Result<T, crate::error::Error>
-    where
-        T: Deserialize<'a>,
-    {
-        bincode::deserialize(data).map_err(|err| Error::Other(err.to_string()))
-    }
-
-    #[derive(Debug, Serialize, Deserialize, Clone)]
-    enum Msg {
-        Round1(Round1Msg),
-        Round2(Round2Msg),
-        Round3(Round3Msg),
-    }
-
-    #[derive(Debug, Serialize, Deserialize, Clone)]
-    struct Round1Msg {
-        pub power: u16,
-        pub hitpoints: u16,
-        pub armor: u16,
-        pub name: String,
-    }
-
-    #[derive(Debug, Serialize, Deserialize, Clone)]
-    struct Round2Msg {
-        pub x: u16,
-        pub y: u16,
-        pub z: u16,
-    }
-
-    #[derive(Debug, Serialize, Deserialize, Clone)]
-    struct Round3Msg {
-        rotation: u16,
-        velocity: (u16, u16, u16),
-    }
-
-    async fn wait_for_nodes_connected(nodes: &[GossipHandle]) {
-        let node_count = nodes.len();
-
-        // wait for the nodes to connect to each other
-        let max_retries = 10 * node_count;
-        let mut retry = 0;
-        loop {
-            gadget_logging::debug!(%node_count, %max_retries, %retry, "Checking if all nodes are connected to each other");
-            let connected = nodes
-                .iter()
-                .map(super::super::gossip::GossipHandle::connected_peers)
-                .collect::<Vec<_>>();
-
-            let all_connected = connected
-                .iter()
-                .enumerate()
-                .inspect(|(node, peers)| {
-                    gadget_logging::debug!("Node {node} has {peers} connected peers");
-                })
-                .all(|(_, &peers)| peers >= node_count - 1);
-            if all_connected {
-                gadget_logging::debug!("All nodes are connected to each other");
-                return;
-            }
-            sleep(Duration::from_millis(300)).await;
-            retry += 1;
-            assert!(
-                retry <= max_retries,
-                "Failed to connect all nodes to each other"
-            );
-        }
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    #[allow(clippy::cast_possible_truncation)]
-    async fn test_p2p() {
-        setup_log();
-        let nodes = stream::iter(0..*NODE_COUNT)
-            .map(|_| node())
-            .collect::<Vec<_>>()
-            .await;
-
-        wait_for_nodes_connected(&nodes).await;
-
-        let mut mapping = BTreeMap::new();
-        for (i, node) in nodes.iter().enumerate() {
-            mapping.insert(i as u16, node.my_id);
-        }
-
-        let mut tasks = Vec::new();
-        for (i, node) in nodes.into_iter().enumerate() {
-            let task = tokio::spawn(run_protocol(node, i as u16, mapping.clone()));
-            tasks.push(task);
-        }
-        // Wait for all tasks to finish
-        let results = futures::future::try_join_all(tasks)
-            .await
-            .expect("Failed to run protocol");
-        // Assert that all are okay.
-        assert!(
-            results.iter().all(std::result::Result::is_ok),
-            "Some nodes failed to run protocol"
-        );
-    }
-
-    #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
-    async fn run_protocol<N: Network>(
-        node: N,
-        i: u16,
-        mapping: BTreeMap<u16, crate::GossipMsgPublicKey>,
-    ) -> Result<(), crate::error::Error> {
-        let task_hash = [0u8; 32];
-        // Safety note: We should be passed a NetworkMultiplexer, and all uses of the N: Network
-        // used throughout the program must also use the multiplexer to prevent mixed messages.
-        let multiplexer = NetworkMultiplexer::new(node);
-
-        let round1_network = multiplexer.multiplex(StreamKey {
-            task_hash, // To differentiate between different instances of a running program (i.e., a task)
-            round_id: 0, // To differentiate between different subsets of a running task
-        });
-
-        let round2_network = multiplexer.multiplex(StreamKey {
-            task_hash, // To differentiate between different instances of a running program (i.e., a task)
-            round_id: 1, // To differentiate between different subsets of a running task
-        });
-
-        let round3_network = multiplexer.multiplex(StreamKey {
-            task_hash, // To differentiate between different instances of a running program (i.e., a task)
-            round_id: 2, // To differentiate between different subsets of a running task
-        });
-
-        //let (round1_tx, round1_rx) = node.
-        // Round 1 (broadcast)
-        let msg = {
-            let round = Round1Msg {
-                power: i * 100,
-                hitpoints: (i + 1) * 50,
-                armor: i + 2,
-                name: format!("Player {}", i),
-            };
-            round1_network.build_protocol_message(
-                IdentifierInfo {
-                    message_id: 0,
-                    round_id: 0,
-                },
-                i,
-                None,
-                &Msg::Round1(round),
-                None,
-            )
-        };
-
-        gadget_logging::debug!("Broadcast Message");
-        round1_network
-            .send(msg)
-            .map_err(|_| crate::error::Error::Other("Failed to send message".into()))?;
-
-        // Wait for all other nodes to send their messages
-        let mut msgs = BTreeMap::new();
-        while let Some(msg) = round1_network.recv().await {
-            let m = deserialize::<Msg>(&msg.payload).unwrap();
-            gadget_logging::debug!(from = %msg.sender.user_id, ?m, "Received message");
-            // Expecting Round1 message
-            assert!(
-                matches!(m, Msg::Round1(_)),
-                "Expected Round1 message but got {:?} from node {}",
-                m,
-                msg.sender.user_id,
-            );
-            let old = msgs.insert(msg.sender.user_id, m);
-            assert!(
-                old.is_none(),
-                "Duplicate message from node {}",
-                msg.sender.user_id,
-            );
-            // Break if all messages are received
-            if msgs.len() == *NODE_COUNT - 1 {
-                break;
-            }
-        }
-        gadget_logging::debug!("Done r1 w/ {i}");
-
-        // Round 2 (P2P)
-        let msgs = (0..*NODE_COUNT)
-            .map(|r| r as u16)
-            .filter(|&j| j != i)
-            .map(|j| {
-                let peer_pk = mapping.get(&j).copied().unwrap();
-                round2_network.build_protocol_message(
-                    IdentifierInfo {
-                        message_id: 0,
-                        round_id: 0,
-                    },
-                    i,
-                    Some(j),
-                    &Msg::Round2(Round2Msg {
-                        x: i * 10,
-                        y: (i + 1) * 20,
-                        z: i + 2,
-                    }),
-                    Some(peer_pk),
-                )
-            })
-            .collect::<Vec<_>>();
-        for msg in msgs {
-            let to = msg.recipient.map(|r| r.user_id).expect(
-                "Recipient should be present for P2P message. This is a bug in the test code",
-            );
-            gadget_logging::debug!(%to, "Send P2P Message");
-            round2_network.send(msg)?;
-        }
-
-        // Wait for all other nodes to send their messages
-        let mut msgs = BTreeMap::new();
-        while let Some(msg) = round2_network.recv().await {
-            let m = deserialize::<Msg>(&msg.payload).unwrap();
-            gadget_logging::info!(
-                "[Node {}] Received message from {} | Intended Recipient: {}",
-                i,
-                msg.sender.user_id,
-                msg.recipient
-                    .as_ref()
-                    .map_or_else(|| "Broadcast".into(), |r| r.user_id.to_string())
-            );
-            // Expecting Round2 message
-            assert!(
-                matches!(m, Msg::Round2(_)),
-                "Expected Round2 message but got {:?} from node {}",
-                m,
-                msg.sender.user_id,
-            );
-            let old = msgs.insert(msg.sender.user_id, m);
-            assert!(
-                old.is_none(),
-                "Duplicate message from node {}",
-                msg.sender.user_id,
-            );
-            // Break if all messages are received
-            if msgs.len() == *NODE_COUNT - 1 {
-                break;
-            }
-        }
-        gadget_logging::debug!("Done r2 w/ {i}");
-
-        // Round 3 (broadcast)
-
-        let msg = {
-            let round = Round3Msg {
-                rotation: i * 30,
-                velocity: (i + 1, i + 2, i + 3),
-            };
-            round3_network.build_protocol_message(
-                IdentifierInfo {
-                    message_id: 0,
-                    round_id: 0,
-                },
-                i,
-                None,
-                &Msg::Round3(round),
-                None,
-            )
-        };
-
-        gadget_logging::debug!("Broadcast Message");
-        round3_network.send(msg)?;
-
-        // Wait for all other nodes to send their messages
-        let mut msgs = BTreeMap::new();
-        while let Some(msg) = round3_network.recv().await {
-            let m = deserialize::<Msg>(&msg.payload).unwrap();
-            gadget_logging::debug!(from = %msg.sender.user_id, ?m, "Received message");
-            // Expecting Round3 message
-            assert!(
-                matches!(m, Msg::Round3(_)),
-                "Expected Round3 message but got {:?} from node {}",
-                m,
-                msg.sender.user_id,
-            );
-            let old = msgs.insert(msg.sender.user_id, m);
-            assert!(
-                old.is_none(),
-                "Duplicate message from node {}",
-                msg.sender.user_id,
-            );
-            // Break if all messages are received
-            if msgs.len() == *NODE_COUNT - 1 {
-                break;
-            }
-        }
-        gadget_logging::debug!("Done r3 w/ {i}");
-
-        gadget_logging::info!(node = i, "Protocol completed");
-
-        Ok(())
-    }
-
-    fn node_with_id() -> (GossipHandle, crate::key_types::GossipMsgKeyPair) {
-        let identity = libp2p::identity::Keypair::generate_ed25519();
-        let crypto_key = crate::key_types::Curve::generate_with_seed(None).unwrap();
-        let bind_port = 0;
-        let handle =
-            crate::setup::start_p2p_network(crate::setup::NetworkConfig::new_service_network(
-                identity,
-                crypto_key.clone(),
-                Vec::default(),
-                bind_port,
-                TOPIC,
-            ))
-            .unwrap();
-
-        (handle, crypto_key)
-    }
-
-    fn node() -> GossipHandle {
-        node_with_id().0
-    }
-
-    static NODE_COUNT: LazyLock<usize> =
-        LazyLock::new(|| std::env::var("IN_CI").map_or_else(|_| 10, |_| 2));
-    #[allow(dead_code)]
-    static MESSAGE_COUNT: LazyLock<usize> =
-        LazyLock::new(|| std::env::var("IN_CI").map_or_else(|_| 10, |_| 100));
-
-    #[tokio::test(flavor = "multi_thread")]
-    #[allow(clippy::cast_possible_truncation)]
-    async fn test_stress_test_multiplexer() {
-        setup_log();
-        gadget_logging::info!("Starting test_stress_test_multiplexer");
-
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-
-        let _subnetwork0 = multiplexer0.multiplex(stream_key);
-        let _subnetwork1 = multiplexer1.multiplex(stream_key);
-
-        // Create a channel for forwarding
-        let (forward_tx, mut forward_rx) = tokio::sync::mpsc::unbounded_channel();
-
-        // Create a subnetwork with forwarding
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        let subnetwork1 = multiplexer1.multiplex_with_forwarding(stream_key, forward_tx);
-
-        let payload = StressTestPayload { value: 42 };
-        let msg = subnetwork0.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1),
-            &payload,
-            Some(subnetwork1.public_id()),
-        );
-
-        gadget_logging::info!("Sending message from subnetwork0");
-        subnetwork0.send(msg.clone()).unwrap();
-
-        // Message should be forwarded to the forward_rx channel
-        let forwarded_msg = forward_rx.recv().await.unwrap();
-        let received: StressTestPayload = deserialize(&forwarded_msg.payload).unwrap();
-        assert_eq!(received.value, payload.value);
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_nested_multiplexer() {
-        setup_log();
-        gadget_logging::info!("Starting test_nested_multiplexer");
-        let (network0, network1) = get_networks().await;
-
-        nested_multiplex(0, 10, network0, network1).await;
-    }
-
-    async fn get_networks() -> (GossipHandle, GossipHandle) {
-        let network0 = node();
-        let network1 = node();
-
-        let mut gossip_networks = vec![network0, network1];
-
-        wait_for_nodes_connected(&gossip_networks).await;
-
-        (gossip_networks.remove(0), gossip_networks.remove(0))
-    }
-
-    async fn nested_multiplex<N: Network>(
-        cur_depth: usize,
-        max_depth: usize,
-        network0: N,
-        network1: N,
-    ) {
-        gadget_logging::info!("At nested depth = {cur_depth}/{max_depth}");
-
-        if cur_depth == max_depth {
-            return;
-        }
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            #[allow(clippy::cast_possible_truncation)]
-            task_hash: blake3_256(&[(cur_depth % 255) as u8]),
-            round_id: 0,
-        };
-
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        let subnetwork1 = multiplexer1.multiplex(stream_key);
-        let subnetwork1_id = subnetwork1.public_id();
-
-        // Send a message in the subnetwork0 to subnetwork1 and vice versa, assert values of message
-        let payload = StressTestPayload { value: 42 };
-        let msg = subnetwork0.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1),
-            &payload,
-            Some(subnetwork1_id),
-        );
-
-        gadget_logging::info!("Sending message from subnetwork0");
-        subnetwork0.send(msg.clone()).unwrap();
-
-        // Receive message
-        let received_msg = subnetwork1.recv().await.unwrap();
-        let received: StressTestPayload = deserialize(&received_msg.payload).unwrap();
-        assert_eq!(received.value, payload.value);
-
-        let msg = subnetwork1.build_protocol_message(
-            IdentifierInfo::default(),
-            1,
-            Some(0),
-            &payload,
-            Some(subnetwork0.public_id()),
-        );
-
-        gadget_logging::info!("Sending message from subnetwork1");
-        subnetwork1.send(msg.clone()).unwrap();
-
-        // Receive message
-        let received_msg = subnetwork0.recv().await.unwrap();
-        let received: StressTestPayload = deserialize(&received_msg.payload).unwrap();
-        assert_eq!(received.value, payload.value);
-        tracing::info!("Done nested depth = {cur_depth}/{max_depth}");
-
-        Box::pin(nested_multiplex(
-            cur_depth + 1,
-            max_depth,
-            subnetwork0,
-            subnetwork1,
-        ))
-        .await;
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_closed_channel_handling() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        // Drop subnetwork1's receiver to simulate closed channel
-        let subnetwork1 = multiplexer1.multiplex(stream_key);
-        drop(subnetwork1);
-
-        let payload = StressTestPayload { value: 42 };
-        let msg =
-            subnetwork0.build_protocol_message(IdentifierInfo::default(), 0, None, &payload, None);
-
-        // Sending to a closed channel should return an error
-        assert!(subnetwork0.send(msg).is_ok()); // Changed to ok() since the message will be sent but not received
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_empty_payload() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        let subnetwork1 = multiplexer1.multiplex(stream_key);
-
-        // Test empty payload
-        let empty_payload = StressTestPayload { value: 0 };
-        let msg = subnetwork0.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1),
-            &empty_payload,
-            Some(subnetwork1.public_id()),
-        );
-
-        gadget_logging::info!("Sending message from subnetwork0");
-        subnetwork0.send(msg).unwrap();
-
-        // Receive message
-        let received_msg = subnetwork1.recv().await.unwrap();
-        let received: StressTestPayload = deserialize(&received_msg.payload).unwrap();
-        assert_eq!(received.value, empty_payload.value);
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    #[allow(clippy::cast_possible_truncation)]
-    async fn test_concurrent_messaging() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let mut send_handles = Vec::new();
-        let mut receive_handles = Vec::new();
-
-        // Create multiple messages to send concurrently
-        let message_count = 10;
-
-        // Spawn tasks to send messages
-        for i in 0..message_count {
-            let stream_key = StreamKey {
-                task_hash: blake3_256(&[i]),
-                round_id: 0,
-            };
-
-            let subnetwork0 = multiplexer0.multiplex(stream_key);
-            let subnetwork1 = multiplexer1.multiplex(stream_key);
-            let subnetwork1_id = subnetwork1.public_id();
-
-            let i_u64: u64 = i.into();
-            let payload = StressTestPayload { value: i_u64 };
-            let send_subnetwork0 = subnetwork0;
-            let handle = tokio::spawn(async move {
-                let msg = send_subnetwork0.build_protocol_message(
-                    IdentifierInfo::default(),
-                    0,
-                    Some(1),
-                    &payload,
-                    Some(subnetwork1_id),
-                );
-                send_subnetwork0.send(msg).unwrap();
-            });
-
-            send_handles.push(handle);
-
-            // Spawn tasks to receive messages
-            let handle = tokio::spawn(async move {
-                let msg = subnetwork1.recv().await.unwrap();
-                let received: StressTestPayload = deserialize(&msg.payload).unwrap();
-                received.value as u8 // Return the payload value for verification
-            });
-
-            receive_handles.push(handle);
-        }
-
-        // Wait for all sends to complete
-        for handle in send_handles {
-            handle.await.unwrap();
-        }
-
-        // Wait for all receives and verify we got all messages
-        let mut received_values = Vec::new();
-        for handle in receive_handles {
-            received_values.push(handle.await.unwrap());
-        }
-
-        received_values.sort_unstable();
-        assert_eq!(received_values.len(), message_count as usize);
-        for i in 0..message_count {
-            assert_eq!(received_values[i as usize], i);
-        }
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    #[allow(clippy::cast_possible_truncation)]
-    async fn test_message_ordering() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        let subnetwork1 = multiplexer1.multiplex(stream_key);
-
-        // Send messages with sequential sequence numbers
-        let message_count = 10;
-        for i in 0..message_count {
-            let payload = StressTestPayload { value: i };
-            let msg = subnetwork0.build_protocol_message(
-                IdentifierInfo {
-                    message_id: i,
-                    ..Default::default()
-                },
-                0,
-                Some(1),
-                &payload,
-                Some(subnetwork1.public_id()),
-            );
-            subnetwork0.send(msg).unwrap();
-        }
-
-        // Verify messages are received in order
-        let mut last_seq = 0;
-        for _ in 0..message_count {
-            let msg = subnetwork1.recv().await.unwrap();
-            assert!(
-                msg.identifier_info.message_id >= last_seq,
-                "Messages should be received in order or equal to last sequence number"
-            );
-            last_seq = msg.identifier_info.message_id;
-        }
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_network_id_handling() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-        let _network0_id = network0.public_id();
-        let network1_id = network1.public_id();
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        let stream_key = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-
-        let subnetwork0 = multiplexer0.multiplex(stream_key);
-        let subnetwork1 = multiplexer1.multiplex(stream_key);
-
-        // Test sending with correct network ID
-        let payload = StressTestPayload { value: 42 };
-        let msg = subnetwork0.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1),
-            &payload,
-            Some(network1_id),
-        );
-        gadget_logging::info!("Sending message from subnetwork0");
-        subnetwork0.send(msg.clone()).unwrap();
-
-        // Receive message
-        let received_msg = subnetwork1.recv().await.unwrap();
-        let received: StressTestPayload = deserialize(&received_msg.payload).unwrap();
-        assert_eq!(received.value, payload.value);
-
-        // Test sending with wrong network ID
-        let wrong_key = crate::key_types::Curve::generate_with_seed(None).unwrap();
-        let msg = subnetwork0.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1),
-            &payload,
-            Some(wrong_key.public()),
-        );
-        gadget_logging::info!("Sending message from subnetwork0");
-        subnetwork0.send(msg).unwrap();
-
-        // Message with wrong network ID should not be received
-        let timeout = tokio::time::sleep(tokio::time::Duration::from_millis(100));
-        tokio::select! {
-            () = timeout => (),
-            _ = subnetwork1.recv() => panic!("Should not receive message with wrong network ID"),
-        }
-    }
-
-    #[tokio::test(flavor = "multi_thread")]
-    async fn test_stream_isolation() {
-        setup_log();
-        let (network0, network1) = get_networks().await;
-
-        let multiplexer0 = NetworkMultiplexer::new(network0);
-        let multiplexer1 = NetworkMultiplexer::new(network1);
-
-        // Create two different stream keys
-        let stream_key1 = StreamKey {
-            task_hash: blake3_256(&[1]),
-            round_id: 0,
-        };
-        let stream_key2 = StreamKey {
-            task_hash: blake3_256(&[2]),
-            round_id: 0,
-        };
-
-        let subnetwork0_stream1 = multiplexer0.multiplex(stream_key1);
-        let subnetwork0_stream2 = multiplexer0.multiplex(stream_key2);
-        let subnetwork1_stream1 = multiplexer1.multiplex(stream_key1);
-        let subnetwork1_stream2 = multiplexer1.multiplex(stream_key2);
-
-        // Send messages on both streams
-        let payload1 = StressTestPayload { value: 1 };
-        let payload2 = StressTestPayload { value: 2 };
-
-        let msg1 = subnetwork0_stream1.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1), // Send to node 1
-            &payload1,
-            Some(subnetwork1_stream1.public_id()),
-        );
-        let msg2 = subnetwork0_stream2.build_protocol_message(
-            IdentifierInfo::default(),
-            0,
-            Some(1), // Send to node 1
-            &payload2,
-            Some(subnetwork1_stream2.public_id()),
-        );
-
-        gadget_logging::info!("Sending message from subnetwork0_stream1");
-        subnetwork0_stream1.send(msg1.clone()).unwrap();
-        gadget_logging::info!("Sending message from subnetwork0_stream2");
-        subnetwork0_stream2.send(msg2.clone()).unwrap();
-
-        // Verify messages are received on correct streams
-        gadget_logging::info!("Waiting for message on subnetwork1_stream1");
-        let received_msg1 = subnetwork1_stream1.recv().await.unwrap();
-        gadget_logging::info!("Waiting for message on subnetwork1_stream2");
-        let received_msg2 = subnetwork1_stream2.recv().await.unwrap();
-
-        let received1: StressTestPayload = deserialize(&received_msg1.payload).unwrap();
-        let received2: StressTestPayload = deserialize(&received_msg2.payload).unwrap();
-
-        assert_eq!(received1.value, payload1.value);
-        assert_eq!(received2.value, payload2.value);
-
-        // Verify no cross-stream message leakage
-        let timeout = tokio::time::sleep(tokio::time::Duration::from_millis(100));
-        tokio::select! {
-            () = timeout => (),
-            _ = subnetwork1_stream1.recv() => panic!("Should not receive more messages on stream 1"),
-            _ = subnetwork1_stream2.recv() => panic!("Should not receive more messages on stream 2"),
-        }
-    }
-}
-
-#[derive(Serialize, Deserialize, Debug)]
-struct StressTestPayload {
-    value: u64,
-}
diff --git a/crates/networking/src/round_based_compat.rs b/crates/networking/src/round_based_compat.rs
deleted file mode 100644
index d89d8f077..000000000
--- a/crates/networking/src/round_based_compat.rs
+++ /dev/null
@@ -1,229 +0,0 @@
-use crate::key_types::GossipMsgPublicKey;
-use crate::networking::{
-    IdentifierInfo, NetworkMultiplexer, ProtocolMessage, StreamKey, SubNetwork,
-};
-use core::pin::Pin;
-use core::sync::atomic::AtomicU64;
-use core::task::{ready, Context, Poll};
-use futures::prelude::*;
-use gadget_std::collections::{BTreeMap, HashMap, VecDeque};
-use gadget_std::string::ToString;
-use gadget_std::sync::Arc;
-use round_based::{Delivery, Incoming, MessageType, Outgoing};
-use round_based::{MessageDestination, MsgId, PartyIndex};
-use stream::{SplitSink, SplitStream};
-
-use crate::networking::ParticipantInfo;
-
-pub struct NetworkDeliveryWrapper<M> {
-    /// The wrapped network implementation.
-    network: NetworkWrapper<M>,
-}
-
-impl<M> NetworkDeliveryWrapper<M>
-where
-    M: Clone + Send + Unpin + 'static,
-    M: serde::Serialize + serde::de::DeserializeOwned,
-{
-    /// Create a new `NetworkDeliveryWrapper` over a network implementation with the given party index.
-    #[must_use]
-    pub fn new(
-        mux: Arc<NetworkMultiplexer>,
-        i: PartyIndex,
-        task_hash: [u8; 32],
-        parties: BTreeMap<PartyIndex, GossipMsgPublicKey>,
-    ) -> Self {
-        let (tx_forward, rx) = tokio::sync::mpsc::unbounded_channel();
-        // By default, we create 10 substreams for each party.
-        let mut sub_streams = HashMap::new();
-        for x in 0..10 {
-            let key = StreamKey {
-                task_hash,
-                round_id: x,
-            };
-            // Creates a multiplexed subnetwork, and also forwards all messages to the given channel
-            let _ = sub_streams.insert(key, mux.multiplex_with_forwarding(key, tx_forward.clone()));
-        }
-
-        let network = NetworkWrapper {
-            me: i,
-            mux,
-            incoming_queue: VecDeque::new(),
-            sub_streams,
-            participants: parties,
-            task_hash,
-            tx_forward,
-            rx,
-            next_msg_id: Arc::new(NextMessageId::default()),
-        };
-
-        NetworkDeliveryWrapper { network }
-    }
-}
-
-/// A `NetworkWrapper` wraps a network implementation
-/// and implements [`Stream`] and [`Sink`] for it.
-pub struct NetworkWrapper<M> {
-    /// The current party index.
-    me: PartyIndex,
-    /// Our network Multiplexer.
-    mux: Arc<NetworkMultiplexer>,
-    /// A Map of substreams for each round.
-    sub_streams: HashMap<StreamKey, SubNetwork>,
-    /// A queue of incoming messages.
-    #[allow(dead_code)]
-    incoming_queue: VecDeque<Incoming<M>>,
-    /// Participants in the network with their corresponding public keys.
-    /// Note: This is a `BTreeMap` to ensure that the participants are sorted by their party index.
-    participants: BTreeMap<PartyIndex, GossipMsgPublicKey>,
-    next_msg_id: Arc<NextMessageId>,
-    tx_forward: tokio::sync::mpsc::UnboundedSender<ProtocolMessage>,
-    rx: tokio::sync::mpsc::UnboundedReceiver<ProtocolMessage>,
-    task_hash: [u8; 32],
-}
-
-impl<M> Delivery<M> for NetworkDeliveryWrapper<M>
-where
-    M: Clone + Send + Unpin + 'static,
-    M: serde::Serialize + serde::de::DeserializeOwned,
-    M: round_based::ProtocolMessage,
-{
-    type Send = SplitSink<NetworkWrapper<M>, Outgoing<M>>;
-    type Receive = SplitStream<NetworkWrapper<M>>;
-    type SendError = crate::error::Error;
-    type ReceiveError = crate::error::Error;
-
-    fn split(self) -> (Self::Receive, Self::Send) {
-        let (sink, stream) = self.network.split();
-        (stream, sink)
-    }
-}
-
-impl<M> Stream for NetworkWrapper<M>
-where
-    M: serde::de::DeserializeOwned + Unpin,
-    M: round_based::ProtocolMessage,
-{
-    type Item = Result<Incoming<M>, crate::error::Error>;
-
-    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
-        let res = ready!(self.get_mut().rx.poll_recv(cx));
-        if let Some(res) = res {
-            let msg_type = if res.recipient.is_some() {
-                MessageType::P2P
-            } else {
-                MessageType::Broadcast
-            };
-
-            let id = res.identifier_info.message_id;
-
-            let msg = match serde_json::from_slice(&res.payload) {
-                Ok(msg) => msg,
-                Err(err) => {
-                    gadget_logging::error!(%err, "Failed to deserialize message (round_based_compat)");
-                    return Poll::Ready(Some(Err(crate::error::Error::Other(err.to_string()))));
-                }
-            };
-
-            Poll::Ready(Some(Ok(Incoming {
-                msg,
-                sender: res.sender.user_id,
-                id,
-                msg_type,
-            })))
-        } else {
-            Poll::Ready(None)
-        }
-    }
-}
-
-impl<M> Sink<Outgoing<M>> for NetworkWrapper<M>
-where
-    M: Unpin + serde::Serialize,
-    M: round_based::ProtocolMessage,
-{
-    type Error = crate::error::Error;
-
-    fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        Poll::Ready(Ok(()))
-    }
-
-    fn start_send(self: Pin<&mut Self>, out: Outgoing<M>) -> Result<(), Self::Error> {
-        let this = self.get_mut();
-        let id = this.next_msg_id.next();
-
-        let round_id = out.msg.round();
-
-        gadget_logging::info!(
-            "Round {}: Sending message from {} to {:?} (id: {})",
-            round_id,
-            this.me,
-            out.recipient,
-            id,
-        );
-
-        // Get the substream to send the message to.
-        let key = StreamKey {
-            task_hash: this.task_hash,
-            round_id: i32::from(round_id),
-        };
-        let substream = this.sub_streams.entry(key).or_insert_with(|| {
-            this.mux
-                .multiplex_with_forwarding(key, this.tx_forward.clone())
-        });
-
-        let identifier_info = IdentifierInfo {
-            message_id: id,
-            round_id,
-        };
-        let (to, to_network_id) = match out.recipient {
-            MessageDestination::AllParties => (None, None),
-            MessageDestination::OneParty(p) => (Some(p), this.participants.get(&p).copied()),
-        };
-
-        if matches!(out.recipient, MessageDestination::OneParty(_)) && to_network_id.is_none() {
-            gadget_logging::warn!("Recipient not found when required for {:?}", out.recipient);
-            return Err(crate::error::Error::Other(
-                "Recipient not found".to_string(),
-            ));
-        }
-
-        // Manually construct a `ProtocolMessage` since rounds-based
-        // does not work well with bincode
-        let protocol_message = ProtocolMessage {
-            identifier_info,
-            sender: ParticipantInfo {
-                user_id: this.me,
-                public_key: this.participants.get(&this.me).copied(),
-            },
-            recipient: to.map(|user_id| ParticipantInfo {
-                user_id,
-                public_key: to_network_id,
-            }),
-            payload: serde_json::to_vec(&out.msg).expect("Should be able to serialize message"),
-        };
-
-        match substream.send(protocol_message) {
-            Ok(()) => Ok(()),
-            Err(e) => Err(e),
-        }
-    }
-
-    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        Poll::Ready(Ok(()))
-    }
-
-    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        Poll::Ready(Ok(()))
-    }
-}
-
-#[derive(Default)]
-struct NextMessageId(AtomicU64);
-
-impl NextMessageId {
-    fn next(&self) -> MsgId {
-        self.0
-            .fetch_add(1, gadget_std::sync::atomic::Ordering::Relaxed)
-    }
-}
diff --git a/crates/networking/src/service.rs b/crates/networking/src/service.rs
new file mode 100644
index 000000000..e1d727db4
--- /dev/null
+++ b/crates/networking/src/service.rs
@@ -0,0 +1,538 @@
+use std::{collections::HashSet, sync::Arc, time::Duration};
+
+use crate::{
+    behaviours::{GadgetBehaviour, GadgetBehaviourEvent},
+    blueprint_protocol::{BlueprintProtocolEvent, InstanceMessageRequest, InstanceMessageResponse},
+    discovery::{
+        behaviour::{DerivedDiscoveryBehaviourEvent, DiscoveryEvent},
+        PeerInfo, PeerManager,
+    },
+    error::Error,
+    key_types::{InstanceMsgKeyPair, InstanceMsgPublicKey},
+    service_handle::NetworkServiceHandle,
+    types::ProtocolMessage,
+};
+use crossbeam_channel::{self, Receiver, Sender};
+use futures::StreamExt;
+use libp2p::{
+    identify,
+    identity::Keypair,
+    kad, mdns, ping,
+    swarm::{dial_opts::DialOpts, SwarmEvent},
+    Multiaddr, PeerId, Swarm, SwarmBuilder,
+};
+use tracing::trace;
+use tracing::{debug, info, warn};
+
+/// Events emitted by the network service
+#[derive(Debug)]
+pub enum NetworkEvent {
+    /// New request received from a peer
+    InstanceRequestInbound {
+        peer: PeerId,
+        request: InstanceMessageRequest,
+    },
+    /// New response received from a peer
+    InstanceResponseInbound {
+        peer: PeerId,
+        response: InstanceMessageResponse,
+    },
+    /// New request sent to a peer
+    InstanceRequestOutbound {
+        peer: PeerId,
+        request: InstanceMessageRequest,
+    },
+    /// Response sent to a peer
+    InstanceResponseOutbound {
+        peer: PeerId,
+        response: InstanceMessageResponse,
+    },
+    /// New gossip message received
+    GossipReceived {
+        source: PeerId,
+        topic: String,
+        message: Vec<u8>,
+    },
+    /// New gossip message sent
+    GossipSent { topic: String, message: Vec<u8> },
+    /// Peer connected
+    PeerConnected(PeerId),
+    /// Peer disconnected
+    PeerDisconnected(PeerId),
+    /// Handshake completed successfully
+    HandshakeCompleted { peer: PeerId },
+    /// Handshake failed
+    HandshakeFailed { peer: PeerId, reason: String },
+}
+
+/// Network message types
+#[derive(Debug)]
+pub enum NetworkMessage {
+    InstanceRequest {
+        peer: PeerId,
+        request: InstanceMessageRequest,
+    },
+    GossipMessage {
+        source: PeerId,
+        topic: String,
+        message: Vec<u8>,
+    },
+}
+
+/// Configuration for the network service
+#[derive(Debug, Clone)]
+pub struct NetworkConfig {
+    /// Network name/namespace
+    pub network_name: String,
+    /// Instance id for blueprint protocol
+    pub instance_id: String,
+    /// Instance secret key for blueprint protocol
+    pub instance_key_pair: InstanceMsgKeyPair,
+    /// Local keypair for authentication
+    pub local_key: Keypair,
+    /// Address to listen on
+    pub listen_addr: Multiaddr,
+    /// Target number of peers to maintain
+    pub target_peer_count: u32,
+    /// Bootstrap peers to connect to
+    pub bootstrap_peers: Vec<Multiaddr>,
+    /// Whether to enable mDNS discovery
+    pub enable_mdns: bool,
+    /// Whether to enable Kademlia DHT
+    pub enable_kademlia: bool,
+}
+
+pub struct NetworkService {
+    /// The libp2p swarm
+    swarm: Swarm<GadgetBehaviour>,
+    /// Peer manager for tracking peer states
+    pub(crate) peer_manager: Arc<PeerManager>,
+    /// Channel for sending messages to the network service
+    network_sender: Sender<NetworkMessage>,
+    /// Channel for receiving messages from the network service
+    network_receiver: Receiver<NetworkMessage>,
+    /// Channel for receiving messages from the network service
+    protocol_message_receiver: Receiver<ProtocolMessage>,
+    /// Channel for sending events to the network service
+    event_sender: Sender<NetworkEvent>,
+    /// Channel for receiving events from the network service
+    #[expect(dead_code)] // For future use
+    event_receiver: Receiver<NetworkEvent>,
+    /// Bootstrap peers
+    bootstrap_peers: HashSet<Multiaddr>,
+}
+
+impl NetworkService {
+    /// Create a new network service
+    ///
+    /// # Errors
+    ///
+    /// * See [`GadgetBehaviour::new`]
+    /// * Bad `listen_addr` in the provided [`NetworkConfig`]
+    #[allow(clippy::missing_panics_doc)] // Unwrapping an Infallible
+    pub fn new(
+        config: NetworkConfig,
+        allowed_keys: HashSet<InstanceMsgPublicKey>,
+        // allowed_keys_rx: Receiver<HashSet<InstanceMsgPublicKey>>,
+    ) -> Result<Self, Error> {
+        let NetworkConfig {
+            network_name,
+            instance_id,
+            instance_key_pair,
+            local_key,
+            listen_addr,
+            target_peer_count,
+            bootstrap_peers,
+            enable_mdns: _,
+            enable_kademlia: _,
+        } = config;
+
+        let peer_manager = Arc::new(PeerManager::new(allowed_keys));
+        let blueprint_protocol_name = format!("/{network_name}/{instance_id}");
+
+        let (network_sender, network_receiver) = crossbeam_channel::unbounded();
+        let (protocol_message_sender, protocol_message_receiver) = crossbeam_channel::unbounded();
+        let (event_sender, event_receiver) = crossbeam_channel::unbounded();
+
+        // Create the swarm
+        let behaviour = GadgetBehaviour::new(
+            &network_name,
+            &blueprint_protocol_name,
+            &local_key,
+            &instance_key_pair,
+            target_peer_count,
+            peer_manager.clone(),
+            protocol_message_sender.clone(),
+        )?;
+
+        let mut swarm = SwarmBuilder::with_existing_identity(local_key)
+            .with_tokio()
+            .with_tcp(
+                libp2p::tcp::Config::default().nodelay(true),
+                libp2p::noise::Config::new,
+                libp2p::yamux::Config::default,
+            )?
+            .with_quic_config(|mut config| {
+                config.handshake_timeout = Duration::from_secs(30);
+                config
+            })
+            .with_dns()?
+            .with_behaviour(|_| behaviour)
+            .unwrap()
+            .build();
+
+        swarm
+            .behaviour_mut()
+            .blueprint_protocol
+            .subscribe(&blueprint_protocol_name)?;
+
+        // Start listening
+        swarm.listen_on(listen_addr)?;
+        let bootstrap_peers = bootstrap_peers.into_iter().collect();
+
+        Ok(Self {
+            swarm,
+            peer_manager,
+            network_sender,
+            network_receiver,
+            protocol_message_receiver,
+            event_sender,
+            event_receiver,
+            bootstrap_peers,
+        })
+    }
+
+    /// Get a sender to send messages to the network service
+    pub fn network_sender(&self) -> Sender<NetworkMessage> {
+        self.network_sender.clone()
+    }
+
+    pub fn start(self) -> NetworkServiceHandle {
+        let local_peer_id = *self.swarm.local_peer_id();
+        let network_sender = self.network_sender.clone();
+        let protocol_message_receiver = self.protocol_message_receiver.clone();
+
+        // Create handle with new interface
+        let handle = NetworkServiceHandle::new(
+            local_peer_id,
+            self.swarm
+                .behaviour()
+                .blueprint_protocol
+                .blueprint_protocol_name
+                .clone(),
+            self.peer_manager.clone(),
+            network_sender,
+            protocol_message_receiver,
+        );
+
+        // Add our own peer ID to the peer manager with all listening addresses
+        let mut info = PeerInfo::default();
+        for addr in self.swarm.listeners() {
+            info.addresses.insert(addr.clone());
+        }
+        self.peer_manager.update_peer(local_peer_id, info);
+
+        // Spawn background task
+        tokio::spawn(async move {
+            Box::pin(self.run()).await;
+        });
+
+        handle
+    }
+
+    /// Run the network service
+    async fn run(mut self) {
+        info!("Starting network service");
+
+        // Bootstrap with Kademlia
+        if let Err(e) = self.swarm.behaviour_mut().bootstrap() {
+            warn!("Failed to bootstrap with Kademlia: {}", e);
+        }
+
+        // Connect to bootstrap peers
+        for addr in &self.bootstrap_peers {
+            debug!("Dialing bootstrap peer at {}", addr);
+            if let Err(e) = self.swarm.dial(addr.clone()) {
+                warn!("Failed to dial bootstrap peer: {}", e);
+            }
+        }
+
+        loop {
+            tokio::select! {
+                swarm_event = self.swarm.select_next_some() => {
+                    match swarm_event {
+                        SwarmEvent::NewListenAddr { address, .. } => {
+                            info!("New listen address: {}", address);
+                            let local_peer_id = *self.swarm.local_peer_id();
+                            let mut info = self.peer_manager.get_peer_info(&local_peer_id)
+                                .unwrap_or_default();
+                            info.addresses.insert(address.clone());
+                            self.peer_manager.update_peer(local_peer_id, info);
+                        },
+                        SwarmEvent::Behaviour(event) => {
+                            if let Err(e) = handle_behaviour_event(
+                                &mut self.swarm,
+                                &self.peer_manager,
+                                event,
+                                &self.event_sender,
+                            )
+                            {
+                                warn!("Failed to handle swarm event: {}", e);
+                            }
+                        },
+                        _ => {}
+                    }
+                }
+                Ok(msg) = async { self.network_receiver.try_recv() } => {
+                    if let Err(e) = handle_network_message(
+                        &mut self.swarm,
+                        msg,
+                        &self.peer_manager,
+                        &self.event_sender,
+                    )
+                    {
+                        warn!("Failed to handle network message: {}", e);
+                    }
+                }
+                else => break,
+            }
+        }
+
+        info!("Network service stopped");
+    }
+
+    /// Get the current listening address
+    pub fn get_listen_addr(&self) -> Option<Multiaddr> {
+        self.swarm.listeners().next().cloned()
+    }
+}
+
+/// Handle a behaviour event
+fn handle_behaviour_event(
+    swarm: &mut Swarm<GadgetBehaviour>,
+    peer_manager: &Arc<PeerManager>,
+    event: GadgetBehaviourEvent,
+    event_sender: &Sender<NetworkEvent>,
+) -> Result<(), Error> {
+    match event {
+        GadgetBehaviourEvent::ConnectionLimits(_) => {}
+        GadgetBehaviourEvent::Discovery(discovery_event) => {
+            handle_discovery_event(swarm, peer_manager, discovery_event, event_sender)?;
+        }
+        GadgetBehaviourEvent::BlueprintProtocol(blueprint_event) => {
+            handle_blueprint_protocol_event(swarm, peer_manager, blueprint_event, event_sender)?;
+        }
+        GadgetBehaviourEvent::Ping(ping_event) => {
+            handle_ping_event(swarm, peer_manager, ping_event, event_sender)?;
+        }
+    }
+
+    Ok(())
+}
+
+/// Handle a discovery event
+fn handle_discovery_event(
+    swarm: &mut Swarm<GadgetBehaviour>,
+    peer_manager: &Arc<PeerManager>,
+    event: DiscoveryEvent,
+    event_sender: &Sender<NetworkEvent>,
+) -> Result<(), Error> {
+    match event {
+        DiscoveryEvent::PeerConnected(peer_id) => {
+            info!("Peer connected, {peer_id}");
+            // Update peer info when connected
+            if let Some(info) = swarm.behaviour().discovery.peer_info.get(&peer_id) {
+                peer_manager.update_peer(peer_id, info.clone());
+            }
+            event_sender.send(NetworkEvent::PeerConnected(peer_id))?;
+        }
+        DiscoveryEvent::PeerDisconnected(peer_id) => {
+            info!("Peer disconnected, {peer_id}");
+            peer_manager.remove_peer(&peer_id, "disconnected");
+            event_sender.send(NetworkEvent::PeerDisconnected(peer_id))?;
+        }
+        DiscoveryEvent::Discovery(discovery_event) => match &*discovery_event {
+            DerivedDiscoveryBehaviourEvent::Identify(identify::Event::Received {
+                peer_id,
+                info,
+                ..
+            }) => {
+                info!(%peer_id, "Received identify event");
+                let protocols: HashSet<String> = info
+                    .protocols
+                    .iter()
+                    .map(std::string::ToString::to_string)
+                    .collect();
+
+                trace!(%peer_id, ?protocols, "Supported protocols");
+
+                let blueprint_protocol_name =
+                    &swarm.behaviour().blueprint_protocol.blueprint_protocol_name;
+                if !protocols.contains(blueprint_protocol_name) {
+                    warn!(%peer_id, %blueprint_protocol_name, "Peer does not support required protocol");
+                    peer_manager.ban_peer_with_default_duration(*peer_id, "protocol unsupported");
+                    return Ok(());
+                }
+
+                // Get existing peer info or create new one
+                let mut peer_info = peer_manager.get_peer_info(peer_id).unwrap_or_default();
+
+                // Update identify info
+                peer_info.identify_info = Some(info.clone());
+
+                trace!(%peer_id, listen_addrs=?info.listen_addrs, "Adding identify addresses");
+                // Add all addresses from identify info
+                for addr in &info.listen_addrs {
+                    peer_info.addresses.insert(addr.clone());
+                }
+
+                trace!(%peer_id, "Updating peer info with identify information");
+                peer_manager.update_peer(*peer_id, peer_info);
+                debug!(%peer_id, "Successfully processed identify information");
+            }
+            DerivedDiscoveryBehaviourEvent::Kademlia(kad::Event::OutboundQueryProgressed {
+                result: kad::QueryResult::GetClosestPeers(Ok(ok)),
+                ..
+            }) => {
+                // Process newly discovered peers
+                for peer_info in &ok.peers {
+                    if !peer_manager.get_peers().contains_key(&peer_info.peer_id) {
+                        info!(%peer_info.peer_id, "Newly discovered peer from Kademlia");
+                        let info = PeerInfo::default();
+                        peer_manager.update_peer(peer_info.peer_id, info);
+                        let addrs: Vec<_> = peer_info.addrs.clone();
+                        for addr in addrs {
+                            debug!(%peer_info.peer_id, %addr, "Dialing peer from Kademlia");
+                            if let Err(e) = swarm.dial(DialOpts::from(addr)) {
+                                warn!("Failed to dial address: {}", e);
+                            }
+                        }
+                    }
+                }
+            }
+            DerivedDiscoveryBehaviourEvent::Mdns(mdns::Event::Discovered(list)) => {
+                // Add newly discovered peers from mDNS
+                for (peer_id, addr) in list {
+                    if !peer_manager.get_peers().contains_key(peer_id) {
+                        info!(%peer_id, %addr, "Newly discovered peer from Mdns");
+                        let mut info = PeerInfo::default();
+                        info.addresses.insert(addr.clone());
+                        peer_manager.update_peer(*peer_id, info);
+                        debug!(%peer_id, %addr, "Dialing peer from Mdns");
+                        if let Err(e) = swarm.dial(DialOpts::from(addr.clone())) {
+                            warn!("Failed to dial address: {}", e);
+                        }
+                    }
+                }
+            }
+            _ => {}
+        },
+    }
+
+    Ok(())
+}
+
+/// Handle a blueprint event
+fn handle_blueprint_protocol_event(
+    _swarm: &mut Swarm<GadgetBehaviour>,
+    _peer_manager: &Arc<PeerManager>,
+    event: BlueprintProtocolEvent,
+    event_sender: &Sender<NetworkEvent>,
+) -> Result<(), Error> {
+    match event {
+        BlueprintProtocolEvent::Request {
+            peer,
+            request,
+            channel: _,
+        } => event_sender.send(NetworkEvent::InstanceRequestInbound { peer, request })?,
+        BlueprintProtocolEvent::Response {
+            peer,
+            response,
+            request_id: _,
+        } => event_sender.send(NetworkEvent::InstanceResponseInbound { peer, response })?,
+        BlueprintProtocolEvent::GossipMessage {
+            source,
+            topic,
+            message,
+        } => event_sender.send(NetworkEvent::GossipReceived {
+            source,
+            topic: topic.to_string(),
+            message,
+        })?,
+    }
+
+    Ok(())
+}
+
+/// Handle a ping event
+#[expect(clippy::unnecessary_wraps)]
+fn handle_ping_event(
+    _swarm: &mut Swarm<GadgetBehaviour>,
+    _peer_manager: &Arc<PeerManager>,
+    event: ping::Event,
+    _event_sender: &Sender<NetworkEvent>,
+) -> Result<(), Error> {
+    match event.result {
+        Ok(rtt) => {
+            trace!(
+                "PingSuccess::Ping rtt to {} is {} ms",
+                event.peer,
+                rtt.as_millis()
+            );
+        }
+        Err(ping::Failure::Unsupported) => {
+            debug!(peer=%event.peer, "Ping protocol unsupported");
+        }
+        Err(ping::Failure::Timeout) => {
+            debug!("Ping timeout: {}", event.peer);
+        }
+        Err(ping::Failure::Other { error }) => {
+            debug!("Ping failure: {error}");
+        }
+    }
+
+    Ok(())
+}
+
+/// Handle a network message
+fn handle_network_message(
+    swarm: &mut Swarm<GadgetBehaviour>,
+    msg: NetworkMessage,
+    peer_manager: &Arc<PeerManager>,
+    event_sender: &Sender<NetworkEvent>,
+) -> Result<(), Error> {
+    match msg {
+        NetworkMessage::InstanceRequest { peer, request } => {
+            // Only send requests to verified peers
+            if !peer_manager.is_peer_verified(&peer) {
+                warn!(%peer, "Attempted to send request to unverified peer");
+                return Ok(());
+            }
+
+            debug!(%peer, ?request, "Sending instance request");
+            swarm
+                .behaviour_mut()
+                .blueprint_protocol
+                .send_request(&peer, request.clone());
+            event_sender.send(NetworkEvent::InstanceRequestOutbound { peer, request })?;
+        }
+        NetworkMessage::GossipMessage {
+            source,
+            topic,
+            message,
+        } => {
+            debug!(%source, %topic, "Publishing gossip message");
+            if let Err(e) = swarm
+                .behaviour_mut()
+                .blueprint_protocol
+                .publish(&topic, message.clone())
+            {
+                warn!(%source, %topic, "Failed to publish gossip message: {:?}", e);
+                return Ok(());
+            }
+            event_sender.send(NetworkEvent::GossipSent { topic, message })?;
+        }
+    }
+
+    Ok(())
+}
diff --git a/crates/networking/src/service_handle.rs b/crates/networking/src/service_handle.rs
new file mode 100644
index 000000000..d46d34748
--- /dev/null
+++ b/crates/networking/src/service_handle.rs
@@ -0,0 +1,198 @@
+use crate::types::MessageRouting;
+use crate::{
+    blueprint_protocol::InstanceMessageRequest,
+    discovery::{PeerInfo, PeerManager},
+    service::NetworkMessage,
+    types::ProtocolMessage,
+};
+use crossbeam_channel::{self, Receiver, Sender};
+use libp2p::{Multiaddr, PeerId};
+use std::sync::Arc;
+use tokio::task::JoinHandle;
+use tracing::debug;
+
+/// Handle for sending outgoing messages to the network
+#[derive(Clone)]
+pub struct NetworkSender {
+    network_message_sender: Sender<NetworkMessage>,
+}
+
+impl NetworkSender {
+    #[must_use]
+    pub fn new(network_message_sender: Sender<NetworkMessage>) -> Self {
+        Self {
+            network_message_sender,
+        }
+    }
+
+    /// Send a protocol message over the network
+    ///
+    /// # Errors
+    ///
+    /// See [`crossbeam_channel::Sender::send`]
+    pub fn send_message(&self, message: NetworkMessage) -> Result<(), String> {
+        self.network_message_sender
+            .send(message)
+            .map_err(|e| e.to_string())
+    }
+}
+
+/// Handle for receiving incoming messages from the network
+pub struct NetworkReceiver {
+    protocol_message_receiver: Receiver<ProtocolMessage>,
+}
+
+impl NetworkReceiver {
+    #[must_use]
+    pub fn new(protocol_message_receiver: Receiver<ProtocolMessage>) -> Self {
+        Self {
+            protocol_message_receiver,
+        }
+    }
+
+    /// Get the next protocol message
+    ///
+    /// # Errors
+    ///
+    /// See [`crossbeam_channel::Receiver::try_recv()`]
+    pub fn try_recv(&self) -> Result<ProtocolMessage, crossbeam_channel::TryRecvError> {
+        self.protocol_message_receiver.try_recv()
+    }
+}
+
+/// Combined handle for the network service
+pub struct NetworkServiceHandle {
+    pub local_peer_id: PeerId,
+    pub blueprint_protocol_name: Arc<str>,
+    pub sender: NetworkSender,
+    pub receiver: NetworkReceiver,
+    pub peer_manager: Arc<PeerManager>,
+}
+
+impl Clone for NetworkServiceHandle {
+    fn clone(&self) -> Self {
+        Self {
+            local_peer_id: self.local_peer_id,
+            blueprint_protocol_name: self.blueprint_protocol_name.clone(),
+            sender: self.sender.clone(),
+            receiver: NetworkReceiver::new(self.receiver.protocol_message_receiver.clone()),
+            peer_manager: self.peer_manager.clone(),
+        }
+    }
+}
+
+impl NetworkServiceHandle {
+    #[must_use]
+    pub fn new(
+        local_peer_id: PeerId,
+        blueprint_protocol_name: String,
+        peer_manager: Arc<PeerManager>,
+        network_message_sender: Sender<NetworkMessage>,
+        protocol_message_receiver: Receiver<ProtocolMessage>,
+    ) -> Self {
+        Self {
+            local_peer_id,
+            blueprint_protocol_name: Arc::from(blueprint_protocol_name),
+            sender: NetworkSender::new(network_message_sender),
+            receiver: NetworkReceiver::new(protocol_message_receiver),
+            peer_manager,
+        }
+    }
+
+    pub fn next_protocol_message(&mut self) -> Option<ProtocolMessage> {
+        self.receiver.try_recv().ok()
+    }
+
+    #[must_use]
+    pub fn peers(&self) -> Vec<PeerId> {
+        self.peer_manager
+            .get_peers()
+            .clone()
+            .into_read_only()
+            .iter()
+            .map(|(peer_id, _)| *peer_id)
+            .collect()
+    }
+
+    #[must_use]
+    pub fn peer_info(&self, peer_id: &PeerId) -> Option<PeerInfo> {
+        self.peer_manager.get_peer_info(peer_id)
+    }
+
+    /// Send a message
+    ///
+    /// # Errors
+    ///
+    /// See [`crossbeam_channel::Sender::send`]
+    pub fn send(&self, routing: MessageRouting, message: impl Into<Vec<u8>>) -> Result<(), String> {
+        let protocol_message = ProtocolMessage {
+            protocol: self.blueprint_protocol_name.clone().to_string(),
+            routing,
+            payload: message.into(),
+        };
+
+        let raw_payload = bincode::serialize(&protocol_message).map_err(|err| err.to_string())?;
+        match protocol_message.routing.recipient {
+            Some(recipient) => {
+                let instance_message_request = InstanceMessageRequest::Protocol {
+                    protocol: self.blueprint_protocol_name.clone().to_string(),
+                    payload: raw_payload,
+                    metadata: None,
+                };
+
+                let Some(public_key) = recipient.public_key else {
+                    return Ok(());
+                };
+
+                let Some(peer_id) = self.peer_manager.get_peer_id_from_public_key(&public_key)
+                else {
+                    return Ok(());
+                };
+
+                self.send_network_message(NetworkMessage::InstanceRequest {
+                    peer: peer_id,
+                    request: instance_message_request,
+                })?;
+                debug!("Sent outbound p2p `NetworkMessage` to {:?}", peer_id);
+            }
+            None => {
+                let gossip_message = NetworkMessage::GossipMessage {
+                    source: self.local_peer_id,
+                    topic: self.blueprint_protocol_name.clone().to_string(),
+                    message: raw_payload,
+                };
+                self.send_network_message(gossip_message)?;
+                debug!("Sent outbound gossip `NetworkMessage`");
+            }
+        }
+
+        Ok(())
+    }
+
+    pub(crate) fn send_network_message(&self, message: NetworkMessage) -> Result<(), String> {
+        self.sender.send_message(message)
+    }
+
+    #[must_use]
+    pub fn get_listen_addr(&self) -> Option<Multiaddr> {
+        // Get the first peer info for our local peer ID
+        if let Some(peer_info) = self.peer_manager.get_peer_info(&self.local_peer_id) {
+            // Return the first address from our peer info
+            peer_info.addresses.iter().next().cloned()
+        } else {
+            None
+        }
+    }
+
+    /// Split the handle into separate sender and receiver
+    #[must_use]
+    pub fn split(self) -> (NetworkSender, NetworkReceiver) {
+        (self.sender, self.receiver)
+    }
+}
+
+/// We might also bundle a `JoinHandle` so the user can await its completion if needed.
+pub struct NetworkServiceTaskHandle {
+    /// The join handle for the background service task.
+    pub service_task: JoinHandle<()>,
+}
diff --git a/crates/networking/src/setup.rs b/crates/networking/src/setup.rs
index 807f6014b..e69de29bb 100644
--- a/crates/networking/src/setup.rs
+++ b/crates/networking/src/setup.rs
@@ -1,328 +0,0 @@
-#![allow(unused_results, missing_docs)]
-
-use crate::error::Error;
-use crate::gossip::{
-    GossipHandle, IntraNodePayload, MyBehaviour, NetworkServiceWithoutSwarm, MAX_MESSAGE_SIZE,
-};
-pub use crate::key_types::GossipMsgKeyPair;
-use futures::StreamExt;
-use gadget_std as std;
-use gadget_std::collections::BTreeMap;
-use gadget_std::format;
-use gadget_std::io;
-use gadget_std::net::IpAddr;
-use gadget_std::str::FromStr;
-use gadget_std::string::String;
-use gadget_std::sync::atomic::AtomicUsize;
-use gadget_std::sync::Arc;
-use gadget_std::time::Duration;
-use gadget_std::vec;
-use gadget_std::vec::Vec;
-use libp2p::Multiaddr;
-use libp2p::{
-    gossipsub, gossipsub::IdentTopic, kad::store::MemoryStore, mdns, request_response,
-    swarm::dial_opts::DialOpts, StreamProtocol,
-};
-use lru_mem::LruCache;
-use tokio::select;
-use tokio::sync::{Mutex, RwLock};
-use tokio::task::{spawn, JoinHandle};
-
-/// The version of the gadget sdk
-pub const AGENT_VERSION: &str = "tangle/gadget-sdk/1.0.0";
-/// The version of the client
-pub const CLIENT_VERSION: &str = "1.0.0";
-
-/// The base network configuration for a blueprint's `libp2p` network.
-///
-/// This configuration is used to setup the `libp2p` network for a blueprint.
-/// Construct using [`NetworkConfig::new`] for advanced users or
-/// [`NetworkConfig::new_service_network`] ordinarily.
-pub struct NetworkConfig {
-    pub identity: libp2p::identity::Keypair,
-    pub secret_key: GossipMsgKeyPair,
-    pub bootnodes: Vec<Multiaddr>,
-    pub bind_port: u16,
-    pub topics: Vec<String>,
-}
-
-impl gadget_std::fmt::Debug for NetworkConfig {
-    fn fmt(&self, f: &mut gadget_std::fmt::Formatter<'_>) -> gadget_std::fmt::Result {
-        f.debug_struct("NetworkConfig")
-            .field("identity", &self.identity)
-            .field("bootnodes", &self.bootnodes)
-            .field("bind_port", &self.bind_port)
-            .field("topics", &self.topics)
-            .finish_non_exhaustive()
-    }
-}
-
-impl NetworkConfig {
-    /// For advanced use only. Use `NetworkConfig::new_service_network` for ordinary use.
-    /// This function allows for the creation of a network with multiple topics.
-    #[must_use]
-    pub fn new(
-        identity: libp2p::identity::Keypair,
-        secret_key: GossipMsgKeyPair,
-        bootnodes: Vec<Multiaddr>,
-        bind_port: u16,
-        topics: Vec<String>,
-    ) -> Self {
-        Self {
-            identity,
-            secret_key,
-            bootnodes,
-            bind_port,
-            topics,
-        }
-    }
-
-    /// When constructing a network for a single service, the service name is used as the network name.
-    /// Each service within a blueprint must have a unique network name.
-    pub fn new_service_network<T: Into<String>>(
-        identity: libp2p::identity::Keypair,
-        secret_key: GossipMsgKeyPair,
-        bootnodes: Vec<Multiaddr>,
-        bind_port: u16,
-        service_name: T,
-    ) -> Self {
-        Self::new(
-            identity,
-            secret_key,
-            bootnodes,
-            bind_port,
-            vec![service_name.into()],
-        )
-    }
-}
-
-/// Start a P2P network with the given configuration.
-///
-/// Each service will only have one network. It is necessary that each service calling this function
-/// uses a distinct network name, otherwise, the network will not be able to distinguish between
-/// the different services.
-///
-/// # Arguments
-///
-/// * `config` - The network configuration.
-///
-/// # Errors
-///
-/// Returns an error if the network setup fails.
-pub fn start_p2p_network(config: NetworkConfig) -> Result<GossipHandle, Error> {
-    if config.topics.len() != 1 {
-        return Err(Error::TooManyTopics(config.topics.len()));
-    }
-
-    let (networks, _) = multiplexed_libp2p_network(config)?;
-    let network = networks.into_iter().next().ok_or(Error::NoNetworkFound)?.1;
-    Ok(network)
-}
-
-pub type NetworkResult = Result<(BTreeMap<String, GossipHandle>, JoinHandle<()>), Error>;
-
-#[allow(clippy::collapsible_else_if, clippy::too_many_lines)]
-/// Starts the multiplexed libp2p network with the given configuration.
-///
-/// # Arguments
-///
-/// * `config` - The network configuration.
-///
-/// # Errors
-///
-/// Returns an error if the network setup fails.
-///
-/// # Panics
-///
-/// Panics if the network name is invalid.
-pub fn multiplexed_libp2p_network(config: NetworkConfig) -> NetworkResult {
-    // Setup both QUIC (UDP) and TCP transports the increase the chances of NAT traversal
-
-    use gadget_std::collections::BTreeMap;
-    let NetworkConfig {
-        identity,
-        bootnodes,
-        bind_port,
-        topics,
-        secret_key,
-    } = config;
-
-    // Ensure all topics are unique
-    let topics_unique = topics
-        .iter()
-        .cloned()
-        .collect::<std::collections::BTreeSet<_>>()
-        .into_iter()
-        .collect::<Vec<_>>();
-
-    if topics_unique.len() != topics.len() {
-        return Err(Error::DuplicateTopics);
-    }
-
-    let networks = topics;
-
-    let my_pk = secret_key.public();
-    let my_id = identity.public().to_peer_id();
-
-    let mut swarm = libp2p::SwarmBuilder::with_existing_identity(identity)
-        .with_tokio()
-        .with_tcp(
-            libp2p::tcp::Config::default().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_behaviour(|key| {
-            // Set a custom gossipsub configuration
-            let gossipsub_config = gossipsub::ConfigBuilder::default()
-                .protocol_id_prefix("/tangle/gadget-binary-sdk/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::try_from_owned(n.clone()).expect("Invalid network name"),
-                        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,
-                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 public_key_to_libp2p_id = Arc::new(RwLock::new(BTreeMap::new()));
-    let mut handles_ret = BTreeMap::new();
-    let connected_peers = Arc::new(AtomicUsize::new(0));
-    for network in networks {
-        let topic = IdentTopic::new(network.clone());
-        swarm.behaviour_mut().gossipsub.subscribe(&topic)?;
-        let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel();
-        inbound_mapping.push((topic.clone(), inbound_tx, connected_peers.clone()));
-
-        handles_ret.insert(
-            network,
-            GossipHandle {
-                connected_peers: connected_peers.clone(),
-                topic,
-                tx_to_outbound: tx_to_outbound.clone(),
-                rx_from_inbound: Arc::new(Mutex::new(inbound_rx)),
-                public_key_to_libp2p_id: public_key_to_libp2p_id.clone(),
-                // Each key is 32 bytes, therefore 512 messages hashes can be stored in the set
-                recent_messages: LruCache::new(16 * 1024).into(),
-                my_id: my_pk,
-            },
-        );
-    }
-
-    let ips_to_bind_to = [
-        IpAddr::from_str("::").unwrap(),      // IN_ADDR_ANY_V6
-        IpAddr::from_str("0.0.0.0").unwrap(), // IN_ADDR_ANY_V4
-    ];
-
-    for addr in ips_to_bind_to {
-        let ip_label = if addr.is_ipv4() { "ip4" } else { "ip6" };
-        // Bind to both UDP and TCP to increase probability of successful NAT traversal.
-        // Use QUIC over UDP to have reliable ordered transport like TCP.
-        swarm.listen_on(format!("/{ip_label}/{addr}/udp/{bind_port}/quic-v1").parse()?)?;
-        swarm.listen_on(format!("/{ip_label}/{addr}/tcp/{bind_port}").parse()?)?;
-    }
-
-    // Dial all bootnodes
-    for bootnode in &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 {
-            inbound_mapping: &inbound_mapping,
-            connected_peers,
-            public_key_to_libp2p_id,
-            secret_key: &secret_key,
-            span: tracing::debug_span!(parent: &span, "network_service"),
-            my_id,
-        };
-
-        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 = spawn(worker);
-    Ok((handles_ret, spawn_handle))
-}
diff --git a/crates/networking/src/types.rs b/crates/networking/src/types.rs
new file mode 100644
index 000000000..6c1e4f4f9
--- /dev/null
+++ b/crates/networking/src/types.rs
@@ -0,0 +1,114 @@
+use crate::key_types::InstanceMsgPublicKey;
+use libp2p::{gossipsub::IdentTopic, PeerId};
+use serde::{Deserialize, Serialize};
+use std::fmt::Display;
+
+/// Maximum allowed size for a message payload
+pub const MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
+
+/// Unique identifier for a participant in the network
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct ParticipantId(pub u16);
+
+impl From<ParticipantId> for u16 {
+    fn from(val: ParticipantId) -> Self {
+        val.0
+    }
+}
+
+/// Type of message delivery mechanism
+#[derive(Debug, Clone)]
+pub enum MessageDelivery {
+    /// Broadcast to all peers via gossipsub
+    Broadcast {
+        /// The topic to broadcast on
+        topic: IdentTopic,
+    },
+    /// Direct P2P message to a specific peer
+    Direct {
+        /// The target peer ID
+        peer_id: PeerId,
+    },
+}
+
+/// Message routing information
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MessageRouting {
+    /// Unique identifier for this message
+    pub message_id: u64,
+    /// The round/sequence number this message belongs to
+    pub round_id: u16,
+    /// The sender's information
+    pub sender: ParticipantInfo,
+    /// Optional recipient information for direct messages
+    pub recipient: Option<ParticipantInfo>,
+}
+
+/// Information about a participant in the network
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ParticipantInfo {
+    /// The participant's unique ID
+    pub id: ParticipantId,
+    /// The participant's public key (if known)
+    pub public_key: Option<InstanceMsgPublicKey>,
+}
+
+/// A protocol message that can be sent over the network
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ProtocolMessage {
+    /// The protocol name
+    pub protocol: String,
+    /// Routing information for the message
+    pub routing: MessageRouting,
+    /// The actual message payload
+    pub payload: Vec<u8>,
+}
+
+/// Internal representation of a message to be sent
+#[derive(Debug)]
+pub struct OutboundMessage {
+    /// The message to be sent
+    pub message: ProtocolMessage,
+    /// How the message should be delivered
+    pub delivery: MessageDelivery,
+}
+
+impl Display for ParticipantId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "P{}", self.0)
+    }
+}
+
+impl Display for MessageRouting {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "msg={} round={} from={} to={:?}",
+            self.message_id,
+            self.round_id,
+            self.sender.id,
+            self.recipient.as_ref().map(|r| r.id)
+        )
+    }
+}
+
+impl Display for ParticipantInfo {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{} key={}",
+            self.id,
+            if self.public_key.is_some() {
+                "yes"
+            } else {
+                "no"
+            }
+        )
+    }
+}
+
+impl Display for ProtocolMessage {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{} payload_size={}", self.routing, self.payload.len())
+    }
+}
diff --git a/crates/runners/core/src/runner.rs b/crates/runners/core/src/runner.rs
index 055e275bf..70225555c 100644
--- a/crates/runners/core/src/runner.rs
+++ b/crates/runners/core/src/runner.rs
@@ -29,7 +29,7 @@ pub trait BackgroundService: Send + Sync + CloneableService + 'static {
 
 pub struct BlueprintRunner {
     pub config: Box<dyn BlueprintConfig>,
-    pub jobs: Vec<Box<dyn InitializableEventHandler + Send + 'static>>,
+    pub jobs: Vec<Box<dyn InitializableEventHandler + Send + Sync + 'static>>,
     pub env: GadgetConfiguration,
     pub background_services: Vec<Box<dyn BackgroundService>>,
 }
@@ -47,7 +47,7 @@ impl BlueprintRunner {
     pub fn job<J, T>(&mut self, job: J) -> &mut Self
     where
         J: Into<JobBuilder<T>>,
-        T: InitializableEventHandler + Send + 'static,
+        T: InitializableEventHandler + Send + Sync + 'static,
     {
         let JobBuilder { event_handler } = job.into();
         self.jobs.push(Box::new(event_handler));
diff --git a/crates/runners/tangle/src/tangle.rs b/crates/runners/tangle/src/tangle.rs
index 435406876..531545e0a 100644
--- a/crates/runners/tangle/src/tangle.rs
+++ b/crates/runners/tangle/src/tangle.rs
@@ -12,7 +12,7 @@ use gadget_std::string::ToString;
 use k256::elliptic_curve::sec1::ToEncodedPoint;
 use tangle_subxt::tangle_testnet_runtime::api;
 use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::PriceTargets as TanglePriceTargets;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::types::PriceTargets as TanglePriceTargets;
 use tangle_subxt::tangle_testnet_runtime::api::services::calls::types::register::RegistrationArgs;
 
 /// Wrapper for `tangle_subxt`'s [`PriceTargets`]
@@ -20,7 +20,7 @@ use tangle_subxt::tangle_testnet_runtime::api::services::calls::types::register:
 /// This provides a [`Default`] impl for a zeroed-out [`PriceTargets`].
 ///
 /// [`PriceTargets`]: tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::PriceTargets
-#[derive(Clone)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct PriceTargets(pub TanglePriceTargets);
 
 impl From<TanglePriceTargets> for PriceTargets {
@@ -41,7 +41,7 @@ impl Default for PriceTargets {
     }
 }
 
-#[derive(Clone, Default)]
+#[derive(Clone, Default, Debug, PartialEq, Eq)]
 pub struct TangleConfig {
     pub price_targets: PriceTargets,
     pub exit_after_register: bool,
@@ -179,10 +179,9 @@ pub async fn register_impl(
     let uncompressed_pk = decompress_pubkey(&ecdsa_key.0 .0).ok_or_else(|| {
         RunnerError::Other("Unable to convert compressed ECDSA key to uncompressed key".to_string())
     })?;
-
     let xt = api::tx().services().register(
         blueprint_id,
-        services::OperatorPreferences {
+        services::types::OperatorPreferences {
             key: uncompressed_pk,
             price_targets: price_targets.clone().0,
         },
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
index 64807d16a..5f8a1d351 100644
--- a/crates/sdk/Cargo.toml
+++ b/crates/sdk/Cargo.toml
@@ -44,9 +44,11 @@ tokio = { workspace = true, default-features = false }
 
 # Networking-related dependencies
 gadget-networking = { workspace = true, optional = true }
-gadget-client-networking = { workspace = true, optional = true }
 gadget-contexts = { workspace = true }
 gadget-context-derive = { workspace = true, optional = true }
+# Round-based protocol support
+gadget-networking-round-based-extension = { workspace = true, optional = true }
+
 
 # Optional dependencies for testing
 gadget-testing-utils = { workspace = true, optional = true }
@@ -82,10 +84,7 @@ web = [
 	"gadget-macros?/web",
 ]
 
-macros = [
-	"dep:gadget-macros",
-	"dep:gadget-context-derive",
-]
+macros = ["dep:gadget-macros", "dep:gadget-context-derive"]
 
 build = ["dep:blueprint-metadata", "dep:blueprint-build-utils"]
 
@@ -97,7 +96,7 @@ tangle = [
 	"gadget-macros?/tangle",
 	"gadget-testing-utils?/tangle",
 	"gadget-utils/tangle",
-	"gadget-event-listeners/tangle"
+	"gadget-event-listeners/tangle",
 ]
 
 evm = [
@@ -108,7 +107,7 @@ evm = [
 	"gadget-utils/evm",
 	"gadget-testing-utils?/anvil",
 	"gadget-macros?/evm",
-	"gadget-event-listeners/evm"
+	"gadget-event-listeners/evm",
 ]
 
 eigenlayer = [
@@ -120,40 +119,31 @@ eigenlayer = [
 	"gadget-testing-utils?/eigenlayer",
 	"gadget-utils/eigenlayer",
 	"gadget-macros?/eigenlayer",
-	"gadget-event-listeners/evm"
+	"gadget-event-listeners/evm",
 ]
 
-testing = [
-	"dep:gadget-testing-utils",
-	"dep:tempfile",
-	"std",
-]
+testing = ["dep:gadget-testing-utils", "dep:tempfile", "std"]
 
 networking = [
 	"dep:gadget-networking",
-	"dep:gadget-client-networking",
 	"gadget-contexts/networking",
-	"gadget-clients/networking",
 	"gadget-keystore/std",
 	"gadget-config/networking",
 	"gadget-macros?/networking",
 	"gadget-context-derive?/networking",
 ]
 
-networking-sp-core-ecdsa = [
-	"gadget-networking/sp-core-ecdsa"
-]
+networking-sp-core-ecdsa = ["gadget-networking/sp-core-ecdsa"]
 
-networking-sr25519 = [
-	"gadget-networking/sp-core-sr25519"
-]
+networking-sr25519 = ["gadget-networking/sp-core-sr25519"]
 
-networking-ed25519 = [
-	"gadget-networking/sp-core-ed25519"
-]
+networking-ed25519 = ["gadget-networking/sp-core-ed25519"]
 
 local-store = ["gadget-stores/local"]
 
-round-based-compat = ["gadget-networking/round-based-compat"]
+round-based-compat = ["dep:gadget-networking-round-based-extension"]
+
+cronjob = ["gadget-event-listeners/cronjob"]
 
-cronjob = ["gadget-event-listeners/cronjob"]
\ No newline at end of file
+[lints.clippy]
+large_enum_variant = "allow" # TODO
\ No newline at end of file
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
index d157535ba..9c31c4f19 100644
--- a/crates/sdk/src/lib.rs
+++ b/crates/sdk/src/lib.rs
@@ -33,6 +33,8 @@ pub mod build {
 pub mod networking {
     /// Networking utilities for blueprints
     pub use gadget_networking::*;
+    #[cfg(feature = "round-based-compat")]
+    pub use gadget_networking_round_based_extension as round_based_compat;
 }
 
 /// Event listener infrastructure for handling blueprint events
diff --git a/flake.lock b/flake.lock
index 63fc2a2c2..0dedf507d 100644
--- a/flake.lock
+++ b/flake.lock
@@ -28,11 +28,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1733130721,
-        "narHash": "sha256-FeL2dez6gE3/u+Dq5Ot4TA0EbonBRo2/7pTYNmBBTNY=",
+        "lastModified": 1738660302,
+        "narHash": "sha256-aLWyhJx2cO/M3/QLoDBpsObFfjC9e/VEN6HtaI0U6IA=",
         "owner": "shazow",
         "repo": "foundry.nix",
-        "rev": "277722b2fc52af44c0827c47f0f170c7a9b17b0e",
+        "rev": "33a209625b9e31227a5f11417e95a3ac7264d811",
         "type": "github"
       },
       "original": {
@@ -44,11 +44,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1733064805,
-        "narHash": "sha256-7NbtSLfZO0q7MXPl5hzA0sbVJt6pWxxtGWbaVUDDmjs=",
+        "lastModified": 1740019556,
+        "narHash": "sha256-vn285HxnnlHLWnv59Og7muqECNMS33mWLM14soFIv2g=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "31d66ae40417bb13765b0ad75dd200400e98de84",
+        "rev": "dad564433178067be1fbdfcce23b546254b6d641",
         "type": "github"
       },
       "original": {
@@ -73,11 +73,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1733106880,
-        "narHash": "sha256-aJmAIjZfWfPSWSExwrYBLRgXVvgF5LP1vaeUGOOIQ98=",
+        "lastModified": 1740104932,
+        "narHash": "sha256-FaN+HBAhOW1wAjxPI/Ko1DX0ax4ucHCZoMJ0dGMxm8o=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "e66c0d43abf5bdefb664c3583ca8994983c332ae",
+        "rev": "c932b3873a5d56126bc1f1416fb8a58315f86c17",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 1568804b4..59ec63992 100644
--- a/flake.nix
+++ b/flake.nix
@@ -19,10 +19,21 @@
     };
   };
 
-  outputs = { self, nixpkgs, rust-overlay, foundry, flake-utils }:
-    flake-utils.lib.eachDefaultSystem (system:
+  outputs =
+    {
+      self,
+      nixpkgs,
+      rust-overlay,
+      foundry,
+      flake-utils,
+    }:
+    flake-utils.lib.eachDefaultSystem (
+      system:
       let
-        overlays = [ (import rust-overlay) foundry.overlay ];
+        overlays = [
+          (import rust-overlay)
+          foundry.overlay
+        ];
         pkgs = import nixpkgs {
           inherit system overlays;
         };
@@ -41,7 +52,6 @@
             # Protocol Buffers
             pkgs.protobuf
             # Mold Linker for faster builds (only on Linux)
-            (lib.optionals pkgs.stdenv.isLinux pkgs.om4)
             (lib.optionals pkgs.stdenv.isLinux pkgs.mold)
             (lib.optionals pkgs.stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.Security)
             (lib.optionals pkgs.stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.SystemConfiguration)
@@ -66,7 +76,13 @@
           ];
           # Environment variables
           RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
-          LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.gmp pkgs.libclang pkgs.openssl.dev pkgs.stdenv.cc.cc ];
+          LD_LIBRARY_PATH = lib.makeLibraryPath [
+            pkgs.gmp
+            pkgs.libclang
+            pkgs.openssl.dev
+            pkgs.stdenv.cc.cc
+          ];
         };
-      });
+      }
+    );
 }


Test patch:
diff --git a/blueprints/examples/src/tests.rs b/blueprints/examples/src/tests.rs
index d65241e3d..a26c1b5ca 100644
--- a/blueprints/examples/src/tests.rs
+++ b/blueprints/examples/src/tests.rs
@@ -24,8 +24,7 @@ use blueprint_sdk::testing::tempfile;
 use blueprint_sdk::testing::utils::anvil::keys::{inject_anvil_key, ANVIL_PRIVATE_KEYS};
 use blueprint_sdk::testing::utils::anvil::{get_receipt, start_default_anvil_testnet};
 use blueprint_sdk::testing::utils::harness::TestHarness;
-use blueprint_sdk::testing::utils::runner::TestEnv;
-use blueprint_sdk::testing::utils::tangle::{InputValue, OutputValue, TangleTestHarness};
+use blueprint_sdk::testing::utils::tangle::{InputValue, TangleTestHarness};
 use blueprint_sdk::tokio;
 use blueprint_sdk::tokio::task::JoinHandle;
 use blueprint_sdk::tokio::time::timeout;
@@ -146,6 +145,7 @@ async fn test_eigenlayer_context() {
 }
 
 #[tokio::test]
+#[ignore] // TODO: waiting for the new event listener
 async fn test_periodic_web_poller() -> Result<()> {
     setup_log();
 
@@ -154,17 +154,22 @@ async fn test_periodic_web_poller() -> Result<()> {
     let harness = TangleTestHarness::setup(temp_dir).await?;
 
     // Setup service
-    let (mut test_env, service_id, _blueprint_id) = harness.setup_services(false).await?;
+    let (mut test_env, service_id, _blueprint_id) = harness.setup_services::<1>(false).await?;
 
     // Add the web poller job
-    test_env.add_job(crate::periodic_web_poller::constructor("*/5 * * * * *"));
+    test_env
+        .add_job(|_env| async move {
+            Ok::<_, ()>(crate::periodic_web_poller::constructor("*/5 * * * * *"))
+        })
+        .await
+        .unwrap();
 
     // Run the test environment
-    test_env.run_runner().await.unwrap();
+    test_env.start().await?;
 
     // Execute job and verify result
     let result = tokio::select! {
-        result = harness.execute_job(service_id, 1, vec![], vec![OutputValue::Uint64(1)]) => {
+        result = harness.submit_job(service_id, 1, vec![]) => {
             match result {
                 Ok(_) => {Ok(())},
                 Err(e) => Err(e),
@@ -193,32 +198,33 @@ async fn test_periodic_web_poller() -> Result<()> {
 }
 
 #[tokio::test]
+#[ignore] // TODO: waiting for the new event listener
 async fn test_services_context() -> Result<()> {
     setup_log();
 
     // Initialize test harness
     let temp_dir = tempfile::TempDir::new()?;
     let harness = TangleTestHarness::setup(temp_dir).await?;
-    let env = harness.env().clone();
 
     // Setup service
-    let (mut test_env, service_id, _blueprint_id) = harness.setup_services(false).await?;
+    let (mut test_env, service_id, _blueprint_id) = harness.setup_services::<1>(false).await?;
 
     // Add the raw tangle events job
-    test_env.add_job(crate::services_context::constructor(env.clone()).await?);
+    test_env
+        .add_job(|env| async move { crate::services_context::constructor(env).await })
+        .await?;
 
     // Run the test environment
     let _test_handle = tokio::spawn(async move {
-        test_env.run_runner().await.unwrap();
+        test_env.start().await.unwrap();
     });
 
     // Execute job and verify result
     let results = harness
-        .execute_job(
+        .submit_job(
             service_id,
             3,
             vec![InputValue::List(BoundedVec(vec![InputValue::Uint8(0)]))],
-            vec![OutputValue::Uint64(1)],
         )
         .await?;
 
@@ -227,6 +233,7 @@ async fn test_services_context() -> Result<()> {
 }
 
 #[tokio::test]
+#[ignore] // TODO: waiting for the new event listener
 async fn test_raw_tangle_events() -> Result<()> {
     setup_log();
 
@@ -236,20 +243,22 @@ async fn test_raw_tangle_events() -> Result<()> {
     let env = harness.env().clone();
 
     // Setup service
-    let (mut test_env, service_id, _blueprint_id) = harness.setup_services(false).await?;
+    let (mut test_env, service_id, _blueprint_id) = harness.setup_services::<1>(false).await?;
 
     // Add the raw tangle events job
-    test_env.add_job(crate::raw_tangle_events::constructor(env.clone()).await?);
+    // test_env
+    //     .add_job(|env| async move { crate::raw_tangle_events::constructor(env).await })
+    //     .await?;
 
     // Spawn the balance transfer task
-    let _handle = balance_transfer_event(env.clone()).await.unwrap();
+    let _handle = balance_transfer_event(env.clone()).await?;
 
     // Run the test environment
-    test_env.run_runner().await.unwrap();
+    test_env.start().await?;
 
     // Execute job and verify result
     let result = tokio::select! {
-        result = harness.execute_job(service_id, 2, vec![], vec![OutputValue::Uint64(1)]) => {
+        result = harness.submit_job(service_id, 2, vec![]) => {
             match result {
                 Ok(_) => {Ok(())},
                 Err(e) => Err(e),
diff --git a/blueprints/incredible-squaring/src/tests.rs b/blueprints/incredible-squaring/src/tests.rs
index 05c0eddc1..ed21b6f4d 100644
--- a/blueprints/incredible-squaring/src/tests.rs
+++ b/blueprints/incredible-squaring/src/tests.rs
@@ -2,10 +2,8 @@ use crate::{MyContext, XsquareEventHandler};
 use blueprint_sdk::logging::setup_log;
 use blueprint_sdk::testing::tempfile;
 use blueprint_sdk::testing::utils::harness::TestHarness;
-use blueprint_sdk::testing::utils::runner::TestEnv;
 use blueprint_sdk::testing::utils::tangle::{InputValue, OutputValue, TangleTestHarness};
 use color_eyre::Result;
-use std::time::Duration;
 
 #[tokio::test]
 async fn test_incredible_squaring() -> Result<()> {
@@ -15,84 +13,88 @@ async fn test_incredible_squaring() -> Result<()> {
     // Initialize test harness (node, keys, deployment)
     let temp_dir = tempfile::TempDir::new()?;
     let harness = TangleTestHarness::setup(temp_dir).await?;
-    let env = harness.env().clone();
-
-    // Create blueprint-specific context
-    let blueprint_ctx = MyContext {
-        env: env.clone(),
-        call_id: None,
-    };
-
-    // Initialize event handler
-    let handler = XsquareEventHandler::new(&env.clone(), blueprint_ctx)
-        .await
-        .unwrap();
 
     // Setup service
-    let (mut test_env, service_id, _blueprint_id) = harness.setup_services(false).await?;
-    test_env.add_job(handler);
-
-    test_env.run_runner().await.unwrap();
-
-    // Execute job and verify result
-    let results = harness
-        .execute_job(
-            service_id,
-            0,
-            vec![InputValue::Uint64(5)],
-            vec![OutputValue::Uint64(25)],
-        )
-        .await?;
-
-    assert_eq!(results.service_id, service_id);
-    Ok(())
-}
+    let (mut test_env, service_id, _blueprint_id) = harness.setup_services::<1>(false).await?;
+    test_env.initialize().await?;
 
-#[tokio::test]
-async fn test_pre_register_incredible_squaring() -> Result<()> {
-    setup_log();
-
-    // Initialize test harness (node, keys, deployment)
-    let temp_dir = tempfile::TempDir::new()?;
-    let harness = TangleTestHarness::setup(temp_dir).await?;
-    let env = harness.env().clone();
+    let nodes = test_env.node_handles().await;
+    let alice_node = &nodes[0];
 
     // Create blueprint-specific context
+    let env = alice_node.gadget_config().await;
     let blueprint_ctx = MyContext {
         env: env.clone(),
         call_id: None,
     };
 
-    // Initialize event handler
-    let handler = XsquareEventHandler::new(&env.clone(), blueprint_ctx)
-        .await
-        .unwrap();
-
-    // Setup service, but we don't register yet
-    let (mut test_env, _, blueprint_id) = harness.setup_services(true).await?;
-    test_env.add_job(handler);
+    // Initialize the event handler
+    let handler = XsquareEventHandler::new(&env, blueprint_ctx).await?;
 
-    // Run once for pre-registration
-    test_env.run_runner().await.unwrap();
+    // Add the job to the node, and start it
+    alice_node.add_job(handler).await;
+    test_env.start().await?;
 
-    tokio::time::sleep(Duration::from_secs(2)).await;
-
-    let service_id = harness.request_service(blueprint_id).await.unwrap();
-
-    // Run again to actually run the service, now that we have registered
-    test_env.run_runner().await.unwrap();
-
-    tokio::time::sleep(Duration::from_secs(2)).await;
-
-    // Execute job and verify result
-    let _results = harness
-        .execute_job(
-            service_id,
-            0,
-            vec![InputValue::Uint64(5)],
-            vec![OutputValue::Uint64(25)],
-        )
+    // Submit job and wait for execution
+    let job = harness
+        .submit_job(service_id, 0, vec![InputValue::Uint64(5)])
         .await?;
+    let results = harness.wait_for_job_execution(service_id, job).await?;
 
+    // Verify results match expected output
+    let results = harness.verify_job(results, vec![OutputValue::Uint64(25)])?;
+
+    assert_eq!(results.service_id, service_id);
     Ok(())
 }
+
+// #[tokio::test]
+// async fn test_pre_registration_incredible_squaring() -> Result<()> {
+//     setup_log();
+//
+//     // Initialize test harness (node, keys, deployment)
+//     let temp_dir = tempfile::TempDir::new()?;
+//     let harness = TangleTestHarness::setup(temp_dir).await?;
+//
+//     // Setup service, but we don't register yet
+//     let (mut test_env, service_id, blueprint_id) = harness.setup_services::<1>(true).await?;
+//     test_env.initialize().await?;
+//
+//     let nodes = test_env.node_handles().await;
+//     let alice_node = &nodes[0];
+//
+//     // Create blueprint-specific context
+//     let env = alice_node.gadget_config().await;
+//     let blueprint_ctx = MyContext {
+//         env: env.clone(),
+//         call_id: None,
+//     };
+//
+//     // Initialize the event handler
+//     let handler = XsquareEventHandler::new(&env, blueprint_ctx).await?;
+//
+//     // Add the job to the node, and run once for pre-registration
+//     alice_node.add_job(handler).await;
+//     test_env.start().await?;
+//
+//     tokio::time::sleep(Duration::from_secs(2)).await;
+//
+//     let service_id = harness.request_service(blueprint_id).await.unwrap();
+//
+//     // Run again to actually run the service, now that we have registered
+//     test_env.run_runner().await.unwrap();
+//
+//     tokio::time::sleep(Duration::from_secs(2)).await;
+//
+//     // Execute job and verify result
+//     let _results = harness
+//         .execute_job(
+//             service_id,
+//             0,
+//             vec![InputValue::Uint64(5)],
+//             vec![OutputValue::Uint64(25)],
+//         )
+//         .await?;
+//
+//     Ok(())
+// }
diff --git a/crates/blueprint/manager/src/sources/testing.rs b/crates/blueprint/manager/src/sources/testing.rs
index 80bb3b838..ac9567622 100644
--- a/crates/blueprint/manager/src/sources/testing.rs
+++ b/crates/blueprint/manager/src/sources/testing.rs
@@ -3,7 +3,7 @@ use crate::sources::BinarySourceFetcher;
 use async_trait::async_trait;
 use gadget_logging::trace;
 use std::path::PathBuf;
-use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::TestFetcher;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::gadget::TestFetcher;
 
 pub struct TestSourceFetcher {
     pub fetcher: TestFetcher,
diff --git a/crates/macros/context-derive/tests/ui/basic.rs b/crates/macros/context-derive/tests/ui/basic.rs
index 903a1c843..df8446b84 100644
--- a/crates/macros/context-derive/tests/ui/basic.rs
+++ b/crates/macros/context-derive/tests/ui/basic.rs
@@ -1,25 +1,16 @@
-use async_trait::async_trait;
 use blueprint_sdk::config::GadgetConfiguration;
 use blueprint_sdk::contexts::instrumented_evm_client::EvmInstrumentedClientContext as _;
 use blueprint_sdk::contexts::keystore::KeystoreContext as _;
-use blueprint_sdk::contexts::p2p::P2pContext as _;
 use blueprint_sdk::contexts::services::ServicesContext as _;
 use blueprint_sdk::contexts::tangle::TangleClientContext as _;
 use blueprint_sdk::macros::ext::clients::GadgetServicesClient as _;
-use blueprint_sdk::macros::ext::crypto::sp_core::SpEcdsa;
-use blueprint_sdk::macros::ext::keystore::backends::Backend;
-use blueprint_sdk::networking::networking::{Network, NetworkMultiplexer, ProtocolMessage};
-use blueprint_sdk::networking::{GossipMsgKeyPair, GossipMsgPublicKey};
-use blueprint_sdk::std::collections::BTreeMap;
 use blueprint_sdk::std::sync::Arc;
 use blueprint_sdk::stores::local_database::LocalDatabase;
 use gadget_context_derive::{
-    EVMProviderContext, KeystoreContext, P2pContext, ServicesContext, TangleClientContext,
+    EVMProviderContext, KeystoreContext, ServicesContext, TangleClientContext,
 };
-use round_based::ProtocolMessage as RoundBasedProtocolMessage;
-use serde::{Deserialize, Serialize};
 
-#[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext, P2pContext)]
+#[derive(KeystoreContext, EVMProviderContext, TangleClientContext, ServicesContext)]
 #[allow(dead_code)]
 struct MyContext {
     foo: String,
@@ -41,7 +32,7 @@ fn main() {
         };
 
         // Test existing context functions
-        let keystore = ctx.keystore();
+        let _keystore = ctx.keystore();
         let _evm_provider = ctx.evm_client();
         let tangle_client = ctx.tangle_client().await.unwrap();
         let _services_client = ctx.services_client().await;
@@ -50,74 +41,13 @@ fn main() {
             .current_service_operators([0; 32], 0)
             .await
             .unwrap();
-        let pub_key = keystore.generate::<SpEcdsa>(None).unwrap();
-        let pair = keystore.get_secret::<SpEcdsa>(&pub_key).unwrap();
-        let p2p_client =
-            ctx.p2p_client(String::from("Foo"), 1337, GossipMsgKeyPair(pair.0.clone()));
-
-        // Test MPC context utility functions
-        let _config = p2p_client.config();
-        let _protocol = p2p_client.network_protocol(None);
-
-        // Test MPC context functions
-
-        let mux = Arc::new(NetworkMultiplexer::new(StubNetwork));
-        let party_index = 0;
-        let task_hash = [0u8; 32];
-        let mut parties = BTreeMap::<u16, _>::new();
-        parties.insert(0, pair.public());
-
-        // Test network delivery wrapper creation
-        let _network_wrapper = p2p_client.create_network_delivery_wrapper::<StubMessage>(
-            mux.clone(),
-            party_index,
-            task_hash,
-            parties.clone(),
-        );
-
-        // TODO: Test party index retrieval
-        // let _party_idx = ctx.get_party_index().await;
-
-        // TODO: Test participants retrieval
-        // let _participants = ctx.get_participants(&tangle_client).await;
 
         // Test blueprint ID retrieval
         let _blueprint_id = tangle_client.blueprint_id();
 
         // Test party index and operators retrieval
         let _party_idx_ops = tangle_client.get_party_index_and_operators().await;
-
-        // TODO: Test service operators ECDSA keys retrieval
-        // let _operator_keys = ctx.current_service_operators_ecdsa_keys().await;
-
-        // TODO: Test current call ID retrieval
-        // let _call_id = tangle_client.current_call_id().await;
     };
 
     drop(body);
 }
-
-#[derive(RoundBasedProtocolMessage, Clone, Serialize, Deserialize)]
-enum StubMessage {}
-
-#[allow(dead_code)]
-struct StubNetwork;
-
-#[async_trait]
-impl Network for StubNetwork {
-    async fn next_message(&self) -> Option<ProtocolMessage> {
-        None
-    }
-
-    async fn send_message(
-        &self,
-        message: ProtocolMessage,
-    ) -> Result<(), blueprint_sdk::networking::error::Error> {
-        drop(message);
-        Ok(())
-    }
-
-    fn public_id(&self) -> GossipMsgPublicKey {
-        todo!()
-    }
-}
diff --git a/crates/networking/extensions/round-based/tests/common/mod.rs b/crates/networking/extensions/round-based/tests/common/mod.rs
new file mode 100644
index 000000000..f78471854
--- /dev/null
+++ b/crates/networking/extensions/round-based/tests/common/mod.rs
@@ -0,0 +1,241 @@
+#![allow(dead_code)]
+use gadget_crypto::KeyType;
+use gadget_networking::{
+    key_types::{Curve, InstanceMsgKeyPair, InstanceMsgPublicKey},
+    service_handle::NetworkServiceHandle,
+    NetworkConfig, NetworkService,
+};
+use libp2p::{
+    identity::{self, Keypair},
+    Multiaddr, PeerId,
+};
+use std::string::ToString;
+use std::{collections::HashSet, time::Duration};
+use tokio::time::timeout;
+use tracing::info;
+
+pub fn init_tracing() {
+    let _ = tracing_subscriber::fmt()
+        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
+        .with_target(true)
+        .with_thread_ids(false)
+        .with_file(true)
+        .with_line_number(true)
+        .try_init();
+}
+
+/// Test node configuration for network tests
+pub struct TestNode {
+    pub service: Option<NetworkService>,
+    pub peer_id: PeerId,
+    pub listen_addr: Option<Multiaddr>,
+    pub instance_key_pair: InstanceMsgKeyPair,
+    pub local_key: Keypair,
+}
+
+impl TestNode {
+    /// Create a new test node with auto-generated keys
+    pub fn new(
+        network_name: &str,
+        instance_id: &str,
+        allowed_keys: HashSet<InstanceMsgPublicKey>,
+        bootstrap_peers: Vec<Multiaddr>,
+    ) -> Self {
+        Self::new_with_keys(
+            network_name,
+            instance_id,
+            allowed_keys,
+            bootstrap_peers,
+            None,
+            None,
+        )
+    }
+
+    /// Create a new test node with specified keys
+    pub fn new_with_keys(
+        network_name: &str,
+        instance_id: &str,
+        allowed_keys: HashSet<InstanceMsgPublicKey>,
+        bootstrap_peers: Vec<Multiaddr>,
+        instance_key_pair: Option<InstanceMsgKeyPair>,
+        local_key: Option<Keypair>,
+    ) -> Self {
+        let local_key = local_key.unwrap_or_else(identity::Keypair::generate_ed25519);
+        let peer_id = local_key.public().to_peer_id();
+
+        // Bind to all interfaces instead of just localhost
+        let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/0".parse().unwrap();
+        info!("Creating test node {peer_id} with TCP address: {listen_addr}");
+
+        let instance_key_pair =
+            instance_key_pair.unwrap_or_else(|| Curve::generate_with_seed(None).unwrap());
+
+        let config = NetworkConfig {
+            network_name: network_name.to_string(),
+            instance_id: instance_id.to_string(),
+            instance_key_pair: instance_key_pair.clone(),
+            local_key: local_key.clone(),
+            listen_addr: listen_addr.clone(),
+            target_peer_count: 10,
+            bootstrap_peers,
+            enable_mdns: true,
+            enable_kademlia: true,
+        };
+
+        let service =
+            NetworkService::new(config, allowed_keys).expect("Failed to create network service");
+
+        Self {
+            service: Some(service),
+            peer_id,
+            listen_addr: None,
+            instance_key_pair,
+            local_key,
+        }
+    }
+
+    /// Start the node and wait for it to be fully initialized
+    pub async fn start(&mut self) -> Result<NetworkServiceHandle, &'static str> {
+        // Take ownership of the service
+        let service = self.service.take().ok_or("Service already started")?;
+        let handle = service.start();
+
+        // Wait for the node to be fully initialized
+        let timeout_duration = Duration::from_secs(10); // Increased timeout
+        match timeout(timeout_duration, async {
+            // First wait for the listening address
+            while self.listen_addr.is_none() {
+                if let Some(addr) = handle.get_listen_addr() {
+                    info!("Node {} listening on {}", self.peer_id, addr);
+                    self.listen_addr = Some(addr.clone());
+
+                    // Extract port from multiaddr
+                    let addr_str = addr.to_string();
+                    let port = addr_str.split('/').nth(4).unwrap_or("0").to_string();
+
+                    // Try localhost first
+                    let localhost_addr = format!("127.0.0.1:{}", port);
+                    match tokio::net::TcpStream::connect(&localhost_addr).await {
+                        Ok(_) => {
+                            info!("Successfully verified localhost port for {}", self.peer_id);
+                            break;
+                        }
+                        Err(e) => {
+                            info!("Localhost port not ready for {}: {}", self.peer_id, e);
+                            // Try external IP
+                            let external_addr = format!("10.0.1.142:{}", port);
+                            match tokio::net::TcpStream::connect(&external_addr).await {
+                                Ok(_) => {
+                                    info!(
+                                        "Successfully verified external port for {}",
+                                        self.peer_id
+                                    );
+                                    break;
+                                }
+                                Err(e) => {
+                                    info!("External port not ready for {}: {}", self.peer_id, e);
+                                    tokio::time::sleep(Duration::from_millis(100)).await;
+                                    continue;
+                                }
+                            }
+                        }
+                    }
+                }
+                tokio::time::sleep(Duration::from_millis(100)).await;
+            }
+
+            // Give the node a moment to initialize protocols
+            tokio::time::sleep(Duration::from_millis(500)).await;
+
+            Ok::<(), &'static str>(())
+        })
+        .await
+        {
+            Ok(Ok(())) => {
+                info!("Node {} fully initialized", self.peer_id);
+                Ok(handle)
+            }
+            Ok(Err(e)) => Err(e),
+            Err(_) => Err("Timeout waiting for node to initialize"),
+        }
+    }
+
+    /// Get the actual listening address
+    pub fn get_listen_addr(&self) -> Option<Multiaddr> {
+        self.listen_addr.clone()
+    }
+}
+
+/// Wait for a condition with timeout
+pub async fn wait_for_condition<F>(timeout: Duration, mut condition: F) -> Result<(), &'static str>
+where
+    F: FnMut() -> bool,
+{
+    let start = std::time::Instant::now();
+    while !condition() {
+        if start.elapsed() > timeout {
+            return Err("Timeout waiting for condition");
+        }
+        tokio::time::sleep(Duration::from_millis(100)).await;
+    }
+    Ok(())
+}
+
+/// Wait for peers to discover each other
+pub async fn wait_for_peer_discovery(
+    handles: &[&NetworkServiceHandle],
+    timeout: Duration,
+) -> Result<(), &'static str> {
+    info!("Waiting for peer discovery...");
+
+    wait_for_condition(timeout, || {
+        for (i, handle1) in handles.iter().enumerate() {
+            for (j, handle2) in handles.iter().enumerate() {
+                if i != j
+                    && !handle1
+                        .peers()
+                        .iter()
+                        .any(|id| *id == handle2.local_peer_id)
+                {
+                    return false;
+                }
+            }
+        }
+        true
+    })
+    .await
+}
+
+/// Wait for peer info to be updated
+pub async fn wait_for_peer_info(
+    handle1: &NetworkServiceHandle,
+    handle2: &NetworkServiceHandle,
+    timeout: Duration,
+) {
+    info!("Waiting for identify info...");
+
+    match tokio::time::timeout(timeout, async {
+        loop {
+            let peer_info1 = handle1.peer_info(&handle2.local_peer_id);
+            let peer_info2 = handle2.peer_info(&handle1.local_peer_id);
+
+            if let Some(peer_info) = peer_info1 {
+                if peer_info.identify_info.is_some() {
+                    // Also verify reverse direction
+                    if let Some(peer_info) = peer_info2 {
+                        if peer_info.identify_info.is_some() {
+                            info!("Identify info populated in both directions");
+                            break;
+                        }
+                    }
+                }
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    {
+        Ok(()) => info!("Peer info updated successfully in both directions"),
+        Err(e) => panic!("Peer info update timed out: {e}"),
+    }
+}
diff --git a/crates/networking/extensions/round-based/tests/rand_protocol.rs b/crates/networking/extensions/round-based/tests/rand_protocol.rs
new file mode 100644
index 000000000..45b1e31ff
--- /dev/null
+++ b/crates/networking/extensions/round-based/tests/rand_protocol.rs
@@ -0,0 +1,296 @@
+//! Simple protocol in which parties cooperate to generate randomness
+
+mod common;
+
+use serde::{Deserialize, Serialize};
+use sha2::{digest::Output, Digest, Sha256};
+
+use round_based::rounds_router::{
+    simple_store::{RoundInput, RoundInputError},
+    CompleteRoundError, RoundsRouter,
+};
+use round_based::{Delivery, Mpc, MpcParty, MsgId, Outgoing, PartyIndex, ProtocolMessage, SinkExt};
+
+/// Protocol message
+#[derive(Clone, Debug, PartialEq, ProtocolMessage, Serialize, Deserialize)]
+pub enum Msg {
+    /// Round 1
+    CommitMsg(CommitMsg),
+    /// Round 2
+    DecommitMsg(DecommitMsg),
+}
+
+/// Message from round 1
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct CommitMsg {
+    /// Party commitment
+    pub commitment: Output<Sha256>,
+}
+
+/// Message from round 2
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct DecommitMsg {
+    /// Randomness generated by party
+    pub randomness: [u8; 32],
+}
+
+/// Carries out the randomness generation protocol
+#[tracing::instrument(skip(party, rng))]
+pub async fn protocol_of_random_generation<R, M>(
+    party: M,
+    i: PartyIndex,
+    n: u16,
+    mut rng: R,
+) -> Result<[u8; 32], Error<M::ReceiveError, M::SendError>>
+where
+    M: Mpc<ProtocolMessage = Msg>,
+    R: rand::RngCore,
+{
+    let MpcParty { delivery, .. } = party.into_party();
+    let (incoming, mut outgoing) = delivery.split();
+
+    // Define rounds
+    let mut rounds = RoundsRouter::<Msg>::builder();
+    let round1 = rounds.add_round(RoundInput::<CommitMsg>::broadcast(i, n));
+    let round2 = rounds.add_round(RoundInput::<DecommitMsg>::broadcast(i, n));
+    let mut rounds = rounds.listen(incoming);
+
+    // --- The Protocol ---
+
+    // 1. Generate local randomness
+    let mut local_randomness = [0u8; 32];
+    rng.fill_bytes(&mut local_randomness);
+
+    tracing::debug!(local_randomness = %hex::encode(local_randomness), "Generated local randomness");
+
+    // 2. Commit local randomness (broadcast m=sha256(randomness))
+    let commitment = Sha256::digest(local_randomness);
+    tracing::debug!(commitment = %hex::encode(commitment), "Committed local randomness");
+    outgoing
+        .send(Outgoing::broadcast(Msg::CommitMsg(CommitMsg {
+            commitment,
+        })))
+        .await
+        .map_err(Error::Round1Send)?;
+
+    tracing::debug!("Sent commitment and waiting for others to send theirs");
+
+    // 3. Receive committed randomness from other parties
+    let commitments = rounds
+        .complete(round1)
+        .await
+        .map_err(Error::Round1Receive)?;
+
+    tracing::debug!("Received commitments from all parties");
+
+    // 4. Open local randomness
+    tracing::debug!("Opening local randomness");
+    outgoing
+        .send(Outgoing::broadcast(Msg::DecommitMsg(DecommitMsg {
+            randomness: local_randomness,
+        })))
+        .await
+        .map_err(Error::Round2Send)?;
+
+    tracing::debug!("Sent decommitment and waiting for others to send theirs");
+
+    // 5. Receive opened local randomness from other parties, verify them, and output protocol randomness
+    let randomness = rounds
+        .complete(round2)
+        .await
+        .map_err(Error::Round2Receive)?;
+
+    tracing::debug!("Received decommitments from all parties");
+
+    let mut guilty_parties = vec![];
+    let mut output = local_randomness;
+    for ((party_i, com_msg_id, commit), (_, decom_msg_id, decommit)) in commitments
+        .into_iter_indexed()
+        .zip(randomness.into_iter_indexed())
+    {
+        let commitment_expected = Sha256::digest(decommit.randomness);
+        if commit.commitment != commitment_expected {
+            guilty_parties.push(Blame {
+                guilty_party: party_i,
+                commitment_msg: com_msg_id,
+                decommitment_msg: decom_msg_id,
+            });
+            continue;
+        }
+
+        output
+            .iter_mut()
+            .zip(decommit.randomness)
+            .for_each(|(x, r)| *x ^= r);
+    }
+
+    if guilty_parties.is_empty() {
+        tracing::debug!(output = %hex::encode(output), "Generated randomness");
+        tracing::info!("Randomness generation protocol completed successfully.");
+        Ok(output)
+    } else {
+        tracing::error!(guilty_parties = ?guilty_parties, "Some parties cheated");
+        Err(Error::PartiesOpenedRandomnessDoesntMatchCommitment { guilty_parties })
+    }
+}
+
+/// Protocol error
+#[derive(Debug, thiserror::Error)]
+pub enum Error<RecvErr, SendErr> {
+    /// Couldn't send a message in the first round
+    #[error("send a message at round 1")]
+    Round1Send(#[source] SendErr),
+    /// Couldn't receive a message in the first round
+    #[error("receive messages at round 1")]
+    Round1Receive(#[source] CompleteRoundError<RoundInputError, RecvErr>),
+    /// Couldn't send a message in the second round
+    #[error("send a message at round 2")]
+    Round2Send(#[source] SendErr),
+    /// Couldn't receive a message in the second round
+    #[error("receive messages at round 2")]
+    Round2Receive(#[source] CompleteRoundError<RoundInputError, RecvErr>),
+
+    /// Some of the parties cheated
+    #[error("malicious parties: {guilty_parties:?}")]
+    PartiesOpenedRandomnessDoesntMatchCommitment {
+        /// List of cheated parties
+        guilty_parties: Vec<Blame>,
+    },
+}
+
+/// Blames a party in cheating during the protocol
+#[derive(Debug)]
+pub struct Blame {
+    /// Index of the cheated party
+    pub guilty_party: PartyIndex,
+    /// ID of the message that party sent in the first round
+    pub commitment_msg: MsgId,
+    /// ID of the message that party sent in the second round
+    pub decommitment_msg: MsgId,
+}
+
+#[cfg(test)]
+mod tests {
+    use std::collections::{HashMap, HashSet};
+    use std::time::Duration;
+
+    use super::common::*;
+    use gadget_networking::{Curve, KeyType};
+    use gadget_networking_round_based_extension::RoundBasedNetworkAdapter;
+    use round_based::MpcParty;
+    use tracing::{debug, info};
+
+    use super::protocol_of_random_generation;
+
+    #[test]
+    fn simulation() {
+        let mut rng = rand_dev::DevRng::new();
+
+        let n: u16 = 5;
+
+        let randomness = round_based::sim::run_with_setup(
+            core::iter::repeat_with(|| rng.fork()).take(n.into()),
+            |i, party, rng| protocol_of_random_generation(party, i, n, rng),
+        )
+        .unwrap()
+        .expect_ok()
+        .expect_eq();
+
+        std::println!("Output randomness: {}", hex::encode(randomness));
+    }
+
+    #[tokio::test]
+    async fn simulation_async() {
+        let mut rng = rand_dev::DevRng::new();
+
+        let n: u16 = 5;
+
+        let randomness = round_based::sim::async_env::run_with_setup(
+            core::iter::repeat_with(|| rng.fork()).take(n.into()),
+            |i, party, rng| protocol_of_random_generation(party, i, n, rng),
+        )
+        .await
+        .expect_ok()
+        .expect_eq();
+
+        std::println!("Output randomness: {}", hex::encode(randomness));
+    }
+
+    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+    async fn p2p_networking() {
+        init_tracing();
+        let network_name = "rand-test-network";
+        let instance_id = "rand-test-instance";
+
+        // Generate node2's key pair first
+        let instance_key_pair2 = Curve::generate_with_seed(None).unwrap();
+        let mut allowed_keys1 = HashSet::new();
+        allowed_keys1.insert(instance_key_pair2.public());
+
+        // Create node1 with node2's key whitelisted
+        let mut node1 = TestNode::new(network_name, instance_id, allowed_keys1, vec![]);
+
+        // Create node2 with node1's key whitelisted and pre-generated key
+        let mut allowed_keys2 = HashSet::new();
+        allowed_keys2.insert(node1.instance_key_pair.public());
+        let mut node2 = TestNode::new_with_keys(
+            network_name,
+            instance_id,
+            allowed_keys2,
+            vec![],
+            Some(instance_key_pair2),
+            None,
+        );
+
+        info!("Starting nodes");
+        // Start both nodes - this should trigger automatic handshake
+        let handle1 = node1.start().await.expect("Failed to start node1");
+        let handle2 = node2.start().await.expect("Failed to start node2");
+
+        wait_for_peer_discovery(&[&handle1, &handle2], Duration::from_secs(5))
+            .await
+            .unwrap();
+
+        let parties = HashMap::from_iter([
+            (0, node1.instance_key_pair.public()),
+            (1, node2.instance_key_pair.public()),
+        ]);
+
+        let node1_network = RoundBasedNetworkAdapter::new(handle1, 0, parties.clone(), instance_id);
+        let node2_network = RoundBasedNetworkAdapter::new(handle2, 1, parties, instance_id);
+
+        let mut tasks = vec![];
+        tasks.push(tokio::spawn(async move {
+            let mut rng = rand_dev::DevRng::new();
+            let mpc_party = MpcParty::connected(node1_network);
+            let randomness = protocol_of_random_generation(mpc_party, 0, 2, &mut rng)
+                .await
+                .expect("Failed to generate randomness");
+            debug!("Node1 generated randomness: {:?}", randomness);
+            randomness
+        }));
+
+        tasks.push(tokio::spawn(async move {
+            let mut rng = rand_dev::DevRng::new();
+            let mpc_party = MpcParty::connected(node2_network);
+            let randomness = protocol_of_random_generation(mpc_party, 1, 2, &mut rng)
+                .await
+                .expect("Failed to generate randomness");
+            debug!("Node2 generated randomness: {:?}", randomness);
+            randomness
+        }));
+
+        let results = futures::future::join_all(tasks).await;
+
+        for result in results {
+            match result {
+                Ok(randomness) => {
+                    debug!("Randomness result: {:?}", randomness);
+                }
+                Err(e) => {
+                    panic!("Error in randomness generation: {:?}", e);
+                }
+            }
+        }
+    }
+}
diff --git a/crates/networking/src/test_helpers.rs b/crates/networking/src/test_helpers.rs
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/crates/networking/src/test_helpers.rs
@@ -0,0 +1,1 @@
+
diff --git a/crates/networking/src/tests/blueprint_protocol.rs b/crates/networking/src/tests/blueprint_protocol.rs
new file mode 100644
index 000000000..040ee064d
--- /dev/null
+++ b/crates/networking/src/tests/blueprint_protocol.rs
@@ -0,0 +1,564 @@
+// TODO
+// use gadget_crypto::KeyType;
+// use libp2p::PeerId;
+// use serde::{Deserialize, Serialize};
+// use std::{collections::HashSet, time::Duration};
+// use tokio::time::timeout;
+// use tracing::info;
+//
+// use crate::{
+//     blueprint_protocol::{InstanceMessageRequest, InstanceMessageResponse},
+//     key_types::{Curve, InstanceMsgKeyPair, InstanceMsgPublicKey},
+//     service::NetworkMessage,
+//     service_handle::NetworkServiceHandle,
+//     tests::{
+//         create_whitelisted_nodes, wait_for_all_handshakes, wait_for_handshake_completion, TestNode,
+//     },
+//     types::{MessageRouting, ParticipantId, ParticipantInfo, ProtocolMessage},
+// };
+//
+// const TEST_TIMEOUT: Duration = Duration::from_secs(10);
+// const PROTOCOL_NAME: &str = "summation/1.0.0";
+//
+// // Protocol message types
+// #[derive(Debug, Clone, Serialize, Deserialize)]
+// enum SummationMessage {
+//     Number(u64),
+//     Verification { sum: u64 },
+// }
+//
+// // Helper to create a protocol message
+// fn create_protocol_message(
+//     protocol: &str,
+//     msg: SummationMessage,
+//     sender: &NetworkServiceHandle,
+//     recipient: Option<PeerId>,
+// ) -> ProtocolMessage {
+//     ProtocolMessage {
+//         protocol: protocol.to_string(),
+//         routing: MessageRouting {
+//             message_id: 0,
+//             round_id: 0,
+//             sender: ParticipantInfo {
+//                 id: ParticipantId(0),
+//                 public_key: None,
+//             },
+//             recipient: recipient.map(|peer_id| ParticipantInfo {
+//                 id: ParticipantId(0),
+//                 public_key: None,
+//             }),
+//         },
+//         payload: bincode::serialize(&msg).expect("Failed to serialize message"),
+//     }
+// }
+//
+// // Helper to extract number from message
+// fn extract_number_from_message(msg: &ProtocolMessage) -> u64 {
+//     match bincode::deserialize::<SummationMessage>(&msg.payload).expect("Failed to deserialize") {
+//         SummationMessage::Number(n) => n,
+//         _ => panic!("Expected number message"),
+//     }
+// }
+//
+// // Helper to extract sum from verification message
+// fn extract_sum_from_verification(msg: &ProtocolMessage) -> u64 {
+//     match bincode::deserialize::<SummationMessage>(&msg.payload).expect("Failed to deserialize") {
+//         SummationMessage::Verification { sum } => sum,
+//         _ => panic!("Expected verification message"),
+//     }
+// }
+//
+// #[tokio::test]
+// async fn test_summation_protocol_basic() {
+//     super::init_tracing();
+//     info!("Starting summation protocol test");
+//
+//     // Create nodes with whitelisted keys
+//     let instance_key_pair2 = Curve::generate_with_seed(None).unwrap();
+//     let mut allowed_keys1 = HashSet::new();
+//     allowed_keys1.insert(instance_key_pair2.public());
+//
+//     let mut node1 = TestNode::new("test-net", "sum-test", allowed_keys1, vec![]).await;
+//
+//     let mut allowed_keys2 = HashSet::new();
+//     allowed_keys2.insert(node1.instance_key_pair.public());
+//     let mut node2 = TestNode::new_with_keys(
+//         "test-net",
+//         "sum-test",
+//         allowed_keys2,
+//         vec![],
+//         Some(instance_key_pair2),
+//         None,
+//     )
+//     .await;
+//
+//     info!("Starting nodes");
+//     let mut handle1 = node1.start().await.expect("Failed to start node1");
+//     let mut handle2 = node2.start().await.expect("Failed to start node2");
+//
+//     info!("Waiting for handshake completion");
+//     wait_for_handshake_completion(&handle1, &handle2, TEST_TIMEOUT).await;
+//
+//     // Generate test numbers
+//     let num1 = 42;
+//     let num2 = 58;
+//     let expected_sum = num1 + num2;
+//
+//     info!("Sending numbers via gossip");
+//     // Send numbers via gossip
+//     handle1
+//         .send(create_protocol_message(
+//             PROTOCOL_NAME,
+//             SummationMessage::Number(num1),
+//             &handle1,
+//             None,
+//         ))
+//         .expect("Failed to send number from node1");
+//
+//     handle2
+//         .send(create_protocol_message(
+//             PROTOCOL_NAME,
+//             SummationMessage::Number(num2),
+//             &handle2,
+//             None,
+//         ))
+//         .expect("Failed to send number from node2");
+//
+//     info!("Waiting for messages to be processed");
+//     // Wait for messages and compute sums
+//     let mut sum1 = 0;
+//     let mut sum2 = 0;
+//     let mut node1_received = false;
+//     let mut node2_received = false;
+//
+//     timeout(TEST_TIMEOUT, async {
+//         loop {
+//             // Process incoming messages
+//             if let Some(msg) = handle1.next_protocol_message() {
+//                 if !node1_received {
+//                     sum1 += extract_number_from_message(&msg);
+//                     node1_received = true;
+//                 }
+//             }
+//             if let Some(msg) = handle2.next_protocol_message() {
+//                 if !node2_received {
+//                     sum2 += extract_number_from_message(&msg);
+//                     node2_received = true;
+//                 }
+//             }
+//
+//             // Check if both nodes have received messages
+//             if node1_received && node2_received {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for summation completion");
+//
+//     info!("Verifying sums via P2P messages");
+//     // Verify sums via P2P messages
+//     handle1
+//         .send(create_protocol_message(
+//             PROTOCOL_NAME,
+//             SummationMessage::Verification { sum: sum1 },
+//             &handle1,
+//             Some(node2.peer_id),
+//         ))
+//         .expect("Failed to send verification from node1");
+//
+//     handle2
+//         .send(create_protocol_message(
+//             PROTOCOL_NAME,
+//             SummationMessage::Verification { sum: sum2 },
+//             &handle2,
+//             Some(node1.peer_id),
+//         ))
+//         .expect("Failed to send verification from node2");
+//
+//     info!("Waiting for verification messages");
+//     // Wait for verification messages
+//     timeout(TEST_TIMEOUT, async {
+//         let mut node1_verified = false;
+//         let mut node2_verified = false;
+//
+//         loop {
+//             // Process verification messages
+//             if let Some(msg) = handle1.next_protocol_message() {
+//                 if !node1_verified {
+//                     assert_eq!(extract_sum_from_verification(&msg), expected_sum);
+//                     node1_verified = true;
+//                 }
+//             }
+//             if let Some(msg) = handle2.next_protocol_message() {
+//                 if !node2_verified {
+//                     assert_eq!(extract_sum_from_verification(&msg), expected_sum);
+//                     node2_verified = true;
+//                 }
+//             }
+//
+//             if node1_verified && node2_verified {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for verification completion");
+//
+//     info!("Summation protocol test completed successfully");
+// }
+//
+// #[tokio::test]
+// async fn test_summation_protocol_multi_node() {
+//     super::init_tracing();
+//     info!("Starting multi-node summation protocol test");
+//
+//     // Create 3 nodes with whitelisted keys
+//     info!("Creating whitelisted nodes");
+//     let mut nodes = create_whitelisted_nodes(3).await;
+//     info!("Created {} nodes successfully", nodes.len());
+//
+//     // Start all nodes
+//     info!("Starting all nodes");
+//     let mut handles = Vec::new();
+//     for (i, node) in nodes.iter_mut().enumerate() {
+//         info!("Starting node {}", i);
+//         handles.push(node.start().await.expect("Failed to start node"));
+//         info!("Node {} started successfully", i);
+//     }
+//
+//     // Convert handles to mutable references
+//     info!("Converting handles to mutable references");
+//     let mut handles: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+//     let handles_len = handles.len();
+//     info!("Converted {} handles", handles_len);
+//
+//     // Wait for all handshakes to complete
+//     info!(
+//         "Waiting for handshake completion between {} nodes",
+//         handles_len
+//     );
+//     wait_for_all_handshakes(&handles, TEST_TIMEOUT).await;
+//     info!("All handshakes completed successfully");
+//
+//     // Generate test numbers
+//     let numbers = vec![42, 58, 100];
+//     let expected_sum: u64 = numbers.iter().sum();
+//     info!(
+//         "Generated test numbers: {:?}, expected sum: {}",
+//         numbers, expected_sum
+//     );
+//
+//     info!("Sending numbers via gossip");
+//     // Each node broadcasts its number
+//     for (i, handle) in handles.iter().enumerate() {
+//         info!("Node {} broadcasting number {}", i, numbers[i]);
+//         handle
+//             .send(create_protocol_message(
+//                 PROTOCOL_NAME,
+//                 SummationMessage::Number(numbers[i]),
+//                 handle,
+//                 None,
+//             ))
+//             .expect("Failed to send number");
+//         info!("Node {} successfully broadcast its number", i);
+//         // Add a small delay between broadcasts to avoid message collisions
+//         tokio::time::sleep(Duration::from_millis(100)).await;
+//     }
+//
+//     info!("Waiting for messages to be processed");
+//     // Wait for all nodes to receive all numbers
+//     let mut sums = vec![0; handles_len];
+//     let mut received = vec![0; handles_len];
+//
+//     timeout(TEST_TIMEOUT, async {
+//         loop {
+//             for (i, handle) in handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     if received[i] < handles_len - 1 {
+//                         let num = extract_number_from_message(&msg);
+//                         sums[i] += num;
+//                         received[i] += 1;
+//                         info!(
+//                             "Node {} received number {}, total sum: {}, received count: {}",
+//                             i, num, sums[i], received[i]
+//                         );
+//                     }
+//                 }
+//             }
+//
+//             let all_received = received.iter().all(|&r| r == handles_len - 1);
+//             info!(
+//                 "Current received counts: {:?}, target count: {}",
+//                 received,
+//                 handles_len - 1
+//             );
+//             if all_received {
+//                 info!("All nodes have received all numbers");
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for summation completion");
+//
+//     info!("Verifying sums via P2P messages");
+//     info!("Final sums: {:?}", sums);
+//     // Each node verifies with every other node
+//     for (i, sender) in handles.iter().enumerate() {
+//         for (j, recipient) in handles.iter().enumerate() {
+//             if i != j {
+//                 info!(
+//                     "Node {} sending verification sum {} to node {}",
+//                     i, sums[i], j
+//                 );
+//                 sender
+//                     .send(create_protocol_message(
+//                         PROTOCOL_NAME,
+//                         SummationMessage::Verification { sum: sums[i] },
+//                         sender,
+//                         Some(recipient.local_peer_id),
+//                     ))
+//                     .expect("Failed to send verification");
+//             }
+//         }
+//     }
+//
+//     info!("Waiting for verification messages");
+//     // Wait for all verifications
+//     timeout(TEST_TIMEOUT, async {
+//         let mut verified = vec![0; handles_len];
+//         loop {
+//             for (i, handle) in handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     if verified[i] < handles_len - 1 {
+//                         let sum = extract_sum_from_verification(&msg);
+//                         info!(
+//                             "Node {} received verification sum {}, expected {}",
+//                             i, sum, expected_sum
+//                         );
+//                         assert_eq!(sum, expected_sum);
+//                         verified[i] += 1;
+//                         info!("Node {} verification count: {}", i, verified[i]);
+//                     }
+//                 }
+//             }
+//
+//             let all_verified = verified.iter().all(|&v| v == handles_len - 1);
+//             info!("Current verification counts: {:?}", verified);
+//             if all_verified {
+//                 info!("All nodes have verified all sums");
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for verification completion");
+//
+//     info!("Multi-node summation protocol test completed successfully");
+// }
+//
+// #[tokio::test]
+// async fn test_summation_protocol_late_join() {
+//     super::init_tracing();
+//     info!("Starting late join summation protocol test");
+//
+//     // Create 3 nodes but only start 2 initially
+//     let mut nodes = create_whitelisted_nodes(3).await;
+//
+//     // Start first two nodes
+//     let mut handles = Vec::new();
+//     for node in nodes[..2].iter_mut() {
+//         handles.push(node.start().await.expect("Failed to start node"));
+//     }
+//
+//     // Convert handles to mutable references
+//     let handles_refs: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+//
+//     // Wait for initial handshakes
+//     info!("Waiting for initial handshake completion");
+//     wait_for_all_handshakes(&handles_refs, TEST_TIMEOUT).await;
+//
+//     // Initial nodes send their numbers
+//     let numbers = vec![42, 58, 100];
+//     let _expected_sum: u64 = numbers.iter().sum();
+//
+//     info!("Initial nodes sending numbers");
+//     for (i, handle) in handles.iter().enumerate() {
+//         handle
+//             .send(create_protocol_message(
+//                 PROTOCOL_NAME,
+//                 SummationMessage::Number(numbers[i]),
+//                 handle,
+//                 None,
+//             ))
+//             .expect("Failed to send number");
+//     }
+//
+//     // Wait for initial nodes to process messages
+//     timeout(TEST_TIMEOUT, async {
+//         let mut received = vec![false; 2];
+//         loop {
+//             for (i, handle) in handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     if !received[i] {
+//                         assert_eq!(extract_number_from_message(&msg), numbers[1 - i]);
+//                         received[i] = true;
+//                     }
+//                 }
+//             }
+//             if received.iter().all(|&r| r) {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for initial summation");
+//
+//     // Start the late joining node
+//     info!("Starting late joining node");
+//     handles.push(nodes[2].start().await.expect("Failed to start late node"));
+//     let all_handles: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+//
+//     // Wait for the new node to complete handshakes
+//     wait_for_all_handshakes(&all_handles, TEST_TIMEOUT).await;
+//
+//     // Late node sends its number and receives history
+//     info!("Late node sending number and receiving history");
+//     handles[2]
+//         .send(create_protocol_message(
+//             PROTOCOL_NAME,
+//             SummationMessage::Number(numbers[2]),
+//             &handles[2],
+//             None,
+//         ))
+//         .expect("Failed to send number from late node");
+//
+//     // Verify final state
+//     timeout(TEST_TIMEOUT, async {
+//         let mut verified = vec![false; handles.len()];
+//         loop {
+//             for (i, handle) in handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     let num = extract_number_from_message(&msg);
+//                     if !verified[i] && (num == numbers[2] || numbers.contains(&num)) {
+//                         verified[i] = true;
+//                     }
+//                 }
+//             }
+//             if verified.iter().all(|&v| v) {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for late node synchronization");
+//
+//     info!("Late join test completed successfully");
+// }
+//
+// #[tokio::test]
+// async fn test_summation_protocol_node_disconnect() {
+//     super::init_tracing();
+//     info!("Starting node disconnect test");
+//
+//     // Create 3 nodes
+//     let mut nodes = create_whitelisted_nodes(3).await;
+//
+//     // Start all nodes
+//     let mut handles = Vec::new();
+//     for node in nodes.iter_mut() {
+//         handles.push(node.start().await.expect("Failed to start node"));
+//     }
+//
+//     // Convert handles to mutable references
+//     let handles_refs: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+//
+//     // Wait for all handshakes
+//     wait_for_all_handshakes(&handles_refs, TEST_TIMEOUT).await;
+//
+//     // Send initial numbers
+//     let numbers = vec![42, 58, 100];
+//     for (i, handle) in handles.iter().enumerate() {
+//         handle
+//             .send(create_protocol_message(
+//                 PROTOCOL_NAME,
+//                 SummationMessage::Number(numbers[i]),
+//                 handle,
+//                 None,
+//             ))
+//             .expect("Failed to send number");
+//     }
+//
+//     // Wait for initial processing
+//     timeout(TEST_TIMEOUT, async {
+//         let mut received = vec![0; handles.len()];
+//         loop {
+//             for (i, handle) in handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     if received[i] < 2 {
+//                         extract_number_from_message(&msg);
+//                         received[i] += 1;
+//                     }
+//                 }
+//             }
+//             if received.iter().all(|&r| r == 2) {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for initial messages");
+//
+//     // Disconnect one node
+//     info!("Disconnecting node");
+//     drop(handles.pop());
+//     let mut remaining_handles: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+//
+//     // Verify remaining nodes can still communicate
+//     info!("Verifying remaining nodes can communicate");
+//     for (i, sender) in remaining_handles.iter().enumerate() {
+//         for (j, recipient) in remaining_handles.iter().enumerate() {
+//             if i != j {
+//                 sender
+//                     .send(create_protocol_message(
+//                         PROTOCOL_NAME,
+//                         SummationMessage::Verification { sum: numbers[i] },
+//                         sender,
+//                         Some(recipient.local_peer_id),
+//                     ))
+//                     .expect("Failed to send verification");
+//             }
+//         }
+//     }
+//
+//     // Wait for verification messages
+//     timeout(TEST_TIMEOUT, async {
+//         let mut verified = vec![false; remaining_handles.len()];
+//         loop {
+//             for (i, handle) in remaining_handles.iter_mut().enumerate() {
+//                 if let Some(msg) = handle.next_protocol_message() {
+//                     if !verified[i] {
+//                         extract_sum_from_verification(&msg);
+//                         verified[i] = true;
+//                     }
+//                 }
+//             }
+//             if verified.iter().all(|&v| v) {
+//                 break;
+//             }
+//             tokio::time::sleep(Duration::from_millis(100)).await;
+//         }
+//     })
+//     .await
+//     .expect("Timeout waiting for verification after disconnect");
+//
+//     info!("Node disconnect test completed successfully");
+// }
diff --git a/crates/networking/src/tests/discovery.rs b/crates/networking/src/tests/discovery.rs
new file mode 100644
index 000000000..001992d43
--- /dev/null
+++ b/crates/networking/src/tests/discovery.rs
@@ -0,0 +1,134 @@
+use super::TestNode;
+use super::{create_whitelisted_nodes, init_tracing};
+use super::{wait_for_peer_discovery, wait_for_peer_info};
+use std::{collections::HashSet, time::Duration};
+use tokio::time::timeout;
+use tracing::info;
+
+#[tokio::test]
+async fn test_peer_discovery_mdns() {
+    init_tracing();
+
+    let network_name = "test-network";
+    let instance_id = "test-instance";
+    let allowed_keys = HashSet::new();
+
+    // Create two nodes
+    let mut node1 = TestNode::new(network_name, instance_id, allowed_keys.clone(), vec![]);
+    let mut node2 = TestNode::new(network_name, instance_id, allowed_keys, vec![]);
+
+    // Start both nodes and wait for them to be listening
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let handle2 = node2.start().await.expect("Failed to start node2");
+
+    // First wait for basic peer discovery (they see each other)
+    let discovery_timeout = Duration::from_secs(20);
+    wait_for_peer_discovery(&[&handle1, &handle2], discovery_timeout)
+        .await
+        .expect("Basic peer discovery timed out");
+}
+
+#[tokio::test]
+async fn test_peer_discovery_kademlia() {
+    init_tracing();
+
+    let network_name = "test-network";
+    let instance_id = "test-instance";
+    let allowed_keys = HashSet::new();
+
+    // Create the first node (bootstrap node)
+    let mut node1 = TestNode::new(network_name, instance_id, allowed_keys.clone(), vec![]);
+
+    // Start node1 and get its listening address
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let node1_addr = node1.get_listen_addr().expect("Node1 should be listening");
+
+    // Create two more nodes that will bootstrap from node1
+    let bootstrap_peers = vec![node1_addr.clone()];
+    let mut node2 = TestNode::new(
+        network_name,
+        instance_id,
+        allowed_keys.clone(),
+        bootstrap_peers.clone(),
+    );
+    let mut node3 = TestNode::new(network_name, instance_id, allowed_keys, bootstrap_peers);
+
+    // Start the remaining nodes
+    let handle2 = node2.start().await.expect("Failed to start node2");
+    let handle3 = node3.start().await.expect("Failed to start node3");
+
+    // Wait for peer discovery through Kademlia DHT
+    let discovery_timeout = Duration::from_secs(20);
+    match timeout(discovery_timeout, async {
+        loop {
+            let peers1 = handle1.peers();
+            let peers2 = handle2.peers();
+            let peers3 = handle3.peers();
+
+            if peers1.contains(&node2.peer_id)
+                && peers1.contains(&node3.peer_id)
+                && peers2.contains(&node1.peer_id)
+                && peers2.contains(&node3.peer_id)
+                && peers3.contains(&node1.peer_id)
+                && peers3.contains(&node2.peer_id)
+            {
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    {
+        Ok(()) => println!("All peers discovered each other through Kademlia"),
+        Err(e) => panic!("Kademlia peer discovery timed out: {e}"),
+    }
+}
+
+#[tokio::test]
+async fn test_peer_info_updates() {
+    init_tracing();
+
+    info!("Creating test nodes...");
+    // Create nodes with whitelisted keys
+    let mut nodes = create_whitelisted_nodes(2).await;
+    let mut node2 = nodes.pop().unwrap();
+    let mut node1 = nodes.pop().unwrap();
+
+    info!("Starting node1...");
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    info!("Node1 started successfully");
+
+    info!("Starting node2...");
+    let handle2 = node2.start().await.expect("Failed to start node2");
+    info!("Node2 started successfully");
+
+    info!("Both nodes started, waiting for peer discovery...");
+
+    // First wait for basic peer discovery (they see each other)
+    let discovery_timeout = Duration::from_secs(30); // Increased timeout
+    match wait_for_peer_discovery(&[&handle1, &handle2], discovery_timeout).await {
+        Ok(()) => info!("Peer discovery successful"),
+        Err(e) => {
+            // Log peer states before failing
+            info!("Node1 peers: {:?}", handle1.peers());
+            info!("Node2 peers: {:?}", handle2.peers());
+            panic!("Peer discovery failed: {}", e);
+        }
+    }
+
+    info!("Peers discovered each other, waiting for identify info...");
+
+    // Now wait for identify info to be populated
+    let identify_timeout = Duration::from_secs(30); // Increased timeout
+    wait_for_peer_info(&handle1, &handle2, identify_timeout).await;
+
+    info!("Test completed successfully - both nodes have identify info");
+
+    // Log final state
+    if let Some(info) = handle1.peer_info(&handle2.local_peer_id) {
+        info!("Node1's info about Node2: {:?}", info);
+    }
+    if let Some(info) = handle2.peer_info(&handle1.local_peer_id) {
+        info!("Node2's info about Node1: {:?}", info);
+    }
+}
diff --git a/crates/networking/src/tests/gossip.rs b/crates/networking/src/tests/gossip.rs
new file mode 100644
index 000000000..99006d04f
--- /dev/null
+++ b/crates/networking/src/tests/gossip.rs
@@ -0,0 +1,182 @@
+use super::{init_tracing, wait_for_handshake_completion, TestNode};
+use crate::{
+    service_handle::NetworkServiceHandle,
+    tests::{create_whitelisted_nodes, wait_for_all_handshakes},
+    types::{MessageRouting, ParticipantId, ParticipantInfo},
+};
+use std::{collections::HashSet, time::Duration};
+use tokio::time::timeout;
+use tracing::info;
+
+const TEST_TIMEOUT: Duration = Duration::from_secs(10);
+const PROTOCOL_NAME: &str = "/test-net/sum-test";
+
+#[tokio::test]
+async fn test_gossip_between_verified_peers() {
+    init_tracing();
+    info!("Starting gossip test between verified peers");
+
+    // Create nodes with whitelisted keys
+    let mut nodes = create_whitelisted_nodes(2).await;
+    let mut node2 = nodes.pop().unwrap();
+    let mut node1 = nodes.pop().unwrap();
+
+    info!("Starting nodes");
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let mut handle2 = node2.start().await.expect("Failed to start node2");
+
+    info!("Waiting for handshake completion");
+    wait_for_handshake_completion(&handle1, &handle2, TEST_TIMEOUT).await;
+
+    // Create test message
+    info!("Sending gossip message from node1");
+
+    let test_payload = b"Hello, gossip network!".to_vec();
+    let routing = MessageRouting {
+        message_id: 1,
+        round_id: 0,
+        sender: ParticipantInfo {
+            id: ParticipantId(1),
+            public_key: Some(node1.instance_key_pair.public()),
+        },
+        recipient: None, // No specific recipient for gossip
+    };
+
+    handle1
+        .send(routing, test_payload.clone())
+        .expect("Failed to send gossip message");
+
+    info!("Waiting for node2 to receive the message");
+    // Wait for node2 to receive the message
+    let received_message = timeout(TEST_TIMEOUT, async {
+        loop {
+            if let Some(msg) = handle2.next_protocol_message() {
+                if msg.protocol == PROTOCOL_NAME {
+                    return msg;
+                }
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    .expect("Timeout waiting for gossip message");
+
+    // Verify message contents
+    assert_eq!(received_message.payload, test_payload);
+    assert_eq!(received_message.protocol, PROTOCOL_NAME);
+    assert_eq!(received_message.routing.message_id, 1);
+    assert_eq!(received_message.routing.round_id, 0);
+    assert_eq!(
+        received_message.routing.sender.public_key,
+        Some(node1.instance_key_pair.public())
+    );
+    assert!(received_message.routing.recipient.is_none());
+
+    info!("Gossip test completed successfully");
+}
+
+#[tokio::test]
+async fn test_multi_node_gossip() {
+    init_tracing();
+    info!("Starting multi-node gossip test");
+
+    // Create three nodes with all keys whitelisted
+    let mut nodes = create_whitelisted_nodes(3).await;
+
+    info!("Starting all nodes");
+    let mut handles: Vec<_> = Vec::new();
+    for node in &mut nodes {
+        handles.push(node.start().await.expect("Failed to start node"));
+    }
+
+    info!("Waiting for all handshakes to complete");
+    let handles_refs: Vec<&mut NetworkServiceHandle> = handles.iter_mut().collect();
+    wait_for_all_handshakes(&handles_refs, TEST_TIMEOUT).await;
+
+    // Create test message
+    let test_payload = b"Multi-node gossip test".to_vec();
+    let routing = MessageRouting {
+        message_id: 1,
+        round_id: 0,
+        sender: ParticipantInfo {
+            id: ParticipantId(0),
+            public_key: Some(nodes[0].instance_key_pair.public()),
+        },
+        recipient: None,
+    };
+
+    info!("Sending gossip message from node 0");
+    handles[0]
+        .send(routing, test_payload.clone())
+        .expect("Failed to send gossip message");
+
+    info!("Waiting for all nodes to receive the message");
+    // Wait for all other nodes to receive the message
+    timeout(TEST_TIMEOUT, async {
+        for (i, handle) in handles.iter_mut().enumerate().skip(1) {
+            let received = loop {
+                if let Some(msg) = handle.next_protocol_message() {
+                    if msg.protocol == PROTOCOL_NAME {
+                        break msg;
+                    }
+                }
+                tokio::time::sleep(Duration::from_millis(100)).await;
+            };
+
+            assert_eq!(
+                received.payload, test_payload,
+                "Node {} received wrong payload",
+                i
+            );
+            assert_eq!(received.protocol, PROTOCOL_NAME);
+            assert_eq!(
+                received.routing.sender.public_key,
+                Some(nodes[0].instance_key_pair.public())
+            );
+            info!("Node {} received the gossip message correctly", i);
+        }
+    })
+    .await
+    .expect("Timeout waiting for gossip messages");
+
+    info!("Multi-node gossip test completed successfully");
+}
+
+#[tokio::test]
+async fn test_unverified_peer_gossip() {
+    init_tracing();
+    info!("Starting unverified peer gossip test");
+
+    // Create two nodes with no whitelisted keys
+    let mut node1 = TestNode::new("test-net", "gossip-test", HashSet::new(), vec![]);
+    let mut node2 = TestNode::new("test-net", "gossip-test", HashSet::new(), vec![]);
+
+    info!("Starting nodes");
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let mut handle2 = node2.start().await.expect("Failed to start node2");
+
+    // Create test message
+    let test_payload = b"This message should not be received".to_vec();
+    let routing = MessageRouting {
+        message_id: 1,
+        round_id: 0,
+        sender: ParticipantInfo {
+            id: ParticipantId(1),
+            public_key: Some(node1.instance_key_pair.public()),
+        },
+        recipient: None,
+    };
+
+    info!("Attempting to send gossip message from unverified node");
+    handle1
+        .send(routing, test_payload.clone())
+        .expect("Failed to send gossip message");
+
+    // Wait a bit to ensure message is not received
+    tokio::time::sleep(Duration::from_secs(2)).await;
+
+    // Verify node2 did not receive the message
+    assert!(handle2.next_protocol_message().is_none());
+
+    info!("Unverified peer gossip test completed successfully");
+}
diff --git a/crates/networking/src/tests/handshake.rs b/crates/networking/src/tests/handshake.rs
new file mode 100644
index 000000000..796997d9e
--- /dev/null
+++ b/crates/networking/src/tests/handshake.rs
@@ -0,0 +1,106 @@
+use crate::tests::create_whitelisted_nodes;
+use crate::tests::TestNode;
+use std::{collections::HashSet, time::Duration};
+use tokio::time::timeout;
+use tracing::info;
+use tracing_subscriber::{fmt, EnvFilter};
+
+const TEST_TIMEOUT: Duration = Duration::from_secs(5);
+
+fn init_tracing() {
+    let _ = fmt()
+        .with_env_filter(EnvFilter::from_default_env())
+        .with_target(true)
+        .with_thread_ids(true)
+        .with_file(true)
+        .with_line_number(true)
+        .try_init();
+}
+
+#[tokio::test]
+async fn test_automatic_handshake() {
+    init_tracing();
+    info!("Starting automatic handshake test");
+
+    // Create nodes with whitelisted keys
+    let mut nodes = create_whitelisted_nodes(2).await;
+    let mut node2 = nodes.pop().unwrap();
+    let mut node1 = nodes.pop().unwrap();
+
+    info!("Starting nodes");
+    // Start both nodes - this should trigger automatic handshake
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let handle2 = node2.start().await.expect("Failed to start node2");
+
+    // Wait for automatic handshake completion
+    info!("Waiting for automatic handshake completion");
+    timeout(TEST_TIMEOUT, async {
+        loop {
+            if handle1.peer_manager.is_peer_verified(&node2.peer_id)
+                && handle2.peer_manager.is_peer_verified(&node1.peer_id)
+            {
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    .expect("Automatic handshake verification timed out");
+
+    // Verify peer info and identify info are present
+    let peer_info1 = handle1
+        .peer_info(&node2.peer_id)
+        .expect("Missing peer info for node2");
+    let peer_info2 = handle2
+        .peer_info(&node1.peer_id)
+        .expect("Missing peer info for node1");
+
+    assert!(
+        peer_info1.identify_info.is_some(),
+        "Missing identify info for node2"
+    );
+    assert!(
+        peer_info2.identify_info.is_some(),
+        "Missing identify info for node1"
+    );
+}
+
+#[tokio::test]
+async fn test_handshake_with_invalid_peer() {
+    init_tracing();
+    info!("Starting invalid peer handshake test");
+
+    let network_name = "test-network";
+    let instance_id = "test-instance";
+
+    // Create node1 with empty whitelist
+    let mut node1 = TestNode::new(network_name, instance_id, HashSet::new(), vec![]);
+
+    // Create node2 with node1's key whitelisted (but node2's key is not whitelisted by node1)
+    let mut allowed_keys2 = HashSet::new();
+    allowed_keys2.insert(node1.instance_key_pair.public());
+    let mut node2 = TestNode::new(network_name, instance_id, allowed_keys2, vec![]);
+
+    info!("Starting nodes");
+    let handle1 = node1.start().await.expect("Failed to start node1");
+    let handle2 = node2.start().await.expect("Failed to start node2");
+
+    // Wait for ban to be applied automatically
+    info!("Waiting for automatic ban");
+    timeout(TEST_TIMEOUT, async {
+        loop {
+            if handle1.peer_manager.is_banned(&node2.peer_id) {
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    .expect("Ban was not applied");
+
+    // Verify peers remain unverified
+    assert!(!handle1.peer_manager.is_peer_verified(&node2.peer_id));
+    assert!(!handle2.peer_manager.is_peer_verified(&node1.peer_id));
+
+    info!("Invalid peer handshake test completed successfully");
+}
diff --git a/crates/networking/src/tests/mod.rs b/crates/networking/src/tests/mod.rs
new file mode 100644
index 000000000..b939cf282
--- /dev/null
+++ b/crates/networking/src/tests/mod.rs
@@ -0,0 +1,347 @@
+#![allow(dead_code)]
+
+use crate::{
+    key_types::{Curve, InstanceMsgKeyPair, InstanceMsgPublicKey},
+    service_handle::NetworkServiceHandle,
+    NetworkConfig, NetworkService,
+};
+use gadget_crypto::KeyType;
+use libp2p::{
+    identity::{self, Keypair},
+    Multiaddr, PeerId,
+};
+use std::{collections::HashSet, time::Duration};
+use tokio::time::timeout;
+use tracing::info;
+
+mod blueprint_protocol;
+mod discovery;
+mod gossip;
+mod handshake;
+
+fn init_tracing() {
+    let _ = tracing_subscriber::fmt()
+        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
+        .with_target(true)
+        .with_thread_ids(true)
+        .with_file(true)
+        .with_line_number(true)
+        .try_init();
+}
+
+/// Test node configuration for network tests
+pub struct TestNode {
+    pub service: Option<NetworkService>,
+    pub peer_id: PeerId,
+    pub listen_addr: Option<Multiaddr>,
+    pub instance_key_pair: InstanceMsgKeyPair,
+    pub local_key: Keypair,
+}
+
+impl TestNode {
+    /// Create a new test node with auto-generated keys
+    pub fn new(
+        network_name: &str,
+        instance_id: &str,
+        allowed_keys: HashSet<InstanceMsgPublicKey>,
+        bootstrap_peers: Vec<Multiaddr>,
+    ) -> Self {
+        Self::new_with_keys(
+            network_name,
+            instance_id,
+            allowed_keys,
+            bootstrap_peers,
+            None,
+            None,
+        )
+    }
+
+    /// Create a new test node with specified keys
+    pub fn new_with_keys(
+        network_name: &str,
+        instance_id: &str,
+        allowed_keys: HashSet<InstanceMsgPublicKey>,
+        bootstrap_peers: Vec<Multiaddr>,
+        instance_key_pair: Option<InstanceMsgKeyPair>,
+        local_key: Option<Keypair>,
+    ) -> Self {
+        let local_key = local_key.unwrap_or_else(identity::Keypair::generate_ed25519);
+        let peer_id = local_key.public().to_peer_id();
+
+        // Bind to all interfaces instead of just localhost
+        let listen_addr: Multiaddr = "/ip4/0.0.0.0/tcp/0".parse().unwrap();
+        info!("Creating test node {peer_id} with TCP address: {listen_addr}");
+
+        let instance_key_pair =
+            instance_key_pair.unwrap_or_else(|| Curve::generate_with_seed(None).unwrap());
+
+        let config = NetworkConfig {
+            network_name: network_name.to_string(),
+            instance_id: instance_id.to_string(),
+            instance_key_pair: instance_key_pair.clone(),
+            local_key: local_key.clone(),
+            listen_addr: listen_addr.clone(),
+            target_peer_count: 10,
+            bootstrap_peers,
+            enable_mdns: true,
+            enable_kademlia: true,
+        };
+
+        let service =
+            NetworkService::new(config, allowed_keys).expect("Failed to create network service");
+
+        Self {
+            service: Some(service),
+            peer_id,
+            listen_addr: None,
+            instance_key_pair,
+            local_key,
+        }
+    }
+
+    /// Start the node and wait for it to be fully initialized
+    pub async fn start(&mut self) -> Result<NetworkServiceHandle, &'static str> {
+        // Take ownership of the service
+        let service = self.service.take().ok_or("Service already started")?;
+        let handle = service.start();
+
+        // Wait for the node to be fully initialized
+        let timeout_duration = Duration::from_secs(10); // Increased timeout
+        match timeout(timeout_duration, async {
+            // First wait for the listening address
+            while self.listen_addr.is_none() {
+                if let Some(addr) = handle.get_listen_addr() {
+                    info!("Node {} listening on {}", self.peer_id, addr);
+                    self.listen_addr = Some(addr.clone());
+
+                    // Extract port from multiaddr
+                    let addr_str = addr.to_string();
+                    let port = addr_str.split('/').nth(4).unwrap_or("0").to_string();
+
+                    // Try localhost first
+                    let localhost_addr = format!("127.0.0.1:{}", port);
+                    match tokio::net::TcpStream::connect(&localhost_addr).await {
+                        Ok(_) => {
+                            info!("Successfully verified localhost port for {}", self.peer_id);
+                            break;
+                        }
+                        Err(e) => {
+                            info!("Localhost port not ready for {}: {}", self.peer_id, e);
+                            // Try external IP
+                            let external_addr = format!("10.0.1.142:{}", port);
+                            match tokio::net::TcpStream::connect(&external_addr).await {
+                                Ok(_) => {
+                                    info!(
+                                        "Successfully verified external port for {}",
+                                        self.peer_id
+                                    );
+                                    break;
+                                }
+                                Err(e) => {
+                                    info!("External port not ready for {}: {}", self.peer_id, e);
+                                    tokio::time::sleep(Duration::from_millis(100)).await;
+                                    continue;
+                                }
+                            }
+                        }
+                    }
+                }
+                tokio::time::sleep(Duration::from_millis(100)).await;
+            }
+
+            // Give the node a moment to initialize protocols
+            tokio::time::sleep(Duration::from_millis(500)).await;
+
+            Ok::<(), &'static str>(())
+        })
+        .await
+        {
+            Ok(Ok(())) => {
+                info!("Node {} fully initialized", self.peer_id);
+                Ok(handle)
+            }
+            Ok(Err(e)) => Err(e),
+            Err(_) => Err("Timeout waiting for node to initialize"),
+        }
+    }
+
+    /// Get the actual listening address
+    pub fn get_listen_addr(&self) -> Option<Multiaddr> {
+        self.listen_addr.clone()
+    }
+
+    /// Update the allowed keys for this node
+    pub fn update_allowed_keys(&self, allowed_keys: HashSet<InstanceMsgPublicKey>) {
+        if let Some(service) = &self.service {
+            service.peer_manager.update_whitelisted_keys(allowed_keys);
+        }
+    }
+}
+
+/// Wait for a condition with timeout
+pub async fn wait_for_condition<F>(timeout: Duration, mut condition: F) -> Result<(), &'static str>
+where
+    F: FnMut() -> bool,
+{
+    let start = std::time::Instant::now();
+    while !condition() {
+        if start.elapsed() > timeout {
+            return Err("Timeout waiting for condition");
+        }
+        tokio::time::sleep(Duration::from_millis(100)).await;
+    }
+    Ok(())
+}
+
+/// Wait for peers to discover each other
+pub async fn wait_for_peer_discovery(
+    handles: &[&NetworkServiceHandle],
+    timeout: Duration,
+) -> Result<(), &'static str> {
+    info!("Waiting for peer discovery...");
+
+    wait_for_condition(timeout, || {
+        for (i, handle1) in handles.iter().enumerate() {
+            for (j, handle2) in handles.iter().enumerate() {
+                if i != j
+                    && !handle1
+                        .peers()
+                        .iter()
+                        .any(|id| *id == handle2.local_peer_id)
+                {
+                    return false;
+                }
+            }
+        }
+        true
+    })
+    .await
+}
+
+/// Wait for peer info to be updated
+pub async fn wait_for_peer_info(
+    handle1: &NetworkServiceHandle,
+    handle2: &NetworkServiceHandle,
+    timeout: Duration,
+) {
+    info!("Waiting for identify info...");
+
+    match tokio::time::timeout(timeout, async {
+        loop {
+            let peer_info1 = handle1.peer_info(&handle2.local_peer_id);
+            let peer_info2 = handle2.peer_info(&handle1.local_peer_id);
+
+            if let Some(peer_info) = peer_info1 {
+                if peer_info.identify_info.is_some() {
+                    // Also verify reverse direction
+                    if let Some(peer_info) = peer_info2 {
+                        if peer_info.identify_info.is_some() {
+                            info!("Identify info populated in both directions");
+                            break;
+                        }
+                    }
+                }
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    {
+        Ok(()) => info!("Peer info updated successfully in both directions"),
+        Err(e) => panic!("Peer info update timed out: {e}"),
+    }
+}
+
+// Helper to wait for handshake completion between multiple nodes
+async fn wait_for_all_handshakes(handles: &[&mut NetworkServiceHandle], timeout_length: Duration) {
+    info!("Starting handshake wait for {} nodes", handles.len());
+    timeout(timeout_length, async {
+        loop {
+            let mut all_verified = true;
+            for (i, handle1) in handles.iter().enumerate() {
+                for (j, handle2) in handles.iter().enumerate() {
+                    if i != j {
+                        let verified = handle1
+                            .peer_manager
+                            .is_peer_verified(&handle2.local_peer_id);
+                        if !verified {
+                            info!("Node {} -> Node {}: handshake not verified yet", i, j);
+                            all_verified = false;
+                            break;
+                        }
+                    }
+                }
+                if !all_verified {
+                    break;
+                }
+            }
+            if all_verified {
+                info!("All handshakes completed successfully");
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    .expect("Handshake verification timed out");
+}
+
+// Helper to wait for handshake completion between two nodes
+async fn wait_for_handshake_completion(
+    handle1: &NetworkServiceHandle,
+    handle2: &NetworkServiceHandle,
+    timeout_length: Duration,
+) {
+    timeout(timeout_length, async {
+        loop {
+            if handle1
+                .peer_manager
+                .is_peer_verified(&handle2.local_peer_id)
+                && handle2
+                    .peer_manager
+                    .is_peer_verified(&handle1.local_peer_id)
+            {
+                break;
+            }
+            tokio::time::sleep(Duration::from_millis(100)).await;
+        }
+    })
+    .await
+    .expect("Handshake verification timed out");
+}
+
+// Helper to create a whitelisted test node
+fn create_node_with_keys(
+    network: &str,
+    instance: &str,
+    allowed_keys: HashSet<InstanceMsgPublicKey>,
+    key_pair: Option<InstanceMsgKeyPair>,
+) -> TestNode {
+    TestNode::new_with_keys(network, instance, allowed_keys, vec![], key_pair, None)
+}
+
+// Helper to create a set of nodes with whitelisted keys
+async fn create_whitelisted_nodes(count: usize) -> Vec<TestNode> {
+    let mut nodes = Vec::with_capacity(count);
+    let mut key_pairs = Vec::with_capacity(count);
+    let mut allowed_keys = HashSet::new();
+
+    // Generate all key pairs first
+    for _ in 0..count {
+        let key_pair = Curve::generate_with_seed(None).unwrap();
+        key_pairs.push(key_pair.clone());
+        allowed_keys.insert(key_pair.public());
+    }
+
+    // Create nodes with whitelisted keys
+    for key_pair in key_pairs {
+        nodes.push(create_node_with_keys(
+            "test-net",
+            "sum-test",
+            allowed_keys.clone(),
+            Some(key_pair),
+        ));
+    }
+
+    nodes
+}
diff --git a/crates/testing-utils/core/src/harness.rs b/crates/testing-utils/core/src/harness.rs
index 5981fa80f..0cb037676 100644
--- a/crates/testing-utils/core/src/harness.rs
+++ b/crates/testing-utils/core/src/harness.rs
@@ -16,9 +16,6 @@ pub trait TestHarness {
 
     /// Gets the gadget configuration
     fn env(&self) -> &GadgetConfiguration;
-
-    /// Gets the configuration
-    fn config(&self) -> &Self::Config;
 }
 
 /// Base implementation of a test harness that can be used by specific implementations
diff --git a/crates/testing-utils/core/src/runner.rs b/crates/testing-utils/core/src/runner.rs
index 456c03ca7..c07aa4939 100644
--- a/crates/testing-utils/core/src/runner.rs
+++ b/crates/testing-utils/core/src/runner.rs
@@ -20,7 +20,7 @@ impl TestRunner {
 
     pub fn add_job<J>(&mut self, job: J) -> &mut Self
     where
-        J: InitializableEventHandler + Send + 'static,
+        J: InitializableEventHandler + Send + Sync + 'static,
     {
         self.inner.job(job);
         self
@@ -45,10 +45,10 @@ pub trait TestEnv: Sized {
     fn new(config: Self::Config, env: GadgetConfiguration) -> Result<Self, Error>;
     fn add_job<J>(&mut self, job: J)
     where
-        J: InitializableEventHandler + Send + 'static;
+        J: InitializableEventHandler + Send + Sync + 'static;
     fn add_background_service<B>(&mut self, service: B)
     where
         B: BackgroundService + Send + 'static;
-    fn get_gadget_config(self) -> GadgetConfiguration;
-    fn run_runner(&mut self) -> impl std::future::Future<Output = Result<(), Error>> + Send;
+    fn get_gadget_config(&self) -> GadgetConfiguration;
+    fn run_runner(&self) -> impl std::future::Future<Output = Result<(), Error>> + Send;
 }
diff --git a/crates/testing-utils/eigenlayer/src/harness.rs b/crates/testing-utils/eigenlayer/src/harness.rs
index 8c652afdf..2fe38d598 100644
--- a/crates/testing-utils/eigenlayer/src/harness.rs
+++ b/crates/testing-utils/eigenlayer/src/harness.rs
@@ -95,10 +95,6 @@ impl TestHarness for EigenlayerTestHarness {
     fn env(&self) -> &GadgetConfiguration {
         &self.base.env
     }
-
-    fn config(&self) -> &Self::Config {
-        &self.base.config
-    }
 }
 
 impl EigenlayerTestHarness {
diff --git a/crates/testing-utils/eigenlayer/src/runner.rs b/crates/testing-utils/eigenlayer/src/runner.rs
index 74b4e6ada..928e686c7 100644
--- a/crates/testing-utils/eigenlayer/src/runner.rs
+++ b/crates/testing-utils/eigenlayer/src/runner.rs
@@ -1,17 +1,20 @@
 #![allow(dead_code)]
+
 use gadget_config::GadgetConfiguration;
 use gadget_core_testing_utils::runner::{TestEnv, TestRunner};
 use gadget_event_listeners::core::InitializableEventHandler;
+use gadget_macros::ext::futures;
 use gadget_runners::core::error::RunnerError as Error;
 use gadget_runners::core::runner::BackgroundService;
 use gadget_runners::eigenlayer::bls::EigenlayerBLSConfig;
+use tokio::sync::Mutex;
 use tokio::task::JoinHandle;
 
 pub struct EigenlayerBLSTestEnv {
     runner: TestRunner,
     config: EigenlayerBLSConfig,
     gadget_config: GadgetConfiguration,
-    runner_handle: Option<JoinHandle<Result<(), Error>>>,
+    runner_handle: Mutex<Option<JoinHandle<Result<(), Error>>>>,
 }
 
 impl TestEnv for EigenlayerBLSTestEnv {
@@ -24,13 +27,13 @@ impl TestEnv for EigenlayerBLSTestEnv {
             runner,
             config,
             gadget_config: env,
-            runner_handle: None,
+            runner_handle: Mutex::new(None),
         })
     }
 
     fn add_job<J>(&mut self, job: J)
     where
-        J: InitializableEventHandler + Send + 'static,
+        J: InitializableEventHandler + Send + Sync + 'static,
     {
         self.runner.add_job(job);
     }
@@ -42,54 +45,58 @@ impl TestEnv for EigenlayerBLSTestEnv {
         self.runner.add_background_service(service);
     }
 
-    fn get_gadget_config(self) -> GadgetConfiguration {
+    fn get_gadget_config(&self) -> GadgetConfiguration {
         self.gadget_config.clone()
     }
 
-    async fn run_runner(&mut self) -> Result<(), Error> {
+    async fn run_runner(&self) -> Result<(), Error> {
         // Spawn the runner in a background task
         let mut runner = self.runner.clone();
         let handle = tokio::spawn(async move { runner.run().await });
 
-        self.runner_handle = Some(handle);
+        let mut _guard = self.runner_handle.lock().await;
+        *_guard = Some(handle);
 
         // Brief delay to allow for startup
         tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
 
         // Just check if it failed immediately
-        if let Some(handle) = self.runner_handle.take() {
-            if handle.is_finished() {
-                gadget_logging::info!("Runner task finished OK");
-                match handle.await {
-                    Ok(Ok(())) => Ok(()),
-                    Ok(Err(e)) => {
-                        gadget_logging::error!("Runner failed during startup: {}", e);
-                        Err(Error::Eigenlayer(format!(
-                            "Runner failed during startup: {}",
-                            e
-                        )))
-                    }
-                    Err(e) => {
-                        gadget_logging::error!("Runner task panicked: {}", e);
-                        Err(Error::Eigenlayer(format!("Runner task panicked: {}", e)))
-                    }
-                }
-            } else {
-                // Put the handle back since the runner is still running
-                self.runner_handle = Some(handle);
-                gadget_logging::info!("Runner started successfully");
-                Ok(())
+        let Some(handle) = _guard.take() else {
+            return Err(Error::Eigenlayer("Failed to spawn runner task".to_string()));
+        };
+
+        if !handle.is_finished() {
+            // Put the handle back since the runner is still running
+            *_guard = Some(handle);
+            gadget_logging::info!("Runner started successfully");
+            return Ok(());
+        }
+
+        gadget_logging::info!("Runner task finished OK");
+        match handle.await {
+            Ok(Ok(())) => Ok(()),
+            Ok(Err(e)) => {
+                gadget_logging::error!("Runner failed during startup: {}", e);
+                Err(Error::Eigenlayer(format!(
+                    "Runner failed during startup: {}",
+                    e
+                )))
+            }
+            Err(e) => {
+                gadget_logging::error!("Runner task panicked: {}", e);
+                Err(Error::Eigenlayer(format!("Runner task panicked: {}", e)))
             }
-        } else {
-            Err(Error::Eigenlayer("Failed to spawn runner task".to_string()))
         }
     }
 }
 
 impl Drop for EigenlayerBLSTestEnv {
     fn drop(&mut self) {
-        if let Some(handle) = &self.runner_handle {
-            handle.abort();
-        }
+        futures::executor::block_on(async {
+            let mut _guard = self.runner_handle.lock().await;
+            if let Some(handle) = _guard.take() {
+                handle.abort();
+            }
+        })
     }
 }
diff --git a/crates/testing-utils/tangle/Cargo.toml b/crates/testing-utils/tangle/Cargo.toml
index fb08d13b9..0d518fea9 100644
--- a/crates/testing-utils/tangle/Cargo.toml
+++ b/crates/testing-utils/tangle/Cargo.toml
@@ -11,8 +11,9 @@ repository.workspace = true
 [dependencies]
 cargo-tangle = { workspace = true, features = ["tangle"] }
 gadget-std = { workspace = true, features = ["std"] }
+gadget-networking = { workspace = true, features = ["std"] }
 gadget-runners = { workspace = true, features = ["tangle", "std"] }
-gadget-config = { workspace = true, features = ["tangle", "std"] }
+gadget-config = { workspace = true, features = ["tangle", "std", "networking"] }
 gadget-contexts = { workspace = true, features = ["tangle", "std"] }
 gadget-macros = { workspace = true, features = ["tangle", "std"] }
 gadget-logging = { workspace = true, features = ["std"] }
@@ -35,6 +36,7 @@ sp-core = { workspace = true }
 tangle-subxt = { workspace = true }
 
 async-trait = { workspace = true }
+futures = { workspace = true }
 tracing = { workspace = true }
 tempfile = { workspace = true }
 reqwest = { workspace = true, features = ["json", "default-tls"] }
diff --git a/crates/testing-utils/tangle/src/harness.rs b/crates/testing-utils/tangle/src/harness.rs
index 98141c2bb..7b7963033 100644
--- a/crates/testing-utils/tangle/src/harness.rs
+++ b/crates/testing-utils/tangle/src/harness.rs
@@ -1,28 +1,23 @@
-use crate::node::transactions::join_operators;
+use crate::multi_node::MultiNodeTestEnv;
+use crate::node::transactions::setup_operator_and_service_multiple;
 use crate::Error;
 use crate::{
     keys::inject_tangle_key,
-    node::{
-        run,
-        transactions::{self, setup_operator_and_service, submit_and_verify_job},
-        NodeConfig,
-    },
-    runner::TangleTestEnv,
+    node::{run, transactions, NodeConfig},
     InputValue, OutputValue,
 };
 use gadget_client_tangle::client::TangleClient;
 use gadget_config::{supported_chains::SupportedChains, ContextConfig, GadgetConfiguration};
 use gadget_contexts::{keystore::KeystoreContext, tangle::TangleClientContext};
-use gadget_core_testing_utils::{
-    harness::{BaseTestHarness, TestHarness},
-    runner::TestEnv,
-};
+use gadget_core_testing_utils::harness::TestHarness;
 use gadget_crypto_tangle_pair_signer::TanglePairSigner;
 use gadget_keystore::backends::Backend;
 use gadget_keystore::crypto::sp_core::{SpEcdsa, SpSr25519};
 use gadget_logging::debug;
-use gadget_runners::tangle::tangle::{PriceTargets, TangleConfig};
-use sp_core::Pair;
+use gadget_runners::core::error::RunnerError;
+use gadget_runners::tangle::tangle::PriceTargets;
+use std::path::{Path, PathBuf};
+use tangle_subxt::tangle_testnet_runtime::api::services::events::JobCalled;
 use tangle_subxt::tangle_testnet_runtime::api::services::{
     calls::types::{call::Job, register::Preferences},
     events::JobResultSubmitted,
@@ -30,26 +25,73 @@ use tangle_subxt::tangle_testnet_runtime::api::services::{
 use tempfile::TempDir;
 use url::Url;
 
+pub const ENDOWED_TEST_NAMES: [&str; 10] = [
+    "Alice",
+    "Bob",
+    "Charlie",
+    "Dave",
+    "Eve",
+    "Ferdinand",
+    "Gina",
+    "Hank",
+    "Ivy",
+    "Jack",
+];
+
 /// Configuration for the Tangle test harness
-#[derive(Default)]
+#[derive(Clone, Debug)]
 pub struct TangleTestConfig {
-    pub http_endpoint: Option<Url>,
-    pub ws_endpoint: Option<Url>,
+    pub http_endpoint: Url,
+    pub ws_endpoint: Url,
+    pub temp_dir: PathBuf,
 }
 
 /// Test harness for Tangle network tests
 pub struct TangleTestHarness {
-    base: BaseTestHarness<TangleTestConfig>,
     pub http_endpoint: Url,
     pub ws_endpoint: Url,
     client: TangleClient,
     pub sr25519_signer: TanglePairSigner<sp_core::sr25519::Pair>,
     pub ecdsa_signer: TanglePairSigner<sp_core::ecdsa::Pair>,
     pub alloy_key: alloy_signer_local::PrivateKeySigner,
-    _temp_dir: tempfile::TempDir,
+    config: TangleTestConfig,
+    temp_dir: tempfile::TempDir,
     _node: crate::node::testnet::SubstrateNode,
 }
 
+pub async fn generate_env_from_node_id(
+    identity: &str,
+    http_endpoint: Url,
+    ws_endpoint: Url,
+    test_dir: &Path,
+) -> Result<GadgetConfiguration, RunnerError> {
+    let keystore_path = test_dir.join(identity.to_ascii_lowercase());
+    tokio::fs::create_dir_all(&keystore_path).await?;
+    inject_tangle_key(&keystore_path, &format!("//{identity}"))
+        .map_err(|err| RunnerError::Other(err.to_string()))?;
+
+    // Create context config
+    let context_config = ContextConfig::create_tangle_config(
+        http_endpoint,
+        ws_endpoint,
+        keystore_path.display().to_string(),
+        None,
+        SupportedChains::LocalTestnet,
+        0,
+        Some(0),
+    );
+
+    // Load environment
+    let mut env = gadget_config::load(context_config)
+        .map_err(|e| Error::Setup(e.to_string()))
+        .map_err(|err| RunnerError::Other(err.to_string()))?;
+
+    // Always set test mode, dont require callers to set env vars
+    env.test_mode = true;
+
+    Ok(env)
+}
+
 #[async_trait::async_trait]
 impl TestHarness for TangleTestHarness {
     type Config = TangleTestConfig;
@@ -63,38 +105,24 @@ impl TestHarness for TangleTestHarness {
         let http_endpoint = Url::parse(&format!("http://127.0.0.1:{}", node.ws_port()))?;
         let ws_endpoint = Url::parse(&format!("ws://127.0.0.1:{}", node.ws_port()))?;
 
-        // Setup testing directory
-        let test_dir_path = test_dir.path().to_string_lossy().into_owned();
-        inject_tangle_key(&test_dir_path, "//Alice")?;
-
-        // Create context config
-        let context_config = ContextConfig::create_tangle_config(
+        // Alice idx = 0
+        let alice_env = generate_env_from_node_id(
+            ENDOWED_TEST_NAMES[0],
             http_endpoint.clone(),
             ws_endpoint.clone(),
-            test_dir_path,
-            None,
-            SupportedChains::LocalTestnet,
-            0,
-            Some(0),
-        );
-
-        // Load environment
-        let mut env =
-            gadget_config::load(context_config).map_err(|e| Error::Setup(e.to_string()))?;
-
-        // Always set test mode, dont require callers to set env vars
-        env.test_mode = true;
+            test_dir.path(),
+        )
+        .await?;
 
         // Create config
         let config = TangleTestConfig {
-            http_endpoint: Some(http_endpoint.clone()),
-            ws_endpoint: Some(ws_endpoint.clone()),
+            http_endpoint: http_endpoint.clone(),
+            ws_endpoint: ws_endpoint.clone(),
+            temp_dir: test_dir.path().to_path_buf(),
         };
 
-        let base = BaseTestHarness::new(env.clone(), config);
-
         // Setup signers
-        let keystore = env.keystore();
+        let keystore = alice_env.keystore();
         let sr25519_public = keystore.first_local::<SpSr25519>()?;
         let sr25519_pair = keystore.get_secret::<SpSr25519>(&sr25519_public)?;
         let sr25519_signer = TanglePairSigner::new(sr25519_pair.0);
@@ -106,16 +134,16 @@ impl TestHarness for TangleTestHarness {
             .alloy_key()
             .map_err(|e| Error::Setup(e.to_string()))?;
 
-        let client = env.tangle_client().await?;
+        let client = alice_env.tangle_client().await?;
         let harness = Self {
-            base,
             http_endpoint,
             ws_endpoint,
             client,
             sr25519_signer,
             ecdsa_signer,
             alloy_key,
-            _temp_dir: test_dir,
+            temp_dir: test_dir,
+            config,
             _node: node,
         };
 
@@ -123,21 +151,60 @@ impl TestHarness for TangleTestHarness {
         harness
             .deploy_mbsm_if_needed()
             .await
-            .map_err(|_| Error::Setup("Failed to deploy MBSM".to_string()))?;
+            .map_err(|e| Error::Setup(format!("Failed to deploy MBSM: {e}")))?;
 
         Ok(harness)
     }
 
     fn env(&self) -> &GadgetConfiguration {
-        &self.base.env
+        &self.client.config
     }
+}
 
-    fn config(&self) -> &Self::Config {
-        &self.base.config
-    }
+struct NodeInfo {
+    env: GadgetConfiguration,
+    client: TangleClient,
+    preferences: Preferences,
 }
 
 impl TangleTestHarness {
+    async fn get_all_node_info<const N: usize>(&self) -> Result<Vec<NodeInfo>, RunnerError> {
+        let mut nodes = vec![];
+
+        for name in &ENDOWED_TEST_NAMES[..N] {
+            let env = generate_env_from_node_id(
+                name,
+                self.http_endpoint.clone(),
+                self.ws_endpoint.clone(),
+                self.temp_dir.path(),
+            )
+            .await?;
+
+            let client = env
+                .tangle_client()
+                .await
+                .map_err(|err| RunnerError::Other(err.to_string()))?;
+
+            let keystore = env.keystore();
+            let ecdsa_public = keystore
+                .first_local::<SpEcdsa>()
+                .map_err(|err| RunnerError::Other(err.to_string()))?;
+
+            let preferences = Preferences {
+                key: gadget_runners::tangle::tangle::decompress_pubkey(&ecdsa_public.0 .0).unwrap(),
+                price_targets: PriceTargets::default().0,
+            };
+
+            nodes.push(NodeInfo {
+                env,
+                client,
+                preferences,
+            })
+        }
+
+        Ok(nodes)
+    }
+
     /// Gets a reference to the Tangle client
     pub fn client(&self) -> &TangleClient {
         &self.client
@@ -152,10 +219,10 @@ impl TangleTestHarness {
         if let Some((rev, addr)) = latest_revision {
             debug!("MBSM is deployed at revision #{rev} at address {addr}");
             return Ok(());
-        } else {
-            debug!("MBSM is not deployed");
         }
 
+        debug!("MBSM is not deployed");
+
         let bytecode = tnt_core_bytecode::bytecode::MASTER_BLUEPRINT_SERVICE_MANAGER;
         transactions::deploy_new_mbsm_revision(
             self.ws_endpoint.as_str(),
@@ -163,7 +230,7 @@ impl TangleTestHarness {
             &self.sr25519_signer,
             self.alloy_key.clone(),
             bytecode,
-            alloy_primitives::address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // TODO: User-defined address?
+            alloy_primitives::address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // TODO: User-defined address
         )
         .await
         .map_err(|e| Error::Setup(e.to_string()))?;
@@ -192,16 +259,6 @@ impl TangleTestHarness {
         manifest.package.unwrap().name
     }
 
-    pub fn get_default_operator_preferences(&self) -> Preferences {
-        Preferences {
-            key: gadget_runners::tangle::tangle::decompress_pubkey(
-                &self.ecdsa_signer.signer().public().0,
-            )
-            .unwrap(),
-            price_targets: PriceTargets::default().0,
-        }
-    }
-
     /// Deploys a blueprint from the current directory and returns its ID
     pub async fn deploy_blueprint(&self) -> Result<u64, Error> {
         let manifest_path = std::env::current_dir()?.join("Cargo.toml");
@@ -219,27 +276,43 @@ impl TangleTestHarness {
     ///
     /// # Note
     /// The Service ID will always be 0 if automatic registration is disabled, as there is not yet a service to have an ID
-    pub async fn setup_services(
+    pub async fn setup_services<const N: usize>(
         &self,
         exit_after_registration: bool,
-    ) -> Result<(TangleTestEnv, u64, u64), Error> {
+    ) -> Result<(MultiNodeTestEnv, u64, u64), Error> {
+        const { assert!(N > 0, "Must have at least 1 initial node") };
+
         // Deploy blueprint
         let blueprint_id = self.deploy_blueprint().await?;
 
-        // Join operators
-        join_operators(&self.client, &self.sr25519_signer)
-            .await
-            .map_err(|e| Error::Setup(e.to_string()))?;
+        let nodes = self.get_all_node_info::<N>().await?;
 
         // Setup operator and get service
-        let preferences = self.get_default_operator_preferences();
         let service_id = if !exit_after_registration {
-            setup_operator_and_service(
-                &self.client,
-                &self.sr25519_signer,
+            let mut all_clients = Vec::new();
+            let mut all_signers = Vec::new();
+            let mut all_preferences = Vec::new();
+
+            for node in nodes {
+                let keystore = node.env.keystore();
+                let sr25519_public = keystore
+                    .first_local::<SpSr25519>()
+                    .map_err(|err| RunnerError::Other(err.to_string()))?;
+                let sr25519_pair = keystore
+                    .get_secret::<SpSr25519>(&sr25519_public)
+                    .map_err(|err| RunnerError::Other(err.to_string()))?;
+                let sr25519_signer = TanglePairSigner::new(sr25519_pair.0);
+                all_clients.push(node.client);
+                all_signers.push(sr25519_signer);
+                all_preferences.push(node.preferences);
+            }
+
+            setup_operator_and_service_multiple(
+                &all_clients[..N],
+                &all_signers[..N],
                 blueprint_id,
-                preferences,
-                !exit_after_registration,
+                &all_preferences,
+                exit_after_registration,
             )
             .await
             .map_err(|e| Error::Setup(e.to_string()))?
@@ -247,62 +320,119 @@ impl TangleTestHarness {
             0
         };
 
-        let config = TangleConfig::new(PriceTargets::default())
-            .with_exit_after_register(exit_after_registration);
-
-        // Create and spawn test environment
-        let test_env = TangleTestEnv::new(config, self.env().clone())?;
+        // Create and initialize the new multi-node environment
+        let executor = MultiNodeTestEnv::new::<N>(self.config.clone()).await?;
 
-        Ok((test_env, service_id, blueprint_id))
+        Ok((executor, service_id, blueprint_id))
     }
 
-    /// Requests a service with the given blueprint and returns the newly created service ID
+    /// Submits a job to be executed
     ///
-    /// This function does not register for a service, it only requests service for a blueprint
-    /// that has already been registered to.
-    pub async fn request_service(&self, blueprint_id: u64) -> Result<u64, Error> {
-        let preferences = self.get_default_operator_preferences();
-        let service_id = setup_operator_and_service(
+    /// # Arguments
+    /// * `service_id` - The ID of the service to submit the job to
+    /// * `job_id` - The ID of the job to submit
+    /// * `inputs` - The input values for the job
+    ///
+    /// # Returns
+    /// The submitted job if successful
+    pub async fn submit_job(
+        &self,
+        service_id: u64,
+        job_id: u8,
+        inputs: Vec<InputValue>,
+    ) -> Result<JobCalled, Error> {
+        let job = transactions::submit_job(
             &self.client,
             &self.sr25519_signer,
-            blueprint_id,
-            preferences,
-            false,
+            service_id,
+            Job::from(job_id),
+            inputs,
+            0, // TODO: Should this take a call ID? or leave it up to the caller to verify?
         )
         .await
         .map_err(|e| Error::Setup(e.to_string()))?;
 
-        Ok(service_id)
+        Ok(job)
     }
 
-    /// Executes a job and verifies its output matches the expected result
+    /// Executes a previously submitted job and waits for completion
     ///
     /// # Arguments
-    /// * `service_id` - The ID of the service to execute the job on
-    /// * `job_id` - The ID of the job to execute
-    /// * `inputs` - The input values for the job
-    /// * `expected` - The expected output values
+    /// * `service_id` - The ID of the service the job was submitted to
+    /// * `job` - The submitted job to execute
     ///
     /// # Returns
-    /// The job results if execution was successful and outputs match expectations
-    pub async fn execute_job(
+    /// The job results if execution was successful
+    pub async fn wait_for_job_execution(
         &self,
         service_id: u64,
-        job_id: u8,
-        inputs: Vec<InputValue>,
-        expected: Vec<OutputValue>,
+        job: JobCalled,
     ) -> Result<JobResultSubmitted, Error> {
-        let results = submit_and_verify_job(
+        let results = transactions::wait_for_completion_of_tangle_job(
             &self.client,
-            &self.sr25519_signer,
             service_id,
-            Job::from(job_id),
-            inputs,
-            expected,
+            job.call_id,
+            1,
         )
         .await
         .map_err(|e| Error::Setup(e.to_string()))?;
 
         Ok(results)
     }
+
+    /// Verifies that job results match expected outputs
+    ///
+    /// # Arguments
+    /// * `results` - The actual job results
+    /// * `expected` - The expected output values
+    ///
+    /// # Returns
+    /// The verified results if they match expectations
+    pub fn verify_job(
+        &self,
+        results: JobResultSubmitted,
+        expected: Vec<OutputValue>,
+    ) -> Result<JobResultSubmitted, Error> {
+        assert_eq!(
+            results.result.len(),
+            expected.len(),
+            "Number of outputs doesn't match expected"
+        );
+
+        for (result, expected) in results.result.iter().zip(expected.iter()) {
+            assert_eq!(result, expected);
+        }
+
+        Ok(results)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn test_harness_setup() {
+        let test_dir = TempDir::new().unwrap();
+        let harness = TangleTestHarness::setup(test_dir).await;
+        assert!(harness.is_ok(), "Harness setup should succeed");
+
+        let harness = harness.unwrap();
+        assert!(
+            harness.client().now().await.is_some(),
+            "Client should be connected to live chain"
+        );
+    }
+
+    #[tokio::test]
+    async fn test_deploy_mbsm() {
+        let test_dir = TempDir::new().unwrap();
+        let harness = TangleTestHarness::setup(test_dir).await.unwrap();
+
+        // MBSM should be deployed during setup
+        let latest_revision = transactions::get_latest_mbsm_revision(harness.client())
+            .await
+            .unwrap();
+        assert!(latest_revision.is_some(), "MBSM should be deployed");
+    }
 }
diff --git a/crates/testing-utils/tangle/src/lib.rs b/crates/testing-utils/tangle/src/lib.rs
index d7f51033c..875af7d18 100644
--- a/crates/testing-utils/tangle/src/lib.rs
+++ b/crates/testing-utils/tangle/src/lib.rs
@@ -11,6 +11,7 @@ mod tests;
 
 pub mod harness;
 pub mod keys;
+pub mod multi_node;
 pub mod node;
 pub mod runner;
 
diff --git a/crates/testing-utils/tangle/src/multi_node.rs b/crates/testing-utils/tangle/src/multi_node.rs
new file mode 100644
index 000000000..c9a37c989
--- /dev/null
+++ b/crates/testing-utils/tangle/src/multi_node.rs
@@ -0,0 +1,545 @@
+use crate::harness::ENDOWED_TEST_NAMES;
+use crate::{
+    harness::{generate_env_from_node_id, TangleTestConfig},
+    runner::TangleTestEnv,
+    Error,
+};
+use futures::future::join_all;
+use gadget_config::{GadgetConfiguration, Multiaddr};
+use gadget_contexts::tangle::TangleClientContext;
+use gadget_contexts::{keystore::KeystoreContext, tangle::TangleClient};
+use gadget_core_testing_utils::runner::TestEnv;
+use gadget_crypto_tangle_pair_signer::TanglePairSigner;
+use gadget_event_listeners::core::InitializableEventHandler;
+use gadget_keystore::backends::Backend;
+use gadget_keystore::crypto::sp_core::SpSr25519;
+use gadget_runners::core::error::RunnerError;
+use gadget_runners::core::runner::BackgroundService;
+use gadget_runners::tangle::tangle::TangleConfig;
+use std::fmt::{Debug, Formatter};
+use std::future::Future;
+use std::str::FromStr;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::Arc;
+use tangle_subxt::subxt::tx::Signer;
+use tokio::sync::{broadcast, mpsc, oneshot, RwLock};
+
+#[derive(Clone, Debug)]
+enum NodeSlot {
+    Occupied(Arc<NodeHandle>),
+    Empty,
+}
+
+/// Improved multi-node test environment with better control and observability
+pub struct MultiNodeTestEnv {
+    nodes: Arc<RwLock<Vec<NodeSlot>>>,
+    command_tx: mpsc::Sender<EnvironmentCommand>,
+    event_tx: broadcast::Sender<TestEvent>,
+    config: Arc<TangleTestConfig>,
+    initialized_tx: Option<oneshot::Sender<()>>,
+    running_nodes: Arc<AtomicUsize>,
+}
+
+#[derive(Debug)]
+enum EnvironmentCommand {
+    AddNode {
+        node_id: usize,
+        result_tx: oneshot::Sender<Result<(), Error>>,
+    },
+    RemoveNode {
+        node_id: usize,
+        result_tx: oneshot::Sender<Result<(), Error>>,
+    },
+    Start {
+        result_tx: oneshot::Sender<Result<(), Error>>,
+    },
+    Shutdown,
+}
+
+#[derive(Debug, Clone)]
+pub enum TestEvent {
+    NodeAdded(usize),
+    NodeRemoved(usize),
+    NodeShutdown(usize),
+    Error(String),
+}
+
+impl MultiNodeTestEnv {
+    /// Creates a new multi-node test environment
+    pub async fn new<const N: usize>(config: TangleTestConfig) -> Result<Self, Error> {
+        const { assert!(N > 0, "Must have at least 1 initial node") };
+
+        let (command_tx, command_rx) = mpsc::channel(32);
+        let (event_tx, _) = broadcast::channel(100);
+        let (initialized_tx, _initialized_rx) = oneshot::channel();
+
+        let env = Self {
+            nodes: Arc::new(RwLock::new(vec![NodeSlot::Empty; N])),
+            command_tx,
+            event_tx: event_tx.clone(),
+            config: Arc::new(config),
+            initialized_tx: Some(initialized_tx),
+            running_nodes: Arc::new(AtomicUsize::new(0)),
+        };
+
+        Self::spawn_command_handler(
+            env.nodes.clone(),
+            env.config.clone(),
+            env.running_nodes.clone(),
+            command_rx,
+            event_tx,
+        );
+
+        Ok(env)
+    }
+
+    /// Initializes the multi node test environment with N nodes
+    pub async fn initialize(&mut self) -> Result<(), Error> {
+        if self.initialized_tx.is_none() {
+            // Already initialized
+            return Ok(());
+        }
+
+        let initial_node_count = self.nodes.read().await.len();
+
+        // First add N nodes
+        for node_id in 0..initial_node_count {
+            self.add_node(node_id).await?;
+        }
+
+        // Setup the bootnodes
+        if initial_node_count > 1 {
+            let nodes = self.nodes.read().await;
+            for (index, node) in nodes.iter().enumerate() {
+                let NodeSlot::Occupied(node) = node else {
+                    panic!("Not all nodes were initialized");
+                };
+
+                let mut bootnodes = Vec::new();
+                for node in nodes.iter().enumerate().filter(|(n, _)| *n != index) {
+                    let NodeSlot::Occupied(node) = node.1 else {
+                        panic!("Not all nodes were initialized");
+                    };
+
+                    bootnodes.push(node.addr.clone());
+                }
+
+                let mut env = node.test_env.write().await;
+                env.update_networking_config(bootnodes, node.port);
+            }
+        }
+
+        // Signal initialization is complete
+        if let Some(tx) = self.initialized_tx.take() {
+            let _ = tx.send(());
+        }
+
+        Ok(())
+    }
+
+    /// Adds a job to the node to be executed when the test is run.
+    ///
+    /// The job is added to the end of the list of jobs and can be stopped using the `stop_job`
+    /// method.
+    pub async fn add_job<
+        T: Fn(GadgetConfiguration) -> F + Clone + Send + Sync + 'static,
+        F: Future<Output = Result<K, E>> + Send + 'static,
+        K: InitializableEventHandler + Send + Sync + 'static,
+        E: std::fmt::Debug + Send + 'static,
+    >(
+        &self,
+        creator: T,
+    ) -> Result<(), E> {
+        let mut nodes = self.nodes.write().await;
+        for node in nodes.iter_mut() {
+            if let NodeSlot::Occupied(node) = node {
+                let job = creator(node.gadget_config().await).await?;
+                node.add_job(job).await;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub async fn start(&mut self) -> Result<(), Error> {
+        // Start all nodes' runners
+        let (result_tx, result_rx) = oneshot::channel();
+        self.command_tx
+            .send(EnvironmentCommand::Start { result_tx })
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+
+        // Wait for initialization to complete
+        result_rx.await.map_err(|e| Error::Setup(e.to_string()))??;
+
+        Ok(())
+    }
+
+    /// Adds a new node to the test environment
+    async fn add_node(&self, node_id: usize) -> Result<(), Error> {
+        let (result_tx, result_rx) = oneshot::channel();
+        self.command_tx
+            .send(EnvironmentCommand::AddNode { node_id, result_tx })
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+        result_rx.await.map_err(|e| Error::Setup(e.to_string()))?
+    }
+
+    /// Subscribes to test environment events
+    pub fn subscribe(&self) -> broadcast::Receiver<TestEvent> {
+        self.event_tx.subscribe()
+    }
+
+    /// Removes a node from the test environment
+    pub async fn remove_node(&self, node_id: usize) -> Result<(), Error> {
+        let (result_tx, result_rx) = oneshot::channel();
+        self.command_tx
+            .send(EnvironmentCommand::RemoveNode { node_id, result_tx })
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+        result_rx.await.map_err(|e| Error::Setup(e.to_string()))?
+    }
+
+    /// Shuts down the test environment
+    pub async fn shutdown(&self) -> Result<(), Error> {
+        self.command_tx
+            .send(EnvironmentCommand::Shutdown)
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+        Ok(())
+    }
+
+    fn spawn_command_handler(
+        nodes: Arc<RwLock<Vec<NodeSlot>>>,
+        config: Arc<TangleTestConfig>,
+        running_nodes: Arc<AtomicUsize>,
+        mut command_rx: mpsc::Receiver<EnvironmentCommand>,
+        event_tx: broadcast::Sender<TestEvent>,
+    ) {
+        tokio::spawn(async move {
+            let nodes = nodes.clone();
+            while let Some(cmd) = command_rx.recv().await {
+                match cmd {
+                    EnvironmentCommand::AddNode { node_id, result_tx } => {
+                        let result = Self::handle_add_node(
+                            nodes.clone(),
+                            node_id,
+                            config.clone(),
+                            &event_tx,
+                        )
+                        .await;
+                        let _ = result_tx.send(result);
+                    }
+                    EnvironmentCommand::RemoveNode { node_id, result_tx } => {
+                        let result =
+                            Self::handle_remove_node(nodes.clone(), node_id, &event_tx).await;
+                        let _ = result_tx.send(result);
+                    }
+                    EnvironmentCommand::Start { result_tx } => {
+                        let result =
+                            Self::handle_start(nodes.clone(), &event_tx, running_nodes.clone())
+                                .await;
+                        let _ = result_tx.send(result);
+                    }
+                    EnvironmentCommand::Shutdown => {
+                        Self::handle_shutdown(nodes.clone(), &event_tx).await;
+                        break;
+                    }
+                }
+            }
+        });
+    }
+
+    pub async fn node_handles(&self) -> Vec<Arc<NodeHandle>> {
+        self.nodes
+            .read()
+            .await
+            .iter()
+            .filter_map(|n| match n {
+                NodeSlot::Occupied(node) => Some(node.clone()),
+                NodeSlot::Empty => None,
+            })
+            .collect()
+    }
+
+    async fn handle_add_node(
+        nodes: Arc<RwLock<Vec<NodeSlot>>>,
+        node_id: usize,
+        config: Arc<TangleTestConfig>,
+        event_tx: &broadcast::Sender<TestEvent>,
+    ) -> Result<(), Error> {
+        let node = NodeHandle::new(node_id, &config).await?;
+        nodes.write().await[node_id] = NodeSlot::Occupied(node);
+        let _ = event_tx.send(TestEvent::NodeAdded(node_id));
+        Ok(())
+    }
+
+    async fn handle_remove_node(
+        nodes: Arc<RwLock<Vec<NodeSlot>>>,
+        node_id: usize,
+        event_tx: &broadcast::Sender<TestEvent>,
+    ) -> Result<(), Error> {
+        let nodes = nodes.read().await;
+
+        let NodeSlot::Occupied(node) = nodes[node_id].clone() else {
+            return Err(Error::Setup(format!("Node {} not found", node_id)));
+        };
+
+        // Send shutdown command to the node
+        if let Err(e) = node.shutdown().await {
+            let _ = event_tx.send(TestEvent::Error(format!(
+                "Failed to shutdown node {}: {}",
+                node_id, e
+            )));
+        }
+        let _ = event_tx.send(TestEvent::NodeRemoved(node_id));
+        Ok(())
+    }
+
+    async fn handle_start(
+        nodes: Arc<RwLock<Vec<NodeSlot>>>,
+        event_tx: &broadcast::Sender<TestEvent>,
+        running_nodes: Arc<AtomicUsize>,
+    ) -> Result<(), Error> {
+        let nodes = nodes.read().await;
+
+        assert!(
+            nodes.iter().all(|n| matches!(n, NodeSlot::Occupied(_))),
+            "Not all nodes were initialized"
+        );
+
+        // Start all node runners concurrently
+        let futures = nodes.iter().enumerate().map(|(node_id, node)| {
+            let running_nodes = running_nodes.clone();
+
+            async move {
+                let NodeSlot::Occupied(node) = node else {
+                    unreachable!()
+                };
+
+                if let Err(e) = node.start_runner().await {
+                    let _ = event_tx.send(TestEvent::Error(format!(
+                        "Failed to start node {}: {}",
+                        node_id, e
+                    )));
+                    return Err(e);
+                }
+                running_nodes.fetch_add(1, Ordering::SeqCst);
+                Ok(())
+            }
+        });
+
+        // Wait for all nodes to start
+        let results = join_all(futures).await;
+        for result in results {
+            result?;
+        }
+
+        Ok(())
+    }
+
+    async fn handle_shutdown(
+        nodes: Arc<RwLock<Vec<NodeSlot>>>,
+        event_tx: &broadcast::Sender<TestEvent>,
+    ) {
+        let nodes = nodes.read().await;
+        for (node_id, node) in nodes.iter().enumerate() {
+            if let NodeSlot::Occupied(node) = node {
+                if let Err(e) = node.shutdown().await {
+                    let _ = event_tx.send(TestEvent::Error(format!(
+                        "Failed to shutdown node {}: {}",
+                        node_id, e
+                    )));
+                }
+            }
+        }
+    }
+}
+
+struct NodeState {
+    is_running: bool,
+}
+
+impl Debug for NodeState {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("NodeState")
+            .field("is_running", &self.is_running)
+            .finish()
+    }
+}
+
+/// Commands that can be sent to individual nodes
+enum NodeCommand {
+    StartRunner {
+        result_tx: oneshot::Sender<Result<(), Error>>,
+    },
+    Shutdown,
+}
+
+impl Debug for NodeCommand {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            NodeCommand::StartRunner { .. } => f.write_str("StartRunner"),
+            NodeCommand::Shutdown => f.write_str("Shutdown"),
+        }
+    }
+}
+
+/// Represents a single node in the multi-node test environment
+pub struct NodeHandle {
+    node_id: usize,
+    addr: Multiaddr,
+    port: u16,
+    client: TangleClient,
+    signer: TanglePairSigner<sp_core::sr25519::Pair>,
+    state: Arc<RwLock<NodeState>>,
+    command_tx: mpsc::Sender<NodeCommand>,
+    test_env: Arc<RwLock<TangleTestEnv>>,
+}
+
+impl NodeHandle {
+    /// Adds a job to the node to be executed when the test is run.
+    ///
+    /// The job is added to the end of the list of jobs and can be stopped using the `stop_job`
+    /// method.
+    pub async fn add_job<K: InitializableEventHandler + Send + Sync + 'static>(&self, job: K) {
+        self.test_env.write().await.add_job(job)
+    }
+
+    pub async fn add_background_service<K: BackgroundService + Send + 'static>(&self, service: K) {
+        self.test_env.write().await.add_background_service(service)
+    }
+
+    pub async fn gadget_config(&self) -> GadgetConfiguration {
+        self.test_env.read().await.get_gadget_config()
+    }
+}
+
+impl Debug for NodeHandle {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("NodeHandle")
+            .field("node_id", &self.node_id)
+            .field("signer", &self.signer.address())
+            .field("test_env", &self.test_env)
+            .finish()
+    }
+}
+
+// Implementation for NodeHandle
+impl NodeHandle {
+    async fn new(node_id: usize, config: &TangleTestConfig) -> Result<Arc<Self>, Error> {
+        let (command_tx, command_rx) = mpsc::channel(32);
+        let state = Arc::new(RwLock::new(NodeState { is_running: true }));
+
+        // Create node environment and client
+        let env = generate_env_from_node_id(
+            ENDOWED_TEST_NAMES[node_id],
+            config.http_endpoint.clone(),
+            config.ws_endpoint.clone(),
+            config.temp_dir.as_path(),
+        )
+        .await?;
+
+        let client = env.tangle_client().await?;
+        let keystore = env.keystore();
+        let sr25519_public = keystore
+            .first_local::<SpSr25519>()
+            .map_err(|err| RunnerError::Other(err.to_string()))?;
+        let sr25519_pair = keystore
+            .get_secret::<SpSr25519>(&sr25519_public)
+            .map_err(|err| RunnerError::Other(err.to_string()))?;
+        let sr25519_signer = TanglePairSigner::new(sr25519_pair.0);
+
+        // Create TangleTestEnv for this node
+        let test_env = TangleTestEnv::new(TangleConfig::default(), env.clone())?;
+
+        let port = find_open_tcp_bind_port();
+        gadget_logging::info!("Binding node {node_id} to port {port}");
+
+        let addr = Multiaddr::from_str(&format!("/ip4/127.0.0.1/tcp/{port}"))
+            .expect("Should parse MultiAddr");
+
+        let node = Arc::new(Self {
+            node_id,
+            addr,
+            port,
+            client,
+            signer: sr25519_signer,
+            state,
+            command_tx,
+            test_env: Arc::new(RwLock::new(test_env)),
+        });
+
+        Self::spawn_command_handler(node.clone(), command_rx);
+        Ok(node)
+    }
+
+    /// Shuts down the node
+    pub async fn shutdown(&self) -> Result<(), Error> {
+        self.command_tx
+            .send(NodeCommand::Shutdown)
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+
+        // Wait for the node to mark itself as not running
+        let mut retries = 0;
+        while retries < 10 {
+            if !self.state.read().await.is_running {
+                return Ok(());
+            }
+            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
+            retries += 1;
+        }
+
+        Err(Error::Setup("Node failed to shutdown in time".to_string()))
+    }
+
+    async fn start_runner(&self) -> Result<(), Error> {
+        let (result_tx, result_rx) = oneshot::channel();
+        self.command_tx
+            .send(NodeCommand::StartRunner { result_tx })
+            .await
+            .map_err(|e| Error::Setup(e.to_string()))?;
+        result_rx.await.map_err(|e| Error::Setup(e.to_string()))?
+    }
+
+    fn spawn_command_handler(node: Arc<Self>, mut command_rx: mpsc::Receiver<NodeCommand>) {
+        let state = node.state.clone();
+
+        tokio::spawn(async move {
+            while let Some(cmd) = command_rx.recv().await {
+                match cmd {
+                    NodeCommand::StartRunner { result_tx } => {
+                        let result = node.test_env.read().await.run_runner().await;
+                        let _ = result_tx.send(result.map_err(|e| Error::Setup(e.to_string())));
+                    }
+                    NodeCommand::Shutdown => {
+                        let mut state = state.write().await;
+                        state.is_running = false;
+                        break;
+                    }
+                }
+            }
+        });
+    }
+
+    /// Gets a reference to the node's client
+    pub fn client(&self) -> &TangleClient {
+        &self.client
+    }
+
+    /// Gets a reference to the node's signer
+    pub fn signer(&self) -> &TanglePairSigner<sp_core::sr25519::Pair> {
+        &self.signer
+    }
+}
+
+pub fn find_open_tcp_bind_port() -> u16 {
+    let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Should bind to localhost");
+    let port = listener
+        .local_addr()
+        .expect("Should have a local address")
+        .port();
+    drop(listener);
+    port
+}
diff --git a/crates/testing-utils/tangle/src/node/mod.rs b/crates/testing-utils/tangle/src/node/mod.rs
index e2b0b4505..5b3e8ef2c 100644
--- a/crates/testing-utils/tangle/src/node/mod.rs
+++ b/crates/testing-utils/tangle/src/node/mod.rs
@@ -9,8 +9,8 @@ pub mod transactions;
 
 pub use testnet::NodeConfig;
 
-const TANGLE_RELEASE_MAC: &str = "https://github.com/tangle-network/tangle/releases/download/v1.2.8/tangle-testnet-manual-seal-darwin-amd64";
-const TANGLE_RELEASE_LINUX: &str = "https://github.com/tangle-network/tangle/releases/download/v1.2.8/tangle-testnet-manual-seal-linux-amd64";
+const TANGLE_RELEASE_MAC: &str = "https://github.com/tangle-network/tangle/releases/download/v1.2.9/tangle-testnet-manual-seal-darwin-amd64";
+const TANGLE_RELEASE_LINUX: &str = "https://github.com/tangle-network/tangle/releases/download/v1.2.9/tangle-testnet-manual-seal-linux-amd64";
 
 /// Downloads the appropriate Tangle binary for the current platform and returns the path
 pub async fn download_tangle_binary() -> Result<PathBuf, Box<dyn std::error::Error>> {
@@ -73,10 +73,11 @@ pub async fn run(config: NodeConfig) -> Result<SubstrateNode, Error> {
 
     // Add binary paths
     if config.use_local_tangle {
-        let tangle_from_env =
-            std::env::var(TANGLE_NODE_ENV).unwrap_or_else(|_| "tangle".to_string());
+        if let Ok(tangle_from_env) = std::env::var(TANGLE_NODE_ENV) {
+            builder.add_binary_path(tangle_from_env);
+        }
+
         builder
-            .add_binary_path(tangle_from_env)
             .add_binary_path("../tangle/target/release/tangle")
             .add_binary_path("../../tangle/target/release/tangle")
             .add_binary_path("../../../tangle/target/release/tangle");
diff --git a/crates/testing-utils/tangle/src/node/testnet.rs b/crates/testing-utils/tangle/src/node/testnet.rs
index e92575671..ca0ea7f1c 100644
--- a/crates/testing-utils/tangle/src/node/testnet.rs
+++ b/crates/testing-utils/tangle/src/node/testnet.rs
@@ -119,7 +119,7 @@ impl SubstrateNodeBuilder {
             let binary_path = &std::path::absolute(binary_path)
                 .expect("bad path")
                 .into_os_string();
-            gadget_logging::info!("Trying to spawn binary at {:?}", binary_path);
+            gadget_logging::info!("Trying to spawn Tangle node binary at {:?}", binary_path);
             self.custom_flags
                 .insert("base-path".into(), Some(path.clone().into()));
 
@@ -148,7 +148,7 @@ impl SubstrateNodeBuilder {
         let stderr_handle = thread::spawn(move || {
             let reader = BufReader::new(stderr);
             for line in reader.lines().map_while(Result::ok) {
-                gadget_logging::debug!("node-stderr: {}", line);
+                tracing::debug!(target: "tangle-node", "node-stderr: {}", line);
                 let _ = init_tx_clone.send(line.clone());
                 let _ = log_tx_clone.send(line);
             }
@@ -162,7 +162,7 @@ impl SubstrateNodeBuilder {
         let stdout_handle = thread::spawn(move || {
             let reader = BufReader::new(stdout);
             for line in reader.lines().map_while(Result::ok) {
-                gadget_logging::debug!("node-stdout: {}", line);
+                tracing::debug!(target: "tangle-node", "node-stdout: {}", line);
                 let _ = log_tx_stdout.send(line);
             }
         });
@@ -209,7 +209,7 @@ impl SubstrateNodeBuilder {
             cmd.arg(arg);
         }
 
-        gadget_logging::trace!("Spawning node with command: {cmd:?}");
+        gadget_logging::trace!("Spawning Tangle node with command: {cmd:?}");
         cmd.spawn()
     }
 }
@@ -284,7 +284,7 @@ impl SubstrateNode {
             let handle = thread::spawn(move || {
                 let reader = BufReader::new(stdout);
                 for line in reader.lines().map_while(Result::ok) {
-                    gadget_logging::debug!("node-stdout: {}", line);
+                    tracing::debug!(target: "tangle-node","node-stdout: {}", line);
                     let _ = log_tx_clone.send(line);
                 }
             });
@@ -297,7 +297,7 @@ impl SubstrateNode {
             let handle = thread::spawn(move || {
                 let reader = BufReader::new(stderr);
                 for line in reader.lines().map_while(Result::ok) {
-                    gadget_logging::debug!("node-stderr: {}", line);
+                    tracing::debug!(target: "tangle-node","node-stderr: {}", line);
                     let _ = log_tx_clone.send(line);
                 }
             });
@@ -330,7 +330,7 @@ impl SubstrateNode {
         cmd.arg(format!("--rpc-port={}", self.ws_port));
         cmd.arg(format!("--port={}", self.p2p_port));
 
-        gadget_logging::debug!("Restarting node with command: {:?}", cmd);
+        gadget_logging::debug!("Restarting Tangle node with command: {:?}", cmd);
         cmd.spawn()
     }
 
@@ -340,7 +340,7 @@ impl SubstrateNode {
             let handle = thread::spawn(move || {
                 let reader = BufReader::new(stdout);
                 for line in reader.lines().map_while(Result::ok) {
-                    gadget_logging::debug!("node-stdout: {}", line);
+                    tracing::debug!(target: "tangle-node", "node-stdout: {}", line);
                     if let Some(tx) = &log_tx {
                         let _ = tx.send(line);
                     }
@@ -354,7 +354,7 @@ impl SubstrateNode {
             let handle = thread::spawn(move || {
                 let reader = BufReader::new(stderr);
                 for line in reader.lines().map_while(Result::ok) {
-                    gadget_logging::debug!("node-stderr: {}", line);
+                    tracing::debug!(target: "tangle-node", "node-stderr: {}", line);
                     if let Some(tx) = &log_tx {
                         let _ = tx.send(line);
                     }
@@ -409,7 +409,7 @@ fn try_find_substrate_port_from_output(rx: mpsc::Receiver<String>) -> SubstrateN
             Err(mpsc::RecvTimeoutError::Disconnected) => break,
         };
 
-        gadget_logging::debug!("{}", line);
+        tracing::debug!(target: "tangle-node", "{}", line);
         log.push_str(&line);
         log.push('\n');
 
diff --git a/crates/testing-utils/tangle/src/node/transactions.rs b/crates/testing-utils/tangle/src/node/transactions.rs
index 3126b8de4..ffe50c833 100644
--- a/crates/testing-utils/tangle/src/node/transactions.rs
+++ b/crates/testing-utils/tangle/src/node/transactions.rs
@@ -18,18 +18,21 @@ use tangle_subxt::subxt::{
     utils::AccountId32,
     Config,
 };
+use tangle_subxt::tangle_testnet_runtime::api::assets::events::created::AssetId;
+use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::services::types::AssetSecurityCommitment;
 use tangle_subxt::tangle_testnet_runtime::api::{
     self,
     runtime_types::{
-        pallet_services::module::Call, sp_arithmetic::per_things::Percent,
-        tangle_primitives::services::Asset, tangle_testnet_runtime::RuntimeCall,
+        pallet_services::module::Call,
+        sp_arithmetic::per_things::Percent,
+        tangle_primitives::services::types::{Asset, AssetSecurityRequirement, MembershipModel},
+        tangle_testnet_runtime::RuntimeCall,
     },
     services::{
         calls::types::{
             call::{Args, Job},
             create_blueprint::Blueprint,
             register::{Preferences, RegistrationArgs},
-            request::{Assets, PaymentAsset},
         },
         events::{JobCalled, JobResultSubmitted, MasterBlueprintServiceManagerRevised},
     },
@@ -198,7 +201,7 @@ pub async fn submit_job<T: Signer<TangleConfig>>(
     service_id: u64,
     job_id: Job,
     job_params: Args,
-    call_id: u64,
+    _call_id: u64, // TODO: Actually verify this
 ) -> Result<JobCalled, TransactionError> {
     let call = api::tx().services().call(service_id, job_id, job_params);
     let events = client
@@ -215,7 +218,6 @@ pub async fn submit_job<T: Signer<TangleConfig>>(
         if job_called.service_id == service_id
             && job_called.job == job_id
             && user.account_id() == job_called.caller
-            && job_called.call_id == call_id
         {
             return Ok(job_called);
         }
@@ -224,25 +226,39 @@ pub async fn submit_job<T: Signer<TangleConfig>>(
     Err(TransactionError::NoJobCalled)
 }
 
-/// Requests a service with a given blueprint. This is meant for testing, and will allow any node
-/// to make a call to run a service, and will have all nodes running the service.
+/// Requests a service with a given blueprint.
+///
+/// This is meant for testing.
+///
+/// `user` will be the only permitted caller, and all `test_nodes` will be selected as operators.
 pub async fn request_service<T: Signer<TangleConfig>>(
     client: &TestClient,
     user: &T,
     blueprint_id: u64,
     test_nodes: Vec<AccountId32>,
     value: u128,
+    optional_assets: Option<Vec<AssetSecurityRequirement<AssetId>>>,
 ) -> Result<(), TransactionError> {
+    info!(requester = ?user.account_id(), ?test_nodes, %blueprint_id, "Requesting service");
+    let min_operators = test_nodes.len() as u32;
+    let security_requirements = optional_assets.unwrap_or_else(|| {
+        vec![AssetSecurityRequirement {
+            asset: Asset::Custom(0),
+            min_exposure_percent: Percent(50),
+            max_exposure_percent: Percent(80),
+        }]
+    });
     let call = api::tx().services().request(
-        None, // TODO: Ensure this is okay for testing
+        None,
         blueprint_id,
-        test_nodes.clone(),
+        Vec::new(),
         test_nodes,
         Default::default(),
-        Assets::from([0]),
+        security_requirements,
         1000,
-        PaymentAsset::from(Asset::Custom(0)),
+        Asset::Custom(0),
         value,
+        MembershipModel::Fixed { min_operators },
     );
     let res = client
         .subxt_client()
@@ -359,17 +375,40 @@ pub async fn get_latest_mbsm_revision(
     Ok(res.0.pop().map(|addr| (ver, addr.0.into())))
 }
 
+pub fn get_security_requirement(a: AssetId, p: &[u8; 2]) -> AssetSecurityRequirement<AssetId> {
+    AssetSecurityRequirement {
+        asset: Asset::Custom(a),
+        min_exposure_percent: Percent(p[0]),
+        max_exposure_percent: Percent(p[1]),
+    }
+}
+
+pub fn get_security_commitment(a: Asset<AssetId>, p: u8) -> AssetSecurityCommitment<AssetId> {
+    AssetSecurityCommitment {
+        asset: a,
+        exposure_percent: Percent(p),
+    }
+}
+
 /// Approves a service request. This is meant for testing, and will always approve the request.
 pub async fn approve_service<T: Signer<TangleConfig>>(
     client: &TestClient,
     caller: &T,
     request_id: u64,
     restaking_percent: u8,
+    optional_assets: Option<Vec<AssetSecurityCommitment<AssetId>>>,
 ) -> Result<(), TransactionError> {
     info!("Approving service request ...");
+    let native_security_commitments =
+        vec![get_security_commitment(Asset::Custom(0), restaking_percent)];
+    let security_commitments = match optional_assets {
+        Some(assets) => [native_security_commitments, assets].concat(),
+        None => native_security_commitments,
+    };
+
     let call = api::tx()
         .services()
-        .approve(request_id, Percent(restaking_percent));
+        .approve(request_id, security_commitments);
     let res = client
         .subxt_client()
         .tx()
@@ -403,18 +442,44 @@ pub async fn get_next_request_id(client: &TestClient) -> Result<u64, Transaction
 /// 5. Returns the newly created service ID from events
 pub async fn setup_operator_and_service<T: Signer<TangleConfig>>(
     client: &TestClient,
-    sr25519_signer: &T,
+    sr25519_signer: T,
     blueprint_id: u64,
     preferences: Preferences,
     exit_after_registration: bool,
 ) -> Result<u64, TransactionError> {
-    if exit_after_registration {
+    setup_operator_and_service_multiple(
+        &[client.clone()],
+        &[sr25519_signer],
+        blueprint_id,
+        &[preferences],
+        exit_after_registration,
+    )
+    .await
+}
+
+pub async fn setup_operator_and_service_multiple<T: Signer<TangleConfig>>(
+    clients: &[TestClient],
+    sr25519_signers: &[T],
+    blueprint_id: u64,
+    preferences: &[Preferences],
+    _exit_after_registration: bool,
+) -> Result<u64, TransactionError> {
+    let alice_signer = sr25519_signers
+        .first()
+        .ok_or(TransactionError::Other("No signers".to_string()))?;
+
+    let alice_client = clients
+        .first()
+        .ok_or(TransactionError::Other("No client".to_string()))?;
+
+    for ((operator, client), preferences) in sr25519_signers.iter().zip(clients).zip(preferences) {
+        join_operators(client, operator).await?;
         // Register for blueprint
         register_blueprint(
             client,
-            sr25519_signer,
+            operator,
             blueprint_id,
-            preferences,
+            preferences.clone(),
             RegistrationArgs::new(),
             0,
         )
@@ -422,29 +487,27 @@ pub async fn setup_operator_and_service<T: Signer<TangleConfig>>(
     }
 
     // Get the current service ID before requesting new service
-    let prev_service_id = get_next_service_id(client).await?;
+    let prev_service_id = get_next_service_id(alice_client).await?;
 
-    // Request service
-    let account_id = sr25519_signer.account_id();
-    request_service(
-        client,
-        sr25519_signer,
-        blueprint_id,
-        vec![account_id.clone()],
-        0,
-    )
-    .await?;
+    let accounts = sr25519_signers
+        .iter()
+        .map(|s| s.account_id())
+        .collect::<Vec<_>>();
+    request_service(alice_client, alice_signer, blueprint_id, accounts, 0, None).await?;
 
     // Approve the service request and wait for completion
-    let request_id = get_next_request_id(client).await?.saturating_sub(1);
-    approve_service(client, sr25519_signer, request_id, 20).await?;
+    let request_id = get_next_request_id(alice_client).await?.saturating_sub(1);
+
+    for (signer, client) in sr25519_signers.iter().zip(clients) {
+        approve_service(client, signer, request_id, 50, None).await?;
+    }
 
     // Get the new service ID from events
-    let new_service_id = get_next_service_id(client).await?;
+    let new_service_id = get_next_service_id(alice_client).await?;
     assert!(new_service_id > prev_service_id);
 
     // Verify the service belongs to our blueprint
-    let service = client
+    let service = alice_client
         .subxt_client()
         .storage()
         .at_latest()
@@ -463,6 +526,22 @@ pub async fn setup_operator_and_service<T: Signer<TangleConfig>>(
     Ok(new_service_id.saturating_sub(1))
 }
 
+/// Submits a job to a service and verifies the outputs match the expected values.
+///
+/// # Warning
+/// This method should only be used in a single node context. For multi-node testing,
+/// use the multi-node test harness methods instead.
+///
+/// # Arguments
+/// * `client` - The test client to use for submitting the job
+/// * `signer` - The signer to use for submitting the job
+/// * `service_id` - The ID of the service to submit the job to
+/// * `job` - The job to submit
+/// * `inputs` - The inputs to provide to the job
+/// * `expected_outputs` - The expected outputs from the job
+///
+/// # Returns
+/// The job results if successful
 pub async fn submit_and_verify_job<T: Signer<TangleConfig>>(
     client: &TestClient,
     signer: &T,
diff --git a/crates/testing-utils/tangle/src/runner.rs b/crates/testing-utils/tangle/src/runner.rs
index 97154685e..efe0b8a3d 100644
--- a/crates/testing-utils/tangle/src/runner.rs
+++ b/crates/testing-utils/tangle/src/runner.rs
@@ -1,17 +1,40 @@
 #![allow(dead_code)]
-use gadget_config::GadgetConfiguration;
+
+use gadget_config::{GadgetConfiguration, Multiaddr};
 use gadget_core_testing_utils::runner::{TestEnv, TestRunner};
 use gadget_event_listeners::core::InitializableEventHandler;
 use gadget_runners::core::error::RunnerError as Error;
 use gadget_runners::core::runner::BackgroundService;
 use gadget_runners::tangle::tangle::TangleConfig;
+use std::fmt::{Debug, Formatter};
+use tokio::sync::Mutex;
 use tokio::task::JoinHandle;
 
 pub struct TangleTestEnv {
     runner: TestRunner,
     config: TangleConfig,
     gadget_config: GadgetConfiguration,
-    runner_handle: Option<JoinHandle<Result<(), Error>>>,
+    runner_handle: Mutex<Option<JoinHandle<Result<(), Error>>>>,
+}
+
+impl TangleTestEnv {
+    pub(crate) fn update_networking_config(
+        &mut self,
+        bootnodes: Vec<Multiaddr>,
+        network_bind_port: u16,
+    ) {
+        self.gadget_config.bootnodes = bootnodes;
+        self.gadget_config.network_bind_port = network_bind_port;
+    }
+}
+
+impl Debug for TangleTestEnv {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("TangleTestEnv")
+            .field("config", &self.config)
+            .field("gadget_config", &self.gadget_config)
+            .finish()
+    }
 }
 
 impl TestEnv for TangleTestEnv {
@@ -24,13 +47,13 @@ impl TestEnv for TangleTestEnv {
             runner,
             config,
             gadget_config: env,
-            runner_handle: None,
+            runner_handle: Mutex::new(None),
         })
     }
 
     fn add_job<J>(&mut self, job: J)
     where
-        J: InitializableEventHandler + Send + 'static,
+        J: InitializableEventHandler + Send + Sync + 'static,
     {
         self.runner.add_job(job);
     }
@@ -42,54 +65,58 @@ impl TestEnv for TangleTestEnv {
         self.runner.add_background_service(service);
     }
 
-    fn get_gadget_config(self) -> GadgetConfiguration {
+    fn get_gadget_config(&self) -> GadgetConfiguration {
         self.gadget_config.clone()
     }
 
-    async fn run_runner(&mut self) -> Result<(), Error> {
+    async fn run_runner(&self) -> Result<(), Error> {
         // Spawn the runner in a background task
         let mut runner = self.runner.clone();
         let handle = tokio::spawn(async move { runner.run().await });
 
-        self.runner_handle = Some(handle);
+        let mut _guard = self.runner_handle.lock().await;
+        *_guard = Some(handle);
 
         // Brief delay to allow for startup
         tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
 
         // Just check if it failed immediately
-        if let Some(handle) = self.runner_handle.take() {
-            if handle.is_finished() {
-                gadget_logging::info!("Runner task finished OK");
-                match handle.await {
-                    Ok(Ok(())) => Ok(()),
-                    Ok(Err(e)) => {
-                        gadget_logging::error!("Runner failed during startup: {}", e);
-                        Err(Error::Tangle(format!(
-                            "Runner failed during startup: {}",
-                            e
-                        )))
-                    }
-                    Err(e) => {
-                        gadget_logging::error!("Runner task panicked: {}", e);
-                        Err(Error::Tangle(format!("Runner task panicked: {}", e)))
-                    }
-                }
-            } else {
-                // Put the handle back since the runner is still running
-                self.runner_handle = Some(handle);
-                gadget_logging::info!("Runner started successfully");
-                Ok(())
+        let Some(handle) = _guard.take() else {
+            return Err(Error::Tangle("Failed to spawn runner task".to_string()));
+        };
+
+        if !handle.is_finished() {
+            // Put the handle back since the runner is still running
+            *_guard = Some(handle);
+            gadget_logging::info!("Runner started successfully");
+            return Ok(());
+        }
+
+        gadget_logging::info!("Runner task finished OK");
+        match handle.await {
+            Ok(Ok(())) => Ok(()),
+            Ok(Err(e)) => {
+                gadget_logging::error!("Runner failed during startup: {}", e);
+                Err(Error::Tangle(format!(
+                    "Runner failed during startup: {}",
+                    e
+                )))
+            }
+            Err(e) => {
+                gadget_logging::error!("Runner task panicked: {}", e);
+                Err(Error::Tangle(format!("Runner task panicked: {}", e)))
             }
-        } else {
-            Err(Error::Tangle("Failed to spawn runner task".to_string()))
         }
     }
 }
 
 impl Drop for TangleTestEnv {
     fn drop(&mut self) {
-        if let Some(handle) = &self.runner_handle {
-            handle.abort();
-        }
+        futures::executor::block_on(async {
+            let mut _guard = self.runner_handle.lock().await;
+            if let Some(handle) = _guard.take() {
+                handle.abort();
+            }
+        })
     }
 }
diff --git a/crates/testing-utils/tangle/src/tests.rs b/crates/testing-utils/tangle/src/tests.rs
index ad84452d9..2b5ce1097 100644
--- a/crates/testing-utils/tangle/src/tests.rs
+++ b/crates/testing-utils/tangle/src/tests.rs
@@ -1,7 +1,6 @@
 use crate::Error;
 use crate::TangleTestHarness;
 use gadget_client_tangle::EventsClient;
-use gadget_clients::GadgetServicesClient;
 use gadget_core_testing_utils::harness::TestHarness;
 use gadget_logging::setup_log;
 use tangle_subxt::subxt::tx::Signer;
@@ -77,7 +76,7 @@ async fn test_services_client() -> Result<(), Error> {
     let blueprints = services
         .query_operator_blueprints(block_hash, harness.sr25519_signer.account_id().clone())
         .await
-        .unwrap();
+        .unwrap_or_default();
     assert!(
         blueprints.is_empty(),
         "New operator should have no blueprints"
@@ -109,32 +108,6 @@ async fn test_events_client() -> Result<(), Error> {
     Ok(())
 }
 
-#[tokio::test]
-async fn test_gadget_services_client() -> Result<(), Error> {
-    setup_log();
-
-    let temp_dir = tempfile::TempDir::new()?;
-    let harness = TangleTestHarness::setup(temp_dir).await?;
-
-    // Test operator set retrieval
-    let operators = harness.client().get_operators().await.unwrap();
-    assert!(!operators.is_empty(), "Should have at least one operator");
-
-    // Test operator ID retrieval
-    let operator_id = harness.client().operator_id().await.unwrap();
-    assert_eq!(
-        operator_id.0.len(),
-        33,
-        "Operator ID should be valid ECDSA public key"
-    );
-
-    // Test blueprint ID retrieval
-    let blueprint_id = harness.client().blueprint_id().await.unwrap();
-    assert!(blueprint_id > 0, "Blueprint ID should be positive");
-
-    Ok(())
-}
-
 #[tokio::test]
 async fn test_service_operators() -> Result<(), Error> {
     setup_log();