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

Issue:
Gadget SDK - libp2p Networking Module
### Overview

As a blueprint developer, I may need access to a peer to peer networking, and for that we need to expose a pre-made p2p networking with sensible defaults. That just one line function that you call to start a p2p network.

```rs
let network = sdk::start_p2p_network().await?;
```

### Checklist

- [x] Add `networking` module
- [x] Expose a `networking::Config` that has the following props:
```rs
struct Config {
	identity: Keypair,
	bootnodes: Vec<Multiaddr>,
	bind_ip: Ipv4Addr,
	bind_port: u16,
	topics: Vec<String>,
}
```
- [x] expose a `start_p2p_network(config)` function


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml
index 74fe6b243..55bf1fbbe 100644
--- a/.github/workflows/validate_pr.yml
+++ b/.github/workflows/validate_pr.yml
@@ -33,32 +33,8 @@ jobs:
         run: cargo fmt -- --check
 
   linting:
-    name: Clippy
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout Code
-        uses: actions/checkout@v2
-
-      - name: Install Rust
-        uses: actions-rs/toolchain@v1
-        with:
-          profile: minimal
-          toolchain: stable
-          target: wasm32-unknown-unknown
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          cache-on-failure: "true"
-
-      - name: Install protobuf
-        run: sudo apt-get install protobuf-compiler
-
-      - name: Run Clippy
-        run: cargo clippy --package shell-manager --tests -- -D warnings
-
-  build:
     timeout-minutes: 120
-    name: cargo build
+    name: cargo clippy
     runs-on: macos-latest
     strategy:
         matrix:
@@ -66,6 +42,7 @@ jobs:
               gadget-core,
               gadget-common,
               shell-manager,
+              shell-sdk,
             ]
     steps:
       - name: checkout code
@@ -83,8 +60,8 @@ jobs:
       - name: install protobuf
         run: brew install protobuf
 
-      - name: build
-        run: cargo build --package ${{ matrix.package }}
+      - name: Run Clippy
+        run: cargo clippy --package ${{ matrix.package }} --tests -- -D warnings
 
   testing:
     timeout-minutes: 90
@@ -96,6 +73,7 @@ jobs:
               gadget-core,
               gadget-common,
               shell-manager,
+              shell-sdk,
             ]
     steps:
       - name: checkout code
@@ -127,6 +105,7 @@ jobs:
   wasm_checks:
     timeout-minutes: 90
     name: WASM checks
+    if: false # Disabled for now
     runs-on: macos-14
     steps:
       - name: checkout code
diff --git a/Cargo.lock b/Cargo.lock
index 25732f96a..a8ea8faf4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3715,20 +3715,31 @@ dependencies = [
 name = "gadget-sdk"
 version = "0.1.0"
 dependencies = [
+ "async-trait",
+ "bincode",
  "ed25519-zebra 4.0.3",
+ "elliptic-curve",
  "env_logger 0.11.3",
+ "futures",
+ "gadget-common",
+ "gadget-core",
+ "gadget-io",
  "getrandom 0.2.15",
  "hex",
  "hyper 0.14.30",
  "k256",
+ "libp2p 0.53.2",
  "log",
  "parking_lot 0.12.2",
  "prometheus",
  "rand",
  "rand_core 0.6.4",
  "schnorrkel",
+ "serde",
  "slog",
  "slog-term",
+ "sp-core",
+ "sp-io",
  "thiserror",
  "tokio",
  "tracing 0.1.40",
@@ -10227,6 +10238,7 @@ dependencies = [
  "gadget-common",
  "gadget-core",
  "gadget-io",
+ "gadget-sdk",
  "getrandom 0.2.15",
  "hex",
  "itertools 0.12.1",
@@ -10249,7 +10261,6 @@ dependencies = [
  "tangle-environment",
  "tangle-primitives",
  "tangle-subxt",
- "tangle-test-utils",
  "tokio_with_wasm",
  "toml 0.8.12",
  "tracing 0.1.40",
diff --git a/Cargo.toml b/Cargo.toml
index 248b27a10..97a8433fa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,6 +29,7 @@ repository = "https://github.com/webb-tools/gadget"
 gadget-core = { path = "./core" }
 gadget-common = { path = "./common", default-features = false }
 gadget-io = { path = "./gadget-io", default-features = false }
+gadget-sdk = { path = "./sdk" }
 tangle-test-utils = { path = "./tangle-test-utils" }
 protocol-macros = { path = "./protocol-macros" }
 shell-sdk = { path = "./shell-sdk", default-features = false }
@@ -192,6 +193,7 @@ anyhow = "1.0.79"
 libsecp256k1 = "0.7.1"
 rayon = { version = "1.8.0" }
 thiserror = { version = "1" }
+elliptic-curve = { version = "0.13.8" }
 aes-gcm = { version = "0.10", default-features = false, features = [
     "aes",
     "alloc",
diff --git a/environments/tangle/Cargo.toml b/environments/tangle/Cargo.toml
index 46f116677..bcc08b196 100644
--- a/environments/tangle/Cargo.toml
+++ b/environments/tangle/Cargo.toml
@@ -9,6 +9,7 @@ repository.workspace = true
 
 [features]
 default = ["std"]
+testing = []
 std = [
     "gadget-common/std",
     "sp-core/std",
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 2612dff75..23649c164 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -13,6 +13,7 @@ parking_lot = { workspace = true }
 rand_core = { version = "0.6.4", default-features = false }
 rand = { workspace = true, optional = true }
 hex = { workspace = true, default-features = false }
+elliptic-curve = { workspace = true }
 
 # Keystore deps
 k256 = { version = "0.13.3", optional = true }
@@ -34,6 +35,17 @@ tracing-subscriber = "0.3.18"
 env_logger = "0.11.3"
 tracing-wasm = { version = "0.2.1", optional = true }
 
+# Networking deps
+gadget-io = { workspace = true, optional = true }
+gadget-core = { workspace = true, optional = true }
+bincode = { workspace = true, optional = true }
+gadget-common = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+async-trait = { workspace = true, optional = true }
+futures = { workspace = true, optional = true }
+sp-io = { workspace = true, optional = true }
+sp-core = { workspace = true, optional = true }
+
 # WASM-only deps
 getrandom = { version = "0.2.15", optional = true }
 wasm-bindgen-test = { version = "0.3.42", optional = true }
@@ -42,6 +54,34 @@ wasm-bindgen-test = { version = "0.3.42", optional = true }
 hyper = { version = "0.14.16", default-features = false, features = ["client"] }
 slog-term = { version = "2.9.1" }
 
+[target.'cfg(not(target_family = "wasm"))'.dependencies.libp2p]
+optional = true
+workspace = true
+features = [
+  "tokio",
+  "gossipsub",
+  "mdns",
+  "noise",
+  "macros",
+  "yamux",
+  "tcp",
+  "quic",
+  "request-response",
+  "cbor",
+  "identify",
+  "kad",
+  "dcutr",
+  "relay",
+  "ping",
+  "dns",
+  "autonat"
+]
+
+[target.'cfg(target_family = "wasm")'.dependencies.libp2p]
+optional = true
+workspace = true
+default-features = false
+
 [features]
 default = ["full", "std"]
 std = [
@@ -56,7 +96,10 @@ std = [
 alloc = ["rand_core/alloc", "hex/alloc"]
 full = [
   "keystore",
+  "networking-libp2p",
 ]
+networking-libp2p = ["gadget-io", "bincode", "libp2p", "gadget-common", "serde", "async-trait", "futures", "gadget-core", "sp-io", "sp-core"]
+
 wasm = [
   "getrandom/js",
   "tracing-wasm",
diff --git a/sdk/src/keystore/backend/fs.rs b/sdk/src/keystore/backend/fs.rs
index 8da44147e..34d61811c 100644
--- a/sdk/src/keystore/backend/fs.rs
+++ b/sdk/src/keystore/backend/fs.rs
@@ -142,7 +142,7 @@ impl Backend for FilesystemKeystore {
 
     #[cfg(feature = "keystore-ecdsa")]
     fn ecdsa_generate_new(&self, seed: Option<&[u8]>) -> Result<ecdsa::Public, Error> {
-        let secret = ecdsa::generate_with_optional_seed(seed)?;
+        let secret = ecdsa::generate_with_optional_seed(seed).map_err(Error::Ecdsa)?;
         let public = secret.public_key();
         let path = self.key_file_path(&public.to_sec1_bytes(), KeyType::Ecdsa);
         Self::write_to_file(path, &secret.to_bytes()[..])?;
@@ -157,7 +157,7 @@ impl Backend for FilesystemKeystore {
     ) -> Result<Option<ecdsa::Signature>, Error> {
         let secret_bytes = self.secret_by_type(&public.to_sec1_bytes(), KeyType::Ecdsa)?;
         if let Some(buf) = secret_bytes {
-            let secret = ecdsa::secret_from_bytes(&buf)?;
+            let secret = ecdsa::secret_from_bytes(&buf).map_err(Error::Ecdsa)?;
             Ok(Some(ecdsa::sign(&secret, msg)))
         } else {
             Ok(None)
@@ -209,7 +209,7 @@ impl Backend for FilesystemKeystore {
     fn expose_ecdsa_secret(&self, public: &ecdsa::Public) -> Result<Option<ecdsa::Secret>, Error> {
         let secret_bytes = self.secret_by_type(&public.to_sec1_bytes(), KeyType::Ecdsa)?;
         if let Some(buf) = secret_bytes {
-            Ok(Some(ecdsa::secret_from_bytes(&buf)?))
+            Ok(Some(ecdsa::secret_from_bytes(&buf).map_err(Error::Ecdsa)?))
         } else {
             Ok(None)
         }
@@ -254,6 +254,7 @@ impl Backend for FilesystemKeystore {
         self.iter_keys(KeyType::Ecdsa)
             .flat_map(|b| ecdsa::Public::from_sec1_bytes(&b))
     }
+
     #[cfg(feature = "keystore-ed25519")]
     fn iter_ed25519(&self) -> impl Iterator<Item = ed25519::Public> {
         self.iter_keys(KeyType::Ed25519)
diff --git a/sdk/src/keystore/backend/mem.rs b/sdk/src/keystore/backend/mem.rs
index 3eb2770c4..190c58c81 100644
--- a/sdk/src/keystore/backend/mem.rs
+++ b/sdk/src/keystore/backend/mem.rs
@@ -9,7 +9,7 @@ use w3f_bls::SerializableToBytes;
 
 use parking_lot::RwLock;
 
-use crate::keystore::{bls381, ecdsa, ed25519, sr25519, Backend};
+use crate::keystore::{bls381, ecdsa, ed25519, sr25519, Backend, Error};
 
 /// The type alias for the In Memory `KeyMap`.
 type KeyMap<P, S> = Arc<RwLock<BTreeMap<P, S>>>;
@@ -95,10 +95,7 @@ impl InMemoryKeystore {
 
 impl Backend for InMemoryKeystore {
     #[cfg(feature = "keystore-sr25519")]
-    fn sr25519_generate_new(
-        &self,
-        seed: Option<&[u8]>,
-    ) -> Result<sr25519::Public, crate::keystore::Error> {
+    fn sr25519_generate_new(&self, seed: Option<&[u8]>) -> Result<sr25519::Public, Error> {
         let secret = sr25519::generate_with_optional_seed(seed)?;
         let public = secret.to_public();
         let old = self.sr25519.write().insert(public, secret);
@@ -111,7 +108,7 @@ impl Backend for InMemoryKeystore {
         &self,
         public: &sr25519::Public,
         msg: &[u8],
-    ) -> Result<Option<sr25519::Signature>, crate::keystore::Error> {
+    ) -> Result<Option<sr25519::Signature>, Error> {
         let lock = self.sr25519.read();
         let secret = lock.get(public);
         if let Some(secret) = secret {
@@ -122,10 +119,7 @@ impl Backend for InMemoryKeystore {
     }
 
     #[cfg(feature = "keystore-ed25519")]
-    fn ed25519_generate_new(
-        &self,
-        seed: Option<&[u8]>,
-    ) -> Result<ed25519::Public, crate::keystore::Error> {
+    fn ed25519_generate_new(&self, seed: Option<&[u8]>) -> Result<ed25519::Public, Error> {
         let secret = ed25519::generate_with_optional_seed(seed)?;
         let public = ed25519::to_public(&secret);
         let old = self
@@ -141,7 +135,7 @@ impl Backend for InMemoryKeystore {
         &self,
         public: &ed25519::Public,
         msg: &[u8],
-    ) -> Result<Option<ed25519::Signature>, crate::keystore::Error> {
+    ) -> Result<Option<ed25519::Signature>, Error> {
         let lock = self.ed25519.read();
         let secret = lock.get(&Ed25519PublicWrapper(*public));
         if let Some(secret) = secret {
@@ -152,11 +146,8 @@ impl Backend for InMemoryKeystore {
     }
 
     #[cfg(feature = "keystore-ecdsa")]
-    fn ecdsa_generate_new(
-        &self,
-        seed: Option<&[u8]>,
-    ) -> Result<ecdsa::Public, crate::keystore::Error> {
-        let secret = ecdsa::generate_with_optional_seed(seed)?;
+    fn ecdsa_generate_new(&self, seed: Option<&[u8]>) -> Result<ecdsa::Public, Error> {
+        let secret = ecdsa::generate_with_optional_seed(seed).map_err(Error::Ecdsa)?;
         let public = secret.public_key();
         let old = self.ecdsa.write().insert(public, secret);
         assert!(old.is_none(), "generated key already exists");
@@ -168,7 +159,7 @@ impl Backend for InMemoryKeystore {
         &self,
         public: &ecdsa::Public,
         msg: &[u8],
-    ) -> Result<Option<ecdsa::Signature>, crate::keystore::Error> {
+    ) -> Result<Option<ecdsa::Signature>, Error> {
         let lock = self.ecdsa.read();
         let secret = lock.get(public);
         if let Some(secret) = secret {
@@ -179,10 +170,7 @@ impl Backend for InMemoryKeystore {
     }
 
     #[cfg(feature = "keystore-bls381")]
-    fn bls381_generate_new(
-        &self,
-        seed: Option<&[u8]>,
-    ) -> Result<bls381::Public, crate::keystore::Error> {
+    fn bls381_generate_new(&self, seed: Option<&[u8]>) -> Result<bls381::Public, Error> {
         let secret = bls381::generate_with_optional_seed(seed);
         let public = bls381::to_public(&secret);
         let old = self
@@ -198,7 +186,7 @@ impl Backend for InMemoryKeystore {
         &self,
         public: &bls381::Public,
         msg: &[u8],
-    ) -> Result<Option<bls381::Signature>, crate::keystore::Error> {
+    ) -> Result<Option<bls381::Signature>, Error> {
         let mut lock = self.bls381.write();
         let secret = lock.get_mut(&Bls381PublicWrapper(*public));
         if let Some(secret) = secret {
@@ -212,16 +200,13 @@ impl Backend for InMemoryKeystore {
     fn expose_sr25519_secret(
         &self,
         public: &sr25519::Public,
-    ) -> Result<Option<sr25519::Secret>, crate::keystore::Error> {
+    ) -> Result<Option<sr25519::Secret>, Error> {
         let lock = self.sr25519.read();
         Ok(lock.get(public).cloned())
     }
 
     #[cfg(feature = "keystore-ecdsa")]
-    fn expose_ecdsa_secret(
-        &self,
-        public: &ecdsa::Public,
-    ) -> Result<Option<ecdsa::Secret>, crate::keystore::Error> {
+    fn expose_ecdsa_secret(&self, public: &ecdsa::Public) -> Result<Option<ecdsa::Secret>, Error> {
         let lock = self.ecdsa.read();
         Ok(lock.get(public).cloned())
     }
@@ -230,7 +215,7 @@ impl Backend for InMemoryKeystore {
     fn expose_ed25519_secret(
         &self,
         public: &ed25519::Public,
-    ) -> Result<Option<ed25519::Secret>, crate::keystore::Error> {
+    ) -> Result<Option<ed25519::Secret>, Error> {
         let lock = self.ed25519.read();
         Ok(lock.get(&Ed25519PublicWrapper(*public)).copied())
     }
@@ -239,7 +224,7 @@ impl Backend for InMemoryKeystore {
     fn expose_bls381_secret(
         &self,
         public: &bls381::Public,
-    ) -> Result<Option<bls381::Secret>, crate::keystore::Error> {
+    ) -> Result<Option<bls381::Secret>, Error> {
         let lock = self.bls381.read();
         Ok(lock.get(&Bls381PublicWrapper(*public)).cloned())
     }
diff --git a/sdk/src/keystore/ed25519.rs b/sdk/src/keystore/ed25519.rs
index 508df366a..30cf79a4d 100644
--- a/sdk/src/keystore/ed25519.rs
+++ b/sdk/src/keystore/ed25519.rs
@@ -9,7 +9,7 @@ use crate::random;
 pub fn generate_with_optional_seed(seed: Option<&[u8]>) -> Result<Secret, ed25519_zebra::Error> {
     match seed {
         Some(seed) => Secret::try_from(seed).map_err(|_| ed25519_zebra::Error::InvalidSliceLength),
-        None => Ok(Secret::new(&mut random::getrandom_or_panic())),
+        None => Ok(Secret::new(random::getrandom_or_panic())),
     }
 }
 
diff --git a/sdk/src/keystore/error.rs b/sdk/src/keystore/error.rs
index 03d083078..b2ea0ba91 100644
--- a/sdk/src/keystore/error.rs
+++ b/sdk/src/keystore/error.rs
@@ -14,7 +14,7 @@ pub enum Error {
     /// An error occurred during ecdsa module operation
     #[cfg(feature = "keystore-ecdsa")]
     #[error("ecdsa: {0}")]
-    Ecdsa(#[from] k256::elliptic_curve::Error),
+    Ecdsa(k256::elliptic_curve::Error),
     /// An error occurred during ed25519 module operation
     #[cfg(feature = "keystore-ed25519")]
     #[error("ed25519: {0}")]
diff --git a/sdk/src/keystore/sr25519.rs b/sdk/src/keystore/sr25519.rs
index f4b8ffe5e..83e3037cb 100644
--- a/sdk/src/keystore/sr25519.rs
+++ b/sdk/src/keystore/sr25519.rs
@@ -1,5 +1,6 @@
 //! Schnorrkel keypair implementation.
 
+use rand::prelude::ThreadRng;
 pub use schnorrkel::PublicKey as Public;
 pub use schnorrkel::SecretKey as Secret;
 pub use schnorrkel::Signature;
@@ -12,7 +13,9 @@ pub fn generate_with_optional_seed(
 ) -> Result<Secret, schnorrkel::SignatureError> {
     match seed {
         Some(seed) => Secret::from_bytes(seed),
-        None => Ok(Secret::generate()),
+        None => Ok(schnorrkel::SecretKey::generate_with(
+            &mut ThreadRng::default(),
+        )),
     }
 }
 
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
index c249f6ca4..484545ae9 100644
--- a/sdk/src/lib.rs
+++ b/sdk/src/lib.rs
@@ -6,8 +6,6 @@
     unsafe_code,
     unstable_features,
     unused_import_braces,
-    unused_qualifications,
-    missing_docs,
     rustdoc::broken_intra_doc_links,
     unused_results,
     clippy::all,
@@ -34,3 +32,6 @@ pub mod logging;
 /// Randomness generation module
 #[cfg(not(feature = "wasm"))]
 pub mod random;
+
+#[cfg(feature = "networking-libp2p")]
+pub mod network;
diff --git a/shell-sdk/src/network/gossip.rs b/sdk/src/network/gossip.rs
similarity index 91%
rename from shell-sdk/src/network/gossip.rs
rename to sdk/src/network/gossip.rs
index 3fca8e0f0..e33a1f73b 100644
--- a/shell-sdk/src/network/gossip.rs
+++ b/sdk/src/network/gossip.rs
@@ -1,6 +1,13 @@
+#![allow(
+    missing_debug_implementations,
+    unused_results,
+    clippy::module_name_repetitions,
+    clippy::exhaustive_enums
+)]
 use async_trait::async_trait;
 use gadget_common::environments::GadgetEnvironment;
 use gadget_common::prelude::{DebugLogger, Network};
+use gadget_common::sp_core::ecdsa;
 use gadget_core::job_manager::{ProtocolMessageMetadata, WorkManagerInterface};
 use gadget_io::tokio::sync::mpsc::UnboundedSender;
 use gadget_io::tokio::sync::{Mutex, RwLock};
@@ -10,7 +17,6 @@ use libp2p::{
     gossipsub, mdns, request_response, swarm::NetworkBehaviour, swarm::SwarmEvent, PeerId,
 };
 use serde::{Deserialize, Serialize};
-use sp_core::ecdsa;
 use std::collections::HashMap;
 use std::sync::atomic::AtomicU32;
 use std::sync::Arc;
@@ -108,9 +114,17 @@ impl<'a> NetworkService<'a> {
     }
 
     /// 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::*;
-        use SwarmEvent::*;
+        use MyBehaviourEvent::{
+            Dcutr, Gossipsub, Identify, Kadmelia, Mdns, P2p, Ping, Relay, RelayClient,
+        };
+        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)) => {
@@ -259,11 +273,13 @@ pub struct GossipHandle {
 }
 
 impl GossipHandle {
+    #[must_use]
     pub fn connected_peers(&self) -> usize {
         self.connected_peers
             .load(std::sync::atomic::Ordering::Relaxed) as usize
     }
 
+    #[must_use]
     pub fn topic(&self) -> IdentTopic {
         self.topic.clone()
     }
@@ -275,6 +291,15 @@ pub struct IntraNodePayload {
     message_type: MessageType,
 }
 
+impl std::fmt::Debug for IntraNodePayload {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> 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),
@@ -288,6 +313,7 @@ pub struct GossipMessage {
     pub raw_payload: Vec<u8>,
 }
 
+#[non_exhaustive]
 #[derive(Serialize, Deserialize, Debug)]
 pub enum MyBehaviourRequest {
     Handshake {
@@ -300,6 +326,7 @@ pub enum MyBehaviourRequest {
     },
 }
 
+#[non_exhaustive]
 #[derive(Serialize, Deserialize, Debug)]
 pub enum MyBehaviourResponse {
     Handshaked {
@@ -346,7 +373,7 @@ impl<Env: GadgetEnvironment> Network<Env> for GossipHandle {
                 .read()
                 .await
                 .get(&to)
-                .cloned()
+                .copied()
                 .ok_or_else(|| gadget_common::Error::NetworkError {
                     err: format!(
                         "No libp2p ID found for ecdsa public key: {to}. No handshake happened?"
@@ -386,16 +413,19 @@ impl<Env: GadgetEnvironment> Network<Env> for GossipHandle {
 #[cfg(test)]
 #[cfg(not(target_family = "wasm"))]
 mod tests {
+    use std::future::Future;
+    use std::pin::Pin;
     use std::sync::Arc;
 
     use crate::config::{KeystoreConfig, ShellConfig};
     use crate::network::gossip::GossipHandle;
-    use crate::network::setup::setup_libp2p_network;
+    use crate::network::setup::{setup_multiplexed_libp2p_network, NetworkConfig};
     use crate::shell::wait_for_connection_to_bootnodes;
     use async_trait::async_trait;
     use environment_utils::transaction_manager::tangle::TanglePalletSubmitter;
     use gadget_common::keystore::InMemoryBackend;
     use gadget_common::prelude::DebugLogger;
+    use gadget_common::Error;
     use gadget_core::job_manager::WorkManagerInterface;
     use gadget_io::tokio;
     use sp_application_crypto::sr25519;
@@ -404,8 +434,6 @@ mod tests {
     use tangle_environment::message::TangleProtocolMessage;
     use tangle_environment::work_manager::TangleWorkManager;
     use tangle_environment::TangleEnvironment;
-    use tangle_primitives::jobs::JobId;
-    use tangle_subxt::tangle_testnet_runtime::api::runtime_types::tangle_primitives::roles::RoleType;
 
     #[gadget_io::tokio::test]
     async fn test_gossip_network() {
@@ -460,35 +488,24 @@ mod tests {
                 tx_manager: Arc::new(gadget_common::prelude::Mutex::new(Some(tx_manager))),
             };
 
-            let shell_config = ShellConfig::<InMemoryBackend, TangleEnvironment> {
-                keystore: KeystoreConfig::InMemory,
-                environment: tangle_environment,
-                bind_ip: "127.0.0.1".parse().unwrap(),
-                bind_port,
-                bootnodes,
-                base_path: std::path::PathBuf::new(),
-                node_key: [0u8; 32],
-                services: vec![RoleType::LightClientRelaying], // Dummy value
-                n_protocols: 1,
-                keystore_backend: InMemoryBackend::default(),
-            };
-
             let role_key = get_dummy_role_key_from_index(x);
 
-            let (handles, _) = setup_libp2p_network::<_, TangleEnvironment>(
+            let config = NetworkConfig {
                 identity,
-                &shell_config,
-                logger.clone(),
-                networks.clone(),
                 role_key,
-            )
-            .await
-            .unwrap();
-            all_handles.push((handles, shell_config, logger));
+                bootnodes: bootnodes.clone(),
+                bind_ip,
+                bind_port,
+                topics: networks.clone(),
+                logger: logger.clone(),
+            };
+
+            let (handles, _) = setup_multiplexed_libp2p_network(config).await.unwrap();
+            all_handles.push((handles, bootnodes.clone(), logger));
         }
 
-        for (handles, shell_config, logger) in &all_handles {
-            wait_for_connection_to_bootnodes(shell_config, handles, logger)
+        for (handles, bootnodes, logger) in &all_handles {
+            wait_for_connection_to_bootnodes(bootnodes, handles, logger)
                 .await
                 .unwrap();
         }
@@ -613,20 +630,13 @@ mod tests {
 
     #[async_trait]
     impl TanglePalletSubmitter for StubPalletTx {
-        async fn submit_job_result(
+        async fn submit_job_call_result(
             &self,
-            _role_type: gadget_common::tangle_runtime::RoleType,
-            _job_id: JobId,
-            _result: gadget_common::tangle_runtime::jobs::JobResult<
-                gadget_common::tangle_runtime::MaxParticipants,
-                gadget_common::tangle_runtime::MaxKeyLen,
-                gadget_common::tangle_runtime::MaxSignatureLen,
-                gadget_common::tangle_runtime::MaxDataLen,
-                gadget_common::tangle_runtime::MaxProofLen,
-                gadget_common::tangle_runtime::MaxAdditionalParamsLen,
-            >,
-        ) -> Result<(), gadget_common::Error> {
-            unreachable!("TangetPalletSubmitter::submit_job_result should not be called with a stub implementation `()` ")
+            service_id: u64,
+            call_id: u64,
+            result: tangle_subxt::tangle_testnet_runtime::api::services::calls::types::submit_result::Result,
+        ) -> Result<(), Error> {
+            panic!("Should not be called")
         }
     }
 }
diff --git a/shell-sdk/src/network/handlers/connections.rs b/sdk/src/network/handlers/connections.rs
similarity index 97%
rename from shell-sdk/src/network/handlers/connections.rs
rename to sdk/src/network/handlers/connections.rs
index 7d6b14f29..a50afc105 100644
--- a/shell-sdk/src/network/handlers/connections.rs
+++ b/sdk/src/network/handlers/connections.rs
@@ -1,3 +1,5 @@
+#![allow(unused_results, clippy::used_underscore_binding)]
+
 use crate::network::gossip::{MyBehaviourRequest, NetworkService};
 use libp2p::PeerId;
 use sp_core::{keccak_256, Pair};
diff --git a/shell-sdk/src/network/handlers/dcutr.rs b/sdk/src/network/handlers/dcutr.rs
similarity index 100%
rename from shell-sdk/src/network/handlers/dcutr.rs
rename to sdk/src/network/handlers/dcutr.rs
diff --git a/shell-sdk/src/network/handlers/gossip.rs b/sdk/src/network/handlers/gossip.rs
similarity index 97%
rename from shell-sdk/src/network/handlers/gossip.rs
rename to sdk/src/network/handlers/gossip.rs
index db21d23a0..b34ccd292 100644
--- a/shell-sdk/src/network/handlers/gossip.rs
+++ b/sdk/src/network/handlers/gossip.rs
@@ -1,3 +1,5 @@
+#![allow(unused_results)]
+
 use crate::network::gossip::{GossipMessage, NetworkService};
 
 use libp2p::gossipsub::TopicHash;
@@ -8,7 +10,7 @@ use std::sync::Arc;
 impl NetworkService<'_> {
     #[tracing::instrument(skip(self, event))]
     pub(crate) async fn handle_gossip(&mut self, event: gossipsub::Event) {
-        use gossipsub::Event::*;
+        use gossipsub::Event::{GossipsubNotSupported, Message, Subscribed, Unsubscribed};
         let with_connected_peers = |topic: &TopicHash, f: fn(&Arc<AtomicU32>)| {
             let maybe_mapping = self
                 .inbound_mapping
diff --git a/shell-sdk/src/network/handlers/identify.rs b/sdk/src/network/handlers/identify.rs
similarity index 96%
rename from shell-sdk/src/network/handlers/identify.rs
rename to sdk/src/network/handlers/identify.rs
index 847ed25ad..22e42f4d3 100644
--- a/shell-sdk/src/network/handlers/identify.rs
+++ b/sdk/src/network/handlers/identify.rs
@@ -3,7 +3,7 @@ use crate::network::gossip::NetworkService;
 impl NetworkService<'_> {
     #[tracing::instrument(skip(self, event))]
     pub(crate) async fn handle_identify_event(&mut self, event: libp2p::identify::Event) {
-        use 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.
diff --git a/shell-sdk/src/network/handlers/kadmelia.rs b/sdk/src/network/handlers/kadmelia.rs
similarity index 100%
rename from shell-sdk/src/network/handlers/kadmelia.rs
rename to sdk/src/network/handlers/kadmelia.rs
diff --git a/shell-sdk/src/network/handlers/mdns.rs b/sdk/src/network/handlers/mdns.rs
similarity index 96%
rename from shell-sdk/src/network/handlers/mdns.rs
rename to sdk/src/network/handlers/mdns.rs
index 705097be6..022d60fd1 100644
--- a/shell-sdk/src/network/handlers/mdns.rs
+++ b/sdk/src/network/handlers/mdns.rs
@@ -4,7 +4,7 @@ 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::*;
+        use mdns::Event::{Discovered, Expired};
         match event {
             Discovered(list) => {
                 for (peer_id, multiaddr) in list {
diff --git a/shell-sdk/src/network/handlers/mod.rs b/sdk/src/network/handlers/mod.rs
similarity index 100%
rename from shell-sdk/src/network/handlers/mod.rs
rename to sdk/src/network/handlers/mod.rs
diff --git a/shell-sdk/src/network/handlers/p2p.rs b/sdk/src/network/handlers/p2p.rs
similarity index 93%
rename from shell-sdk/src/network/handlers/p2p.rs
rename to sdk/src/network/handlers/p2p.rs
index 29b1a6ce4..27dc9fd72 100644
--- a/shell-sdk/src/network/handlers/p2p.rs
+++ b/sdk/src/network/handlers/p2p.rs
@@ -1,8 +1,10 @@
+#![allow(unused_results)]
+
 use crate::network::gossip::{MyBehaviourRequest, MyBehaviourResponse, NetworkService};
 use libp2p::gossipsub::IdentTopic;
 
+use gadget_common::sp_core::{keccak_256, Pair};
 use libp2p::{request_response, PeerId};
-use sp_core::{keccak_256, Pair};
 use sp_io::crypto::ecdsa_verify_prehashed;
 
 impl NetworkService<'_> {
@@ -11,7 +13,7 @@ impl NetworkService<'_> {
         &mut self,
         event: request_response::Event<MyBehaviourRequest, MyBehaviourResponse>,
     ) {
-        use request_response::Event::*;
+        use request_response::Event::{InboundFailure, Message, OutboundFailure, ResponseSent};
         match event {
             Message { peer, message } => {
                 self.logger
@@ -46,7 +48,7 @@ impl NetworkService<'_> {
         peer: PeerId,
         message: request_response::Message<MyBehaviourRequest, MyBehaviourResponse>,
     ) {
-        use request_response::Message::*;
+        use request_response::Message::{Request, Response};
         match message {
             Request {
                 request,
@@ -75,10 +77,10 @@ impl NetworkService<'_> {
     async fn handle_p2p_response(
         &mut self,
         peer: PeerId,
-        _request_id: request_response::OutboundRequestId,
+        request_id: request_response::OutboundRequestId,
         message: MyBehaviourResponse,
     ) {
-        use crate::network::gossip::MyBehaviourResponse::*;
+        use crate::network::gossip::MyBehaviourResponse::{Handshaked, MessageHandled};
         match message {
             Handshaked {
                 ecdsa_public_key,
@@ -115,7 +117,7 @@ impl NetworkService<'_> {
         req: MyBehaviourRequest,
         channel: request_response::ResponseChannel<MyBehaviourResponse>,
     ) {
-        use crate::network::gossip::MyBehaviourRequest::*;
+        use crate::network::gossip::MyBehaviourRequest::{Handshake, Message};
         let result = match req {
             Handshake {
                 ecdsa_public_key,
diff --git a/shell-sdk/src/network/handlers/ping.rs b/sdk/src/network/handlers/ping.rs
similarity index 100%
rename from shell-sdk/src/network/handlers/ping.rs
rename to sdk/src/network/handlers/ping.rs
diff --git a/shell-sdk/src/network/handlers/relay.rs b/sdk/src/network/handlers/relay.rs
similarity index 100%
rename from shell-sdk/src/network/handlers/relay.rs
rename to sdk/src/network/handlers/relay.rs
diff --git a/shell-sdk/src/network/matchbox.rs b/sdk/src/network/matchbox.rs
similarity index 100%
rename from shell-sdk/src/network/matchbox.rs
rename to sdk/src/network/matchbox.rs
diff --git a/shell-sdk/src/network/mod.rs b/sdk/src/network/mod.rs
similarity index 100%
rename from shell-sdk/src/network/mod.rs
rename to sdk/src/network/mod.rs
diff --git a/shell-sdk/src/network/setup.rs b/sdk/src/network/setup.rs
similarity index 63%
rename from shell-sdk/src/network/setup.rs
rename to sdk/src/network/setup.rs
index d601cb3f0..e519db7da 100644
--- a/shell-sdk/src/network/setup.rs
+++ b/sdk/src/network/setup.rs
@@ -1,9 +1,8 @@
-use crate::config::ShellConfig;
+#![allow(unused_results, missing_docs)]
 #[cfg(not(target_family = "wasm"))]
 use crate::network::gossip::{
     GossipHandle, IntraNodePayload, MyBehaviour, NetworkServiceWithoutSwarm, MAX_MESSAGE_SIZE,
 };
-use crate::shell::{AGENT_VERSION, CLIENT_VERSION};
 use futures::StreamExt;
 use gadget_common::config::DebugLogger;
 
@@ -13,12 +12,11 @@ use libp2p::{
     swarm::dial_opts::DialOpts, StreamProtocol,
 };
 
-use gadget_common::environments::GadgetEnvironment;
-use gadget_common::prelude::KeystoreBackend;
+use gadget_common::sp_core::ecdsa;
 use gadget_io::tokio::select;
 use gadget_io::tokio::sync::{Mutex, RwLock};
 use gadget_io::tokio::task::{spawn, JoinHandle};
-use sp_core::ecdsa;
+use libp2p::Multiaddr;
 use std::collections::HashMap;
 use std::error::Error;
 use std::io;
@@ -28,16 +26,148 @@ use std::sync::atomic::AtomicU32;
 use std::sync::Arc;
 use std::time::Duration;
 
-#[allow(clippy::collapsible_else_if)]
+/// The version of the shell-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 role_key: ecdsa::Pair,
+    pub bootnodes: Vec<Multiaddr>,
+    pub bind_ip: IpAddr,
+    pub bind_port: u16,
+    pub topics: Vec<String>,
+    pub logger: DebugLogger,
+}
+
+impl std::fmt::Debug for NetworkConfig {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("NetworkConfig")
+            .field("identity", &self.identity)
+            .field("bootnodes", &self.bootnodes)
+            .field("bind_ip", &self.bind_ip)
+            .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,
+        role_key: ecdsa::Pair,
+        bootnodes: Vec<Multiaddr>,
+        bind_ip: IpAddr,
+        bind_port: u16,
+        topics: Vec<String>,
+        logger: DebugLogger,
+    ) -> Self {
+        Self {
+            identity,
+            role_key,
+            bootnodes,
+            bind_ip,
+            bind_port,
+            topics,
+            logger,
+        }
+    }
+
+    /// 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,
+        role_key: ecdsa::Pair,
+        bootnodes: Vec<Multiaddr>,
+        bind_ip: IpAddr,
+        bind_port: u16,
+        service_name: T,
+        logger: DebugLogger,
+    ) -> Self {
+        Self::new(
+            identity,
+            role_key,
+            bootnodes,
+            bind_ip,
+            bind_port,
+            vec![service_name.into()],
+            logger,
+        )
+    }
+}
+
+/// 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.
+/// Starts the P2P network with the given configuration.
+///
+/// # Arguments
+///
+/// * `config` - The network configuration.
+///
+/// # Errors
+///
+/// Returns an error if the network setup fails.
+pub fn start_p2p_network(config: NetworkConfig) -> Result<GossipHandle, Box<dyn Error>> {
+    if config.topics.len() != 1 {
+        return Err("Only one network topic is allowed when running this function".into());
+    }
+
+    let (networks, _) = multiplexed_libp2p_network(config)?;
+    let network = networks.into_iter().next().ok_or("No network found")?.1;
+    Ok(network)
+}
+
+pub type NetworkResult = Result<(HashMap<String, GossipHandle>, JoinHandle<()>), Box<dyn Error>>;
+
+#[allow(clippy::collapsible_else_if, clippy::too_many_lines)]
 #[cfg(not(target_family = "wasm"))]
-pub async fn setup_libp2p_network<KBE: KeystoreBackend, Env: GadgetEnvironment>(
-    identity: libp2p::identity::Keypair,
-    config: &ShellConfig<KBE, Env>,
-    logger: DebugLogger,
-    networks: Vec<String>,
-    role_key: ecdsa::Pair,
-) -> Result<(HashMap<String, GossipHandle>, JoinHandle<()>), Box<dyn Error>> {
+/// 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
+    let NetworkConfig {
+        identity,
+        bootnodes,
+        bind_ip,
+        bind_port,
+        topics,
+        logger,
+        role_key,
+    } = config;
+
+    // Ensure all topics are unique
+    let topics_unique = topics
+        .iter()
+        .cloned()
+        .collect::<std::collections::HashSet<_>>()
+        .into_iter()
+        .collect::<Vec<_>>();
+
+    if topics_unique.len() != topics.len() {
+        return Err("All topics must be unique".into());
+    }
+
+    let networks = topics;
 
     let mut swarm = libp2p::SwarmBuilder::with_existing_identity(identity)
         .with_tokio()
@@ -153,8 +283,6 @@ pub async fn setup_libp2p_network<KBE: KeystoreBackend, Env: GadgetEnvironment>(
         );
     }
 
-    let bind_ip = config.bind_ip;
-
     let mut ips_to_bind_to = vec![bind_ip];
 
     if let IpAddr::V6(v6_addr) = bind_ip {
@@ -173,12 +301,12 @@ pub async fn setup_libp2p_network<KBE: KeystoreBackend, Env: GadgetEnvironment>(
 
     for addr in ips_to_bind_to {
         let ip_label = if addr.is_ipv4() { "ip4" } else { "ip6" };
-        swarm.listen_on(format!("/{ip_label}/{addr}/udp/{}/quic-v1", config.bind_port).parse()?)?;
-        swarm.listen_on(format!("/{ip_label}/{addr}/tcp/{}", config.bind_port).parse()?)?;
+        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 &config.bootnodes {
+    for bootnode in &bootnodes {
         swarm.dial(
             DialOpts::unknown_peer_id()
                 .address(bootnode.clone())
diff --git a/shell-manager/src/gadget/native.rs b/shell-manager/src/gadget/native.rs
index f49af2e76..759640898 100644
--- a/shell-manager/src/gadget/native.rs
+++ b/shell-manager/src/gadget/native.rs
@@ -23,7 +23,7 @@ pub async fn maybe_handle(
     logger: &DebugLogger,
 ) -> color_eyre::Result<()> {
     for (gh, fetcher) in onchain_services.iter().zip(onchain_gh_fetchers) {
-        let native_wasm_metadata = NativeGithubMetadata {
+        let native_github_metadata = NativeGithubMetadata {
             git: gh.git.clone(),
             tag: gh.tag.clone(),
             owner: gh.owner.clone(),
@@ -34,7 +34,7 @@ pub async fn maybe_handle(
 
         if let Err(err) = handle_github_source(
             blueprints,
-            &native_wasm_metadata,
+            &native_github_metadata,
             shell_config,
             shell_manager_opts,
             fetcher,
diff --git a/shell-sdk/Cargo.toml b/shell-sdk/Cargo.toml
index 249777758..86a3e66a1 100644
--- a/shell-sdk/Cargo.toml
+++ b/shell-sdk/Cargo.toml
@@ -59,6 +59,7 @@ log = { version = "0.4", default-features = false }
 gadget-core = { workspace = true }
 gadget-common = { workspace = true, default-features = false, features = ["tangle-testnet"] }
 gadget-io = { workspace = true, default-features = false }
+gadget-sdk = { workspace = true, features = ["networking-libp2p"] }
 sp-core = { workspace = true, default-features = false }
 sp-io = { workspace = true, default-features = false }
 sp-keystore = { workspace = true }
@@ -86,6 +87,9 @@ env_logger = { workspace = true }
 pallet-services = { workspace = true }
 tangle-primitives = { workspace = true }
 
+[target.'cfg(target_family = "wasm")'.dev-dependencies]
+wasm-bindgen-test = "0.3.34"
+
 [target.'cfg(not(target_family = "wasm"))'.dependencies.libp2p]
 workspace = true
 features = [
@@ -112,11 +116,5 @@ features = [
 workspace = true
 default-features = false
 
-[target.'cfg(target_family = "wasm")'.dev-dependencies]
-wasm-bindgen-test = "0.3.34"
-
-[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
-tangle-test-utils = { workspace = true }
-
 [dev-dependencies]
 tangle-environment = { workspace = true }
\ No newline at end of file
diff --git a/shell-sdk/src/lib.rs b/shell-sdk/src/lib.rs
index f13964a2b..c24846ba2 100644
--- a/shell-sdk/src/lib.rs
+++ b/shell-sdk/src/lib.rs
@@ -1,13 +1,13 @@
 pub mod config;
 pub mod entry;
 pub mod keystore;
-pub mod network;
 pub mod shell;
 
 pub use config::*;
 pub use entry::run_shell_for_protocol;
 pub use gadget_common::prelude::*;
 pub use gadget_core::gadget::general::Client;
+pub use gadget_sdk::network::setup::{start_p2p_network, NetworkConfig};
 pub use shell::generate_node_input;
 
 pub mod prelude {
diff --git a/shell-sdk/src/shell.rs b/shell-sdk/src/shell.rs
index 53a8b4bd1..1ef964648 100644
--- a/shell-sdk/src/shell.rs
+++ b/shell-sdk/src/shell.rs
@@ -14,13 +14,11 @@ use gadget_io::tokio::task::JoinHandle;
 use sp_core::{ed25519, keccak_256, sr25519, Pair};
 
 use crate::config::ShellConfig;
-use crate::network::gossip::GossipHandle;
 pub use gadget_io::KeystoreContainer;
+use gadget_sdk::network::gossip::GossipHandle;
+use gadget_sdk::network::setup::NetworkConfig;
 use itertools::Itertools;
-
-/// The version of the shell-sdk
-pub const AGENT_VERSION: &str = "tangle/gadget-shell-sdk/1.0.0";
-pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
+use libp2p::Multiaddr;
 
 pub type ShellNodeInput<KBE, Env> = NodeInput<Env, GossipHandle, KBE, ()>;
 
@@ -47,18 +45,22 @@ pub async fn generate_node_input<KBE: KeystoreBackend, Env: GadgetEnvironment>(
         .sorted()
         .collect::<Vec<_>>();
 
-    let (networks, network_task) = crate::network::setup::setup_libp2p_network(
+    let libp2p_config = NetworkConfig::new(
         libp2p_key,
-        &config,
-        logger.clone(),
-        network_ids,
         role_key,
-    )
-    .await
-    .map_err(|e| color_eyre::eyre::eyre!("Failed to setup network: {e}"))?;
+        config.bootnodes.clone(),
+        config.bind_ip,
+        config.bind_port,
+        network_ids.clone(),
+        logger.clone(),
+    );
+
+    let (networks, network_task) =
+        gadget_sdk::network::setup::multiplexed_libp2p_network(libp2p_config)
+            .map_err(|e| color_eyre::eyre::eyre!("Failed to setup network: {e}"))?;
 
     logger.debug("Successfully initialized network, now waiting for bootnodes to connect ...");
-    wait_for_connection_to_bootnodes(&config, &networks, &logger).await?;
+    wait_for_connection_to_bootnodes(&config.bootnodes, &networks, &logger).await?;
 
     let node_input = generate_node_input_for_required_protocols(
         &config.environment,
@@ -130,13 +132,14 @@ where
     )
 }
 
-pub async fn wait_for_connection_to_bootnodes<KBE: KeystoreBackend, Env: GadgetEnvironment>(
-    config: &ShellConfig<KBE, Env>,
+pub async fn wait_for_connection_to_bootnodes(
+    bootnodes: &[Multiaddr],
     handles: &HashMap<String, GossipHandle>,
     logger: &DebugLogger,
 ) -> color_eyre::Result<()> {
-    let n_required = config.bootnodes.len();
+    let n_required = bootnodes.len();
     let n_networks = handles.len();
+
     logger.debug(format!(
         "Waiting for {n_required} peers to show up across {n_networks} networks"
     ));


Test patch: