فهرست منبع

script/research/fud: File-sharing Utility Daemon init commit

aggstam 4 سال پیش
والد
کامیت
13982b9662

+ 109 - 0
script/research/fud/README.md

@@ -0,0 +1,109 @@
+fud
+=======
+
+File-sharing Utility Daemon, using DHT for records discovery.
+
+## Usage
+
+```
+fud 0.3.0
+File-sharing Utility Daemon, using DHT for records discovery.
+
+USAGE:
+    fud [FLAGS] [OPTIONS]
+
+FLAGS:
+    -h, --help       Prints help information
+    -V, --version    Prints version information
+    -v               Increase verbosity (-vvv supported)
+
+OPTIONS:
+    -c, --config <config>                Configuration file to use
+        --folder <folder>                Path to the contents directory [default: ~/.config/darkfi/fud]
+        --p2p-accept <p2p-accept>        P2P accept address
+        --p2p-external <p2p-external>    P2P external address
+        --p2p-peer <p2p-peer>...         Connect to peer (repeatable flag)
+        --p2p-seed <p2p-seed>...         Connect to seed (repeatable flag)
+        --rpc-listen <rpc-listen>        JSON-RPC listen URL [default: tcp://127.0.0.1:9540]
+        --slots <slots>                  Connection slots [default: 8]
+```
+
+On first execution, daemon will create default config file ~/.config/darkfi/fud_config.toml.
+Configuration must be verified and application should be configured accordingly.
+Additionaly, default content folder will be created at ~/.config/darkfi/fud.
+
+Run fud as follows:
+
+```
+% fud
+13:23:04 [INFO] Starting JSON-RPC server
+13:23:04 [INFO] Starting sync P2P network
+13:23:04 [WARN] Skipping seed sync process since no seeds are configured.
+13:23:04 [INFO] Initializing fud dht state for folder: "/home/x/.config/darkfi/fud"
+13:23:04 [INFO] Not configured for accepting incoming connections.
+13:23:04 [INFO] JSON-RPC listener bound to tcp://127.0.0.1:9540
+13:23:04 [INFO] Entry: seedd_config.toml
+13:23:04 [INFO] Starting 8 outbound connection slots.
+13:23:04 [INFO] Entry: lt.py
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #0
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #1
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #2
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #3
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #6
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #4
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #5
+13:23:04 [WARN] Hosts address pool is empty. Retrying connect slot #7
+13:23:07 [INFO] Caught termination signal, cleaning up and exiting...
+```
+
+fu
+=======
+
+Command-line client for fud.
+
+## Usage
+
+```
+fu 0.3.0
+Daemon that spawns P2P seeds
+
+USAGE:
+    fu [OPTIONS] <SUBCOMMAND>
+
+OPTIONS:
+    -e, --endpoint <ENDPOINT>    fud JSON-RPC endpoint [default: tcp://127.0.0.1:9540]
+    -h, --help                   Print help information
+    -v                           Increase verbosity (-vvv supported)
+    -V, --version                Print version information
+
+SUBCOMMANDS:
+    get     Retrieve provided file name from the fud network
+    help    Print this message or the help of the given subcommand(s)
+    list    List fud folder contents
+    sync    Sync fud folder contents and signal network for record changes
+```
+
+Execution examples:
+
+```
+% fu list
+13:25:14 [INFO] ----------Content-------------
+13:25:14 [INFO] 	seedd_config.toml
+13:25:14 [INFO] 	lt.py
+13:25:14 [INFO] ------------------------------
+13:25:14 [INFO] ----------New files-----------
+13:25:14 [INFO] No new files to import.
+13:25:14 [INFO] ------------------------------
+13:25:14 [INFO] ----------Removed keys--------
+13:25:14 [INFO] No keys were removed.
+13:25:14 [INFO] ------------------------------
+
+$ fu sync
+13:25:46 [INFO] Daemon synced successfully!
+
+$ fu get -f lt.py
+13:26:23 [INFO] File waits you at: /home/x/.config/darkfi/fud/lt.py
+
+$ fu get -f sdsd
+Error: JsonRpcError("\"Did not find key\"")
+```

+ 2 - 0
script/research/fud/fu/.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 20 - 0
script/research/fud/fu/Cargo.toml

@@ -0,0 +1,20 @@
+[package]
+name = "fu"
+version = "0.3.0"
+homepage = "https://dark.fi"
+description = "Command-line client for fud"
+authors = ["darkfi <dev@dark.fi>"]
+repository = "https://github.com/darkrenaissance/darkfi"
+license = "AGPL-3.0-only"
+edition = "2021"
+
+[dependencies]
+async-std = {version = "1.12.0", features = ["attributes"]}
+clap = {version = "3.2.16", features = ["derive"]}
+darkfi = {path = "../../../../", features = ["util"]}
+log = "0.4.17"
+serde_json = "1.0.83"
+simplelog = "0.12.0"
+url = "2.2.2"
+
+[workspace]

+ 132 - 0
script/research/fud/fu/src/main.rs

@@ -0,0 +1,132 @@
+use clap::{Parser, Subcommand};
+use log::info;
+use serde_json::json;
+use simplelog::{ColorChoice, TermLogger, TerminalMode};
+use url::Url;
+
+use darkfi::{
+    cli_desc,
+    rpc::{client::RpcClient, jsonrpc::JsonRequest},
+    util::cli::{get_log_config, get_log_level},
+    Result,
+};
+
+#[derive(Parser)]
+#[clap(name = "fu", about = cli_desc!(), version)]
+#[clap(arg_required_else_help(true))]
+struct Args {
+    #[clap(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
+
+    #[clap(short, long, default_value = "tcp://127.0.0.1:9540")]
+    /// fud JSON-RPC endpoint
+    endpoint: Url,
+
+    #[clap(subcommand)]
+    command: Subcmd,
+}
+
+#[derive(Subcommand)]
+enum Subcmd {
+    /// List fud folder contents
+    List,
+
+    /// Sync fud folder contents and signal network for record changes
+    Sync,
+
+    /// Retrieve provided file name from the fud network
+    Get {
+        #[clap(short, long)]
+        /// File name
+        file: String,
+    },
+}
+
+struct Fu {
+    pub rpc_client: RpcClient,
+}
+
+impl Fu {
+    async fn close_connection(&self) -> Result<()> {
+        self.rpc_client.close().await
+    }
+
+    async fn list(&self) -> Result<()> {
+        let req = JsonRequest::new("list", json!([]));
+        let rep = self.rpc_client.request(req).await?;
+
+        // Extract response
+        let content = rep[0].as_array().unwrap();
+        let new = rep[1].as_array().unwrap();
+        let deleted = rep[2].as_array().unwrap();
+
+        // Print info
+        info!("----------Content-------------");
+        if content.is_empty() {
+            info!("No file records exists in DHT.");
+        } else {
+            for name in content {
+                info!("\t{}", name.as_str().unwrap());
+            }
+        }
+        info!("------------------------------");
+
+        info!("----------New files-----------");
+        if new.is_empty() {
+            info!("No new files to import.");
+        } else {
+            for name in new {
+                info!("\t{}", name.as_str().unwrap());
+            }
+        }
+        info!("------------------------------");
+
+        info!("----------Removed keys--------");
+        if deleted.is_empty() {
+            info!("No keys were removed.");
+        } else {
+            for key in deleted {
+                info!("\t{}", key.as_str().unwrap());
+            }
+        }
+        info!("------------------------------");
+
+        Ok(())
+    }
+
+    async fn sync(&self) -> Result<()> {
+        let req = JsonRequest::new("sync", json!([]));
+        self.rpc_client.request(req).await?;
+        info!("Daemon synced successfully!");
+        Ok(())
+    }
+
+    async fn get(&self, file: String) -> Result<()> {
+        let req = JsonRequest::new("get", json!([file]));
+        let rep = self.rpc_client.request(req).await?;
+        let path = rep.as_str().unwrap();
+        info!("File waits you at: {}", path);
+        Ok(())
+    }
+}
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let args = Args::parse();
+
+    let log_level = get_log_level(args.verbose.into());
+    let log_config = get_log_config();
+    TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
+
+    let rpc_client = RpcClient::new(args.endpoint).await?;
+    let fu = Fu { rpc_client };
+
+    match args.command {
+        Subcmd::List => fu.list().await,
+        Subcmd::Sync => fu.sync().await,
+        Subcmd::Get { file } => fu.get(file).await,
+    }?;
+
+    fu.close_connection().await
+}

+ 2 - 0
script/research/fud/fud/.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 32 - 0
script/research/fud/fud/Cargo.toml

@@ -0,0 +1,32 @@
+[package]
+name = "fud"
+version = "0.3.0"
+homepage = "https://dark.fi"
+description = "File-sharing Utility Daemon, using DHT for records discovery."
+authors = ["darkfi <dev@dark.fi>"]
+repository = "https://github.com/darkrenaissance/darkfi"
+license = "AGPL-3.0-only"
+edition = "2021"
+
+[dependencies]
+async-channel = "1.6.1"
+async-executor = "1.4.1"
+async-std = "1.12.0"
+async-trait = "0.1.57"
+blake3 = "1.3.1"
+ctrlc-async = {version = "3.2.2", default-features = false, features = ["async-std", "termination"]}
+darkfi = {path = "../../../../", features = ["dht"]}
+easy-parallel = "3.2.0"
+futures-lite = "1.12.0"
+log = "0.4.17"
+serde_json = "1.0.83"
+simplelog = "0.12.0"
+url = "2.2.2"
+
+# Argument parsing
+serde = "1.0.142"
+serde_derive = "1.0.142"
+structopt = "0.3.26"
+structopt-toml = "0.5.1"
+
+[workspace]

+ 28 - 0
script/research/fud/fud/fud_config.toml

@@ -0,0 +1,28 @@
+## fud configuration file
+##
+## Please make sure you go through all the settings so you can configure
+## your daemon properly.
+##
+## The default values are left commented. They can be overridden either by
+## uncommenting, or by using the command-line.
+
+# Path to the contents directory
+#folder = "~/.config/darkfi/fud"
+
+# JSON-RPC listen URL
+#rpc_listen = "tcp://127.0.0.1:9540"
+
+# P2P accept address
+#p2p_accept = "tls://127.0.0.1:9541"
+
+# P2P external address
+#p2p_external = "tls://127.0.0.1:9541"
+
+# Connection slots
+#slots = 8
+
+# Seed nodes to connect to
+#seed = []
+
+# Peers to connect to
+#peer = []

+ 30 - 0
script/research/fud/fud/src/error.rs

@@ -0,0 +1,30 @@
+use serde_json::Value;
+
+use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult};
+
+pub enum RpcError {
+    UnknownKey = -35107,
+    QueryFailed = -35108,
+    KeyInsertFail = -35110,
+    KeyRemoveFail = -35111,
+    WaitingNetworkError = -35112,
+    FileGenerationFail = -35113,
+}
+
+fn to_tuple(e: RpcError) -> (i64, String) {
+    let msg = match e {
+        RpcError::UnknownKey => "Did not find key",
+        RpcError::QueryFailed => "Failed to query key",
+        RpcError::KeyInsertFail => "Failed to insert key",
+        RpcError::KeyRemoveFail => "Failed to remove key",
+        RpcError::WaitingNetworkError => "Error while waiting network response.",
+        RpcError::FileGenerationFail => "Failed to generate file for key",
+    };
+
+    (e as i64, msg.to_string())
+}
+
+pub fn server_error(e: RpcError, id: Value) -> JsonResult {
+    let (code, msg) = to_tuple(e);
+    JsonError::new(ServerError(code), Some(msg), id).into()
+}

+ 394 - 0
script/research/fud/fud/src/main.rs

@@ -0,0 +1,394 @@
+use async_executor::Executor;
+use async_std::sync::Arc;
+use async_trait::async_trait;
+use futures_lite::future;
+use log::{debug, error, info, warn};
+use serde_derive::Deserialize;
+use serde_json::{json, Value};
+use std::{collections::HashSet, fs, path::PathBuf};
+use structopt::StructOpt;
+use structopt_toml::StructOptToml;
+use url::Url;
+
+use darkfi::{
+    async_daemonize, cli_desc,
+    dht::{waiting_for_response, Dht, DhtPtr},
+    net,
+    rpc::{
+        jsonrpc::{
+            ErrorCode::{InvalidParams, MethodNotFound},
+            JsonError, JsonRequest, JsonResponse, JsonResult,
+        },
+        server::{listen_and_serve, RequestHandler},
+    },
+    util::{
+        cli::{get_log_config, get_log_level, spawn_config},
+        expand_path,
+        path::get_config_path,
+        serial::serialize,
+    },
+    Result,
+};
+
+mod error;
+use error::{server_error, RpcError};
+const CONFIG_FILE: &str = "fud_config.toml";
+const CONFIG_FILE_CONTENTS: &str = include_str!("../fud_config.toml");
+
+#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
+#[serde(default)]
+#[structopt(name = "fud", about = cli_desc!())]
+struct Args {
+    #[structopt(short, long)]
+    /// Configuration file to use
+    config: Option<String>,
+
+    #[structopt(long, default_value = "~/.config/darkfi/fud")]
+    /// Path to the contents directory
+    folder: String,
+
+    #[structopt(long, default_value = "tcp://127.0.0.1:9540")]
+    /// JSON-RPC listen URL
+    rpc_listen: Url,
+
+    #[structopt(long)]
+    /// P2P accept address
+    p2p_accept: Option<Url>,
+
+    #[structopt(long)]
+    /// P2P external address
+    p2p_external: Option<Url>,
+
+    #[structopt(long, default_value = "8")]
+    /// Connection slots
+    slots: u32,
+
+    #[structopt(long)]
+    /// Connect to seed (repeatable flag)
+    p2p_seed: Vec<Url>,
+
+    #[structopt(long)]
+    /// Connect to peer (repeatable flag)
+    p2p_peer: Vec<Url>,
+
+    #[structopt(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
+}
+
+/// Struct representing the daemon.
+pub struct Fud {
+    /// Daemon dht state
+    dht: DhtPtr,
+
+    /// Path to the contents directory
+    folder: PathBuf,
+}
+
+impl Fud {
+    pub async fn new(dht: DhtPtr, folder: PathBuf) -> Result<Self> {
+        Ok(Self { dht, folder })
+    }
+
+    /// Initialize fud dht state by reading the contents folder and generating
+    /// the corresponding dht records.
+    async fn init(&self) -> Result<()> {
+        info!("Initializing fud dht state for folder: {:?}", self.folder);
+
+        if !self.folder.exists() {
+            fs::create_dir_all(&self.folder)?;
+        }
+
+        let entries = fs::read_dir(&self.folder).unwrap();
+        {
+            let mut lock = self.dht.write().await;
+            for entry in entries {
+                let e = entry.unwrap();
+                let name = String::from(e.file_name().to_str().unwrap());
+                info!("Entry: {}", name);
+                let key_hash = blake3::hash(&serialize(&name));
+                let value: Vec<u8> = std::fs::read(e.path()).unwrap();
+                if let Err(e) = lock.insert(key_hash, value).await {
+                    error!("Failed to insert key: {}", e);
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Signaling fud network that node goes offline.
+    async fn disconnect(&self) -> Result<()> {
+        debug!("Peer disconnecting, signaling network");
+
+        {
+            let mut lock = self.dht.write().await;
+            let records = lock.map.clone();
+            for key in records.keys() {
+                let result = lock.remove(*key).await;
+                match result {
+                    Ok(option) => match option {
+                        Some(k) => {
+                            debug!("Hash key removed: {}", k);
+                        }
+                        None => {
+                            warn!("Did not find key: {}", key);
+                        }
+                    },
+                    Err(e) => {
+                        error!("Failed to remove key: {}", e);
+                    }
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    // RPCAPI:
+    // Returns all folder contents, with file changes.
+    // --> {"jsonrpc": "2.0", "method": "list", "params": [], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": "[[files],[new],[deleted]", "id": 1}
+    pub async fn list(&self, id: Value, _params: &[Value]) -> JsonResult {
+        let mut content = HashSet::new();
+        let mut new = HashSet::new();
+        let mut deleted = HashSet::new();
+
+        let entries = fs::read_dir(&self.folder).unwrap();
+        let records = self.dht.read().await.map.clone();
+        let mut entries_hashes = HashSet::new();
+
+        // We iterate files for new records
+        for entry in entries {
+            let e = entry.unwrap();
+            let name = String::from(e.file_name().to_str().unwrap());
+            let key_hash = blake3::hash(&serialize(&name));
+            entries_hashes.insert(key_hash);
+
+            if records.contains_key(&key_hash) {
+                content.insert(name.clone());
+            } else {
+                new.insert(name);
+            }
+        }
+
+        // We check records for removed files
+        for key in records.keys() {
+            if entries_hashes.contains(key) {
+                continue
+            }
+            deleted.insert(key.to_string());
+        }
+
+        JsonResponse::new(json!((content, new, deleted)), id).into()
+    }
+
+    // RPCAPI:
+    // Iterate contents folder and dht for potential changes.
+    // --> {"jsonrpc": "2.0", "method": "sync", "params": [], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": "true", "id": 1}
+    pub async fn sync(&self, id: Value, _params: &[Value]) -> JsonResult {
+        info!("Sync process started");
+
+        let entries = fs::read_dir(&self.folder).unwrap();
+        {
+            let mut lock = self.dht.write().await;
+            let records = lock.map.clone();
+            let mut entries_hashes = HashSet::new();
+
+            // We iterate files for new records
+            for entry in entries {
+                let e = entry.unwrap();
+                let name = String::from(e.file_name().to_str().unwrap());
+                info!("Entry: {}", name);
+                let key_hash = blake3::hash(&serialize(&name));
+                entries_hashes.insert(key_hash);
+
+                if records.contains_key(&key_hash) {
+                    continue
+                }
+
+                let value: Vec<u8> = std::fs::read(e.path()).unwrap();
+                if let Err(e) = lock.insert(key_hash, value).await {
+                    error!("Failed to insert key: {}", e);
+                    return server_error(RpcError::KeyInsertFail, id)
+                }
+            }
+
+            // We check records for removed files
+            let records = lock.map.clone();
+            for key in records.keys() {
+                if entries_hashes.contains(key) {
+                    continue
+                }
+
+                let result = lock.remove(*key).await;
+                match result {
+                    Ok(option) => match option {
+                        Some(k) => {
+                            debug!("Hash key removed: {}", k);
+                        }
+                        None => {
+                            warn!("Did not find key: {}", key);
+                        }
+                    },
+                    Err(e) => {
+                        error!("Failed to remove key: {}", e);
+                        return server_error(RpcError::KeyRemoveFail, id)
+                    }
+                }
+            }
+        }
+
+        JsonResponse::new(json!(true), id).into()
+    }
+
+    // RPCAPI:
+    // Checks if provided key exists and retrieve it from the local map or queries the network.
+    // Returns key or not found message.
+    // --> {"jsonrpc": "2.0", "method": "get", "params": ["name"], "id": 1}
+    // <-- {"jsonrpc": "2.0", "result": "path", "id": 1}
+    async fn get(&self, id: Value, params: &[Value]) -> JsonResult {
+        if params.len() != 1 || !params[0].is_string() {
+            return JsonError::new(InvalidParams, None, id).into()
+        }
+
+        let key = params[0].as_str().unwrap().to_string();
+        let key_hash = blake3::hash(&serialize(&key));
+
+        // We execute this sequence to prevent lock races between threads
+        // Verify key exists
+        let exists = self.dht.read().await.contains_key(key_hash.clone());
+        if let None = exists {
+            info!("Did not find key: {}", key);
+            return server_error(RpcError::UnknownKey, id).into()
+        }
+
+        // Check if key is local or should query network
+        let path = self.folder.join(key.clone());
+        let local = exists.unwrap();
+        if local {
+            match self.dht.read().await.get(key_hash.clone()) {
+                Some(_) => return JsonResponse::new(json!(path), id).into(),
+                None => {
+                    info!("Did not find key: {}", key);
+                    return server_error(RpcError::UnknownKey, id).into()
+                }
+            }
+        }
+
+        info!("Key doesn't exist locally, querring network...");
+        if let Err(e) = self.dht.read().await.request_key(key_hash).await {
+            error!("Failed to query key: {}", e);
+            return server_error(RpcError::QueryFailed, id).into()
+        }
+
+        info!("Waiting response...");
+        match waiting_for_response(self.dht.clone()).await {
+            Ok(response) => {
+                match response {
+                    Some(resp) => {
+                        info!("Key found!");
+                        // Optionally, we insert the key to our local map
+                        if let Err(e) =
+                            self.dht.write().await.insert(resp.key, resp.value.clone()).await
+                        {
+                            error!("Failed to insert key: {}", e);
+                            return server_error(RpcError::KeyInsertFail, id)
+                        }
+
+                        if let Err(e) = std::fs::write(path.clone(), resp.value) {
+                            error!("Failed to generate file for key: {}", e);
+                            return server_error(RpcError::FileGenerationFail, id)
+                        }
+                        JsonResponse::new(json!(path), id).into()
+                    }
+                    None => {
+                        info!("Did not find key: {}", key);
+                        server_error(RpcError::UnknownKey, id).into()
+                    }
+                }
+            }
+            Err(e) => {
+                error!("Error while waiting network response: {}", e);
+                server_error(RpcError::WaitingNetworkError, id).into()
+            }
+        }
+    }
+}
+
+#[async_trait]
+impl RequestHandler for Fud {
+    async fn handle_request(&self, req: JsonRequest) -> JsonResult {
+        if !req.params.is_array() {
+            return JsonError::new(InvalidParams, None, req.id).into()
+        }
+
+        let params = req.params.as_array().unwrap();
+
+        match req.method.as_str() {
+            Some("list") => return self.list(req.id, params).await,
+            Some("sync") => return self.sync(req.id, params).await,
+            Some("get") => return self.get(req.id, params).await,
+            Some(_) | None => return JsonError::new(MethodNotFound, None, req.id).into(),
+        }
+    }
+}
+
+async_daemonize!(realmain);
+async fn realmain(args: Args, ex: Arc<Executor<'_>>) -> Result<()> {
+    // We use this handler to block this function after detaching all
+    // tasks, and to catch a shutdown signal, where we can clean up and
+    // exit gracefully.
+    let (signal, shutdown) = async_channel::bounded::<()>(1);
+    ctrlc_async::set_async_handler(async move {
+        signal.send(()).await.unwrap();
+    })
+    .unwrap();
+
+    // P2P network
+    let network_settings = net::Settings {
+        inbound: args.p2p_accept,
+        outbound_connections: args.slots,
+        external_addr: args.p2p_external,
+        peers: args.p2p_seed.clone(),
+        seeds: args.p2p_seed.clone(),
+        ..Default::default()
+    };
+
+    let p2p = net::P2p::new(network_settings).await;
+
+    // Initialize daemon dht
+    let dht = Dht::new(None, p2p.clone(), shutdown.clone(), ex.clone()).await?;
+
+    // Initialize daemon
+    let folder = expand_path(&args.folder)?;
+    let fud = Fud::new(dht.clone(), folder).await?;
+    let fud = Arc::new(fud);
+
+    // JSON-RPC server
+    info!("Starting JSON-RPC server");
+    ex.spawn(listen_and_serve(args.rpc_listen, fud.clone())).detach();
+
+    info!("Starting sync P2P network");
+    p2p.clone().start(ex.clone()).await?;
+    let _ex = ex.clone();
+    let _p2p = p2p.clone();
+    ex.spawn(async move {
+        if let Err(e) = _p2p.run(_ex).await {
+            error!("Failed starting P2P network: {}", e);
+        }
+    })
+    .detach();
+
+    fud.init().await?;
+
+    // Wait for SIGINT
+    shutdown.recv().await?;
+    print!("\r");
+    info!("Caught termination signal, cleaning up and exiting...");
+
+    fud.disconnect().await?;
+
+    Ok(())
+}