\section{DHT node state}
\begin{code}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE Safe #-}
module Network.Tox.DHT.DhtState where
import Control.Applicative (Applicative, Const (..),
getConst, (<$>), (<*>), (<|>))
import Data.Functor.Identity (Identity (..))
import Data.List (nub, sortBy)
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Monoid (All (..), Monoid, getAll)
import Data.Ord (comparing)
import Data.Traversable (traverse)
import Lens.Family2 (Lens')
import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary, shrink)
import Network.Tox.Crypto.Key (PublicKey)
import Network.Tox.Crypto.KeyPair (KeyPair)
import qualified Network.Tox.Crypto.KeyPair as KeyPair
import Network.Tox.DHT.ClientList (ClientList)
import qualified Network.Tox.DHT.ClientList as ClientList
import Network.Tox.DHT.Distance (Distance)
import Network.Tox.DHT.KBuckets (KBuckets)
import qualified Network.Tox.DHT.KBuckets as KBuckets
import Network.Tox.DHT.NodeList (NodeList)
import qualified Network.Tox.DHT.NodeList as NodeList
import Network.Tox.DHT.PendingReplies (PendingReplies)
import qualified Network.Tox.DHT.Stamped as Stamped
import Network.Tox.NodeInfo.NodeInfo (NodeInfo)
import qualified Network.Tox.NodeInfo.NodeInfo as NodeInfo
import Network.Tox.Time (Timestamp)
\end{code}
Every DHT node contains the following state:
\begin{itemize}
\item DHT Key Pair: The Key Pair used to communicate with other DHT nodes. It
is immutable throughout the lifetime of the DHT node.
\item DHT Close List: A set of Node Infos of nodes that are close to the
DHT Public Key (public part of the DHT Key Pair). The Close List is
represented as a \href{#k-buckets}{k-buckets} data structure, with the DHT
Public Key as the Base Key.
\item DHT Search List: A list of Public Keys of nodes that the DHT node is
searching for, associated with a DHT Search Entry.
\end{itemize}
\begin{code}
data ListStamp = ListStamp { listTime :: Timestamp, listBootstrappedTimes :: Int }
deriving (Eq, Read, Show)
newListStamp :: Timestamp -> ListStamp
newListStamp t = ListStamp t 0
data DhtState = DhtState
{ dhtKeyPair :: KeyPair
, dhtCloseList :: KBuckets
, dhtSearchList :: Map PublicKey DhtSearchEntry
, dhtCloseListStamp :: ListStamp
, dhtPendingReplies :: PendingReplies
}
deriving (Eq, Read, Show)
_dhtKeyPair :: Lens' DhtState KeyPair
_dhtKeyPair f d@DhtState{ dhtKeyPair = a } =
(\a' -> d{ dhtKeyPair = a' }) <$> f a
_dhtCloseListStamp :: Lens' DhtState ListStamp
_dhtCloseListStamp f d@DhtState{ dhtCloseListStamp = a } =
(\a' -> d{ dhtCloseListStamp = a' }) <$> f a
_dhtCloseList :: Lens' DhtState KBuckets
_dhtCloseList f d@DhtState{ dhtCloseList = a } =
(\a' -> d{ dhtCloseList = a' }) <$> f a
_dhtSearchList :: Lens' DhtState (Map PublicKey DhtSearchEntry)
_dhtSearchList f d@DhtState{ dhtSearchList = a } =
(\a' -> d{ dhtSearchList = a' }) <$> f a
_dhtPendingReplies :: Lens' DhtState PendingReplies
_dhtPendingReplies f d@DhtState{ dhtPendingReplies = a } =
(\a' -> d{ dhtPendingReplies = a' }) <$> f a
\end{code}
A DHT node state is initialised using a Key Pair, which is stored in the state
as DHT Key Pair and as base key for the Close List. Both the Close and Search
Lists are initialised to be empty.
\begin{code}
empty :: Timestamp -> KeyPair -> DhtState
empty time keyPair =
DhtState keyPair (KBuckets.empty $ KeyPair.publicKey keyPair)
Map.empty (newListStamp time) Stamped.empty
\end{code}
\subsection{DHT Search Entry}
A DHT Search Entry contains a Client List with base key the searched node's
Public Key. Once the searched node is found, it is also stored in the Search
Entry.
The maximum size of the Client List is set to 8.
(Must be the same or smaller than the bucket size of the close list to make
sure all the closest peers found will know the node being searched
(TODO(zugz): this argument is unclear.)).
A DHT node state therefore contains one Client List for each bucket index in
the Close List, and one Client List for each DHT Search Entry.
These lists are not required to be disjoint - a node may be in multiple Client
Lists simultaneously.
\begin{code}
data DhtSearchEntry = DhtSearchEntry
{ searchNode :: Maybe NodeInfo
, searchStamp :: ListStamp
, searchClientList :: ClientList
}
deriving (Eq, Read, Show)
_searchNode :: Lens' DhtSearchEntry (Maybe NodeInfo)
_searchNode f d@DhtSearchEntry{ searchNode = a } =
(\a' -> d{ searchNode = a' }) <$> f a
_searchStamp :: Lens' DhtSearchEntry ListStamp
_searchStamp f d@DhtSearchEntry{ searchStamp = a } =
(\a' -> d{ searchStamp = a' }) <$> f a
_searchClientList :: Lens' DhtSearchEntry ClientList
_searchClientList f d@DhtSearchEntry{ searchClientList = a } =
(\a' -> d{ searchClientList = a' }) <$> f a
searchEntryClientListSize :: Int
searchEntryClientListSize = 8
\end{code}
A Search Entry is initialised with the searched-for Public Key. The contained
Client List is initialised to be empty.
\begin{code}
emptySearchEntry :: Timestamp -> PublicKey -> DhtSearchEntry
emptySearchEntry time publicKey =
DhtSearchEntry Nothing (newListStamp time) $
ClientList.empty publicKey searchEntryClientListSize
\end{code}
\subsection{Manipulating the DHT node state}
Adding a search key to the DHT node state creates an empty entry in the Search
Nodes list. If a search entry for the public key already existed, the "add"
operation has no effect.
\begin{code}
addSearchKey :: Timestamp -> PublicKey -> DhtState -> DhtState
addSearchKey time searchKey dhtState@DhtState { dhtSearchList } =
dhtState { dhtSearchList = updatedSearchList }
where
searchEntry =
Map.findWithDefault (emptySearchEntry time searchKey) searchKey dhtSearchList
updatedSearchList =
Map.insert searchKey searchEntry dhtSearchList
\end{code}
Removing a search key removes its search entry and all associated data
structures from memory.
\begin{code}
removeSearchKey :: PublicKey -> DhtState -> DhtState
removeSearchKey searchKey dhtState@DhtState { dhtSearchList } =
dhtState { dhtSearchList = Map.delete searchKey dhtSearchList }
containsSearchKey :: PublicKey -> DhtState -> Bool
containsSearchKey searchKey =
Map.member searchKey . dhtSearchList
\end{code}
\input{src/Network/Tox/DHT/NodeList.lhs}
The iteration order over the DHT state is to first process the Close List
k-buckets, then the Search List entry Client Lists. Each of these follows the
iteration order in the corresponding specification.
\begin{code}
traverseNodeLists :: Applicative f =>
(forall l. NodeList l => l -> f l) -> DhtState -> f DhtState
traverseNodeLists f dhtState@DhtState{ dhtCloseList, dhtSearchList } =
(\close' search' ->
dhtState{ dhtCloseList = close', dhtSearchList = search' }) <$>
f dhtCloseList <*>
traverse traverseEntry dhtSearchList
where
traverseEntry entry =
(\x -> entry{ searchClientList = x }) <$> f (searchClientList entry)
foldMapNodeLists :: Monoid m =>
(forall l. NodeList l => l -> m) -> DhtState -> m
foldMapNodeLists f = getConst . traverseNodeLists (Const . f)
mapNodeLists :: (forall l. NodeList l => l -> l) -> DhtState -> DhtState
mapNodeLists f = runIdentity . traverseNodeLists (Identity . f)
\end{code}
A node info is considered to be contained in the DHT State if it is contained
in the Close List or in at least one of the Search Entries.
The size of the DHT state is defined to be the number of node infos it
contains, counted with multiplicity: node infos contained multiple times, e.g.
in the close list and in various search entries, are counted as many times as
they appear. Search keys do not directly count towards the state size. So
the size of the state is the sum of the sizes of the Close List and the Search
Entries.
The state size is relevant to later pruning algorithms that decide when to
remove a node info and when to request a ping from stale nodes. Search keys,
once added, are never automatically pruned.
\begin{code}
size :: DhtState -> Int
size = NodeList.foldNodes (flip $ const (1 +)) 0
\end{code}
Adding a Node Info to the state is done by adding the node to each Node List
in the state.
When adding a node info to the state, the search entry for the node's public
key, if it exists, is updated to contain the new node info. All k-buckets and
Client Lists that already contain the node info will also be updated. See the
corresponding specifications for the update algorithms. However, a node info
will not be added to a search entry when it is the node to which the search
entry is associated (i.e. the node being search for).
\begin{code}
addNode :: Timestamp -> NodeInfo -> DhtState -> DhtState
addNode time nodeInfo =
updateSearchNode (NodeInfo.publicKey nodeInfo) (Just nodeInfo)
. mapNodeLists addUnlessBase
where
addUnlessBase nodeList
| NodeInfo.publicKey nodeInfo == NodeList.baseKey nodeList = nodeList
addUnlessBase nodeList = NodeList.addNode time nodeInfo nodeList
removeNode :: PublicKey -> DhtState -> DhtState
removeNode publicKey =
updateSearchNode publicKey Nothing
. mapNodeLists (NodeList.removeNode publicKey)
viable :: NodeInfo -> DhtState -> Bool
viable nodeInfo = getAll . foldMapNodeLists (All . NodeList.viable nodeInfo)
traverseClientLists ::
Applicative f => (ClientList -> f ClientList) -> DhtState -> f DhtState
traverseClientLists f = traverseNodeLists $ NodeList.traverseClientLists f
closeNodes :: PublicKey -> DhtState -> [ (Distance, NodeInfo) ]
closeNodes publicKey =
nub . sortBy (comparing fst) . foldMapNodeLists (NodeList.closeNodes publicKey)
instance NodeList DhtState where
addNode = addNode
removeNode = removeNode
viable = viable
baseKey = KeyPair.publicKey . dhtKeyPair
traverseClientLists = traverseClientLists
closeNodes = closeNodes
takeClosestNodesTo :: Int -> PublicKey -> DhtState -> [ NodeInfo ]
takeClosestNodesTo n publicKey = map snd . take n . closeNodes publicKey
mapBuckets :: (KBuckets -> KBuckets) -> DhtState -> DhtState
mapBuckets f dhtState@DhtState { dhtCloseList } =
dhtState
{ dhtCloseList = f dhtCloseList
}
mapSearchEntry :: (DhtSearchEntry -> DhtSearchEntry) -> DhtState -> DhtState
mapSearchEntry f dhtState@DhtState { dhtSearchList } =
dhtState
{ dhtSearchList = Map.map f dhtSearchList
}
mapSearchClientLists :: (ClientList -> ClientList) -> DhtState -> DhtState
mapSearchClientLists f =
mapSearchEntry $ \entry@DhtSearchEntry{ searchClientList } ->
entry { searchClientList = f searchClientList }
updateSearchNode :: PublicKey -> Maybe NodeInfo -> DhtState -> DhtState
updateSearchNode publicKey nodeInfo dhtState@DhtState { dhtSearchList } =
dhtState
{ dhtSearchList = Map.adjust update publicKey dhtSearchList
}
where
update entry = entry { searchNode = nodeInfo }
\end{code}
Removing a node info from the state removes it from all k-buckets. If a search
entry for the removed node's public key existed, the node info in that search
entry is unset. The search entry itself is not removed.
\begin{code}
containsNode :: PublicKey -> DhtState -> Bool
containsNode publicKey =
NodeList.foldNodes (\a x -> a || NodeInfo.publicKey x == publicKey) False
instance Arbitrary DhtState where
arbitrary =
initialise <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
where
initialise :: Timestamp -> KeyPair -> [(Timestamp, NodeInfo)] -> [(Timestamp, PublicKey)] -> DhtState
initialise time kp nis =
foldl (flip $ uncurry addSearchKey) (foldl (flip $ uncurry NodeList.addNode) (empty time kp) nis)
shrink dhtState =
Maybe.maybeToList shrunkNode ++ Maybe.maybeToList shrunkSearchKey
where
shrunkNode = do
firstPK <- NodeInfo.publicKey <$> NodeList.foldNodes (\a x -> a <|> Just x) Nothing dhtState
return $ NodeList.removeNode firstPK dhtState
shrunkSearchKey = Nothing
\end{code}