/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::collections::{HashMap, HashSet}; use async_std::sync::{Arc, RwLock}; use chrono::Utc; use darkfi_serial::serialize; use futures::{select, FutureExt}; use log::{debug, error, warn}; use rand::Rng; use smol::Executor; use crate::{ net, net::P2pPtr, util::async_util::sleep, Error::{NetworkNotConnected, UnknownKey}, Result, }; mod messages; use messages::{KeyRequest, KeyResponse, LookupMapRequest, LookupMapResponse, LookupRequest}; mod protocol; use protocol::Protocol; // Constants configuration const SEEN_DURATION: i64 = 120; /// Atomic pointer to DHT state pub type DhtPtr = Arc>; // TODO: proper errors // TODO: lookup table to be based on directly connected peers, not broadcast based // Using string in structures because we are at an external crate // and cant use blake3 serialization. To be replaced once merged with core src. /// Struct representing DHT state. pub struct Dht { /// Daemon id pub id: blake3::Hash, /// Daemon hasmap pub map: HashMap>, /// Network lookup map, containing nodes that holds each key pub lookup: HashMap>, /// P2P network pointer pub p2p: P2pPtr, /// Channel to receive responses from P2P p2p_recv_channel: smol::channel::Receiver, /// Stop signal channel to terminate background processes stop_signal: smol::channel::Receiver<()>, /// Daemon seen requests/responses ids and timestamp, /// to prevent rebroadcasting and loops pub seen: HashMap, } impl Dht { pub async fn new( initial: Option>>, p2p_ptr: P2pPtr, stop_signal: smol::channel::Receiver<()>, ex: Arc>, ) -> Result { // Generate a random id let mut rng = rand::thread_rng(); let n: u16 = rng.gen(); let id = blake3::hash(&serialize(&n)); let map = HashMap::default(); let lookup = match initial { Some(l) => l, None => HashMap::default(), }; let p2p = p2p_ptr.clone(); let (p2p_send_channel, p2p_recv_channel) = smol::channel::unbounded::(); let seen = HashMap::default(); let dht = Arc::new(RwLock::new(Dht { id, map, lookup, p2p, p2p_recv_channel, stop_signal, seen, })); // Registering P2P protocols let registry = p2p_ptr.protocol_registry(); let _dht = dht.clone(); registry .register(net::SESSION_ALL, move |channel, p2p_ptr| { let sender = p2p_send_channel.clone(); let dht = _dht.clone(); async move { Protocol::init(channel, sender, dht, p2p_ptr).await.unwrap() } }) .await; // Task to periodically clean up daemon seen messages ex.spawn(prune_seen_messages(dht.clone())).detach(); Ok(dht) } /// Store provided key value pair, update lookup map and broadcast new insert to network pub async fn insert( &mut self, key: blake3::Hash, value: Vec, ) -> Result> { self.map.insert(key, value); if let Err(e) = self.lookup_insert(key, self.id) { error!(target: "dht", "Failed to insert record to lookup map: {}", e); return Err(e) }; let request = LookupRequest::new(self.id, key, 0); if let Err(e) = self.p2p.broadcast(request).await { error!(target: "dht", "Failed broadcasting request: {}", e); return Err(e) } Ok(Some(key)) } /// Remove provided key value pair and update lookup map pub async fn remove(&mut self, key: blake3::Hash) -> Result> { // Check if key value pair existed and act accordingly match self.map.remove(&key) { Some(_) => { debug!(target: "dht", "Key removed: {}", key); let request = LookupRequest::new(self.id, key, 1); if let Err(e) = self.p2p.broadcast(request).await { error!(target: "dht", "Failed broadcasting request: {}", e); return Err(e) } self.lookup_remove(key, self.id) } None => Ok(None), } } /// Store provided key node pair in lookup map and update network pub fn lookup_insert( &mut self, key: blake3::Hash, node_id: blake3::Hash, ) -> Result> { let mut lookup_set = match self.lookup.get(&key) { Some(s) => s.clone(), None => HashSet::new(), }; lookup_set.insert(node_id); self.lookup.insert(key, lookup_set); Ok(Some(key)) } /// Remove provided node id from keys set in local lookup map pub fn lookup_remove( &mut self, key: blake3::Hash, node_id: blake3::Hash, ) -> Result> { if let Some(s) = self.lookup.get(&key) { let mut lookup_set = s.clone(); lookup_set.remove(&node_id); if lookup_set.is_empty() { self.lookup.remove(&key); } else { self.lookup.insert(key, lookup_set); } } Ok(Some(key)) } /// Verify if provided key exists and return flag if local or in network pub fn contains_key(&self, key: blake3::Hash) -> Option { match self.lookup.contains_key(&key) { true => Some(self.map.contains_key(&key)), false => None, } } /// Get key from local map, acting as daemon cache pub fn get(&self, key: blake3::Hash) -> Option<&Vec> { self.map.get(&key) } /// Generate key request and broadcast it to the network pub async fn request_key(&self, key: blake3::Hash) -> Result<()> { // Verify the key exist in the lookup map. let peers = match self.lookup.get(&key) { Some(v) => v.clone(), None => return Err(UnknownKey), }; debug!(target: "dht", "Key is in peers: {:?}", peers); // We retrieve p2p network connected channels, to verify if we // are connected to a network. // Using len here because is_empty() uses unstable library feature // called 'exact_size_is_empty'. if self.p2p.channels().lock().await.values().len() == 0 { return Err(NetworkNotConnected) } // We create a key request, and broadcast it to the network // We choose last known peer as request recipient let peer = *peers.iter().last().unwrap(); let request = KeyRequest::new(self.id, peer, key); // TODO: ask connected peers directly, not broadcast if let Err(e) = self.p2p.broadcast(request).await { error!(target: "dht", "Failed broadcasting request: {}", e); return Err(e) } Ok(()) } /// Auxilary function to sync lookup map with network pub async fn sync_lookup_map(&mut self) -> Result<()> { debug!(target: "dht", "Starting lookup map sync..."); let channels_map = self.p2p.channels().lock().await.clone(); let values = channels_map.values(); // Using len here because is_empty() uses unstable library feature // called 'exact_size_is_empty'. if values.len() != 0 { // Node iterates the channel peers to ask for their lookup map for channel in values { // Communication setup let msg_subsystem = channel.get_message_subsystem(); msg_subsystem.add_dispatch::().await; let response_sub = channel.subscribe_msg::().await?; // Node creates a `LookupMapRequest` and sends it let order = LookupMapRequest::new(self.id); channel.send(order).await?; // Node stores response data. let resp = response_sub.receive().await?; if resp.lookup.is_empty() { warn!(target: "dht", "Retrieved empty lookup map from an unsynced node, retrying..."); continue } // Store retrieved records debug!(target: "dht", "Processing received records"); for (k, v) in &resp.lookup { for node in v { self.lookup_insert(*k, *node)?; } } break } } else { warn!(target: "dht", "Node is not connected to other nodes"); } debug!(target: "dht", "Lookup map synced!"); Ok(()) } } // Auxilary function to wait for a key response from the P2P network. pub async fn waiting_for_response(dht: DhtPtr) -> Result> { let (p2p_recv_channel, stop_signal, timeout) = { let _dht = dht.read().await; ( _dht.p2p_recv_channel.clone(), _dht.stop_signal.clone(), _dht.p2p.settings().connect_timeout_seconds as u64, ) }; let ex = Arc::new(Executor::new()); let (timeout_s, timeout_r) = smol::channel::unbounded::<()>(); ex.spawn(async move { sleep(timeout).await; timeout_s.send(()).await.unwrap_or(()); }) .detach(); select! { msg = p2p_recv_channel.recv().fuse() => { let response = msg?; return Ok(Some(response)) }, _ = stop_signal.recv().fuse() => {}, _ = timeout_r.recv().fuse() => {}, } Ok(None) } // Auxilary function to periodically prun seen messages, based on when they were received. // This helps us to prevent broadcasting loops. async fn prune_seen_messages(dht: DhtPtr) { loop { sleep(SEEN_DURATION as u64).await; debug!(target: "dht", "Pruning seen messages"); let now = Utc::now().timestamp(); let mut prune = vec![]; let map = dht.read().await.seen.clone(); for (k, v) in map.iter() { if now - v > SEEN_DURATION { prune.push(k); } } let mut map = map.clone(); for i in prune { map.remove(i); } dht.write().await.seen = map; } }