packages feed

kademlia 1.0.0.0 → 1.1.0.0

raw patch · 18 files changed

+1295/−296 lines, 18 filesdep +HUnitdep +QuickCheckdep +tastyPVP ok

version bump matches the API change (PVP)

Dependencies added: HUnit, QuickCheck, tasty, tasty-hunit, tasty-quickcheck

API changes (from Hackage documentation)

+ Network.Kademlia: IDClash :: JoinResult
+ Network.Kademlia: JoinSucces :: JoinResult
+ Network.Kademlia: Node :: Peer -> i -> Node i
+ Network.Kademlia: NodeDown :: JoinResult
+ Network.Kademlia: Peer :: String -> PortNumber -> Peer
+ Network.Kademlia: data JoinResult
+ Network.Kademlia: data Node i
+ Network.Kademlia: data Peer
+ Network.Kademlia: dumpPeers :: KademliaInstance i a -> IO [Node i]
+ Network.Kademlia: lookupNode :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> i -> IO (Maybe (Node i))
+ Network.Kademlia: nodeId :: Node i -> i
+ Network.Kademlia: peer :: Node i -> Peer
+ Network.Kademlia: peerHost :: Peer -> String
+ Network.Kademlia: peerPort :: Peer -> PortNumber
- Network.Kademlia: joinNetwork :: (Serialize i, Ord i, Eq i, Serialize a) => KademliaInstance i a -> (String, Int, i) -> IO ()
+ Network.Kademlia: joinNetwork :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> Node i -> IO JoinResult
- Network.Kademlia: lookup :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> i -> IO (Maybe a)
+ Network.Kademlia: lookup :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> i -> IO (Maybe (a, Node i))

Files

kademlia.cabal view
@@ -1,5 +1,5 @@ name:                kademlia-version:             1.0.0.0+version:             1.1.0.0 homepage:            https://github.com/froozen/kademlia bug-reports:         https://github.com/froozen/kademlia/issues synopsis:            An implementation of the Kademlia DHT Protocol@@ -39,10 +39,10 @@                        Network.Kademlia.ReplyQueue,                        Network.Kademlia.Implementation -  build-depends:       base >= 4 && < 5,+  build-depends:       base >= 4.7 && < 5,                        network >=2.6 && <2.7,                        mtl >=2.1.3.1,-                       bytestring >=0.10 && <0.11,+                       bytestring >=0.10.2 && <0.11,                        transformers >=0.3,                        containers >=0.5.5.1,                        stm >=2.4.3,@@ -50,3 +50,30 @@    hs-source-dirs:      src   default-language:    Haskell2010++test-suite library-test+  type:                exitcode-stdio-1.0+  main-is:             Test.hs+  hs-source-dirs:      test, src+  other-modules:       Protocol, Networking, TestTypes, Types, Tree, Instance,+                       ReplyQueue, Implementation,+                       Network.Kademlia.Networking, Network.Kademlia.Types,+                       Network.Kademlia.Protocol, Network.Kademlia.Instance,+                       Network.Kademlia.Protocol.Parsing, Network.Kademlia.Tree,+                       Network.Kademlia.ReplyQueue,+                       Network.Kademlia.Implementation++  default-language:    Haskell2010+  build-depends:       base >= 4.7 && < 5,+                       network >=2.6 && <2.7,+                       mtl >=2.1.3.1,+                       bytestring >=0.10.2 && <0.11,+                       transformers >=0.3,+                       containers >=0.5.5.1,+                       stm >=2.4.3,+                       transformers-compat >=0.3.3,+                       tasty >= 0.10.1,+                       tasty-quickcheck >= 0.8.3.1,+                       QuickCheck >= 2.4,+                       tasty-hunit >= 0.9.0.1,+                       HUnit >= 1.2.5.2
src/Network/Kademlia.hs view
@@ -71,18 +71,26 @@ >    -- network >    firstInstance <- K.create 12345 . KademliaID . C.pack $ "hello" >+>    -- Create a Node representing the first instance+>    let firstNode = Node (Peer "localhost" 12345) . KademliaID . C.pack $ "hello"+> >    -- Create the second instance and make it join the network >    secondInstance <- K.create 12346 . KademliaID . C.pack $ "uAleu"->    K.joinNetwork secondInstance ("localhost", 12345, "hello")+>    joinResult <- K.joinNetwork secondInstance firstNode >->    -- Store an example value in the network->    let exampleValue = Person 25 "Alan Turing"->    K.store secondInstance (KademliaID . C.pack $ "raxqT") exampleValue+>    -- Make sure the joining was successful+>    case joinResult of+>         JoinSuccess -> do+>             -- Store an example value in the network+>             let exampleValue = Person 25 "Alan Turing"+>             K.store secondInstance (KademliaID . C.pack $ "raxqT") exampleValue >->    -- Look up the value->    result <- K.lookup firstInstance . KademliaID . C.pack $ "raxqT"->    print result+>             -- Look up the value and it's source+>             (value, source) <- K.lookup firstInstance . KademliaID . C.pack $ "raxqT"+>             print value >+>         _ -> return ()+> >    -- Close the instances >    K.close firstInstance >    K.close secondInstance@@ -109,8 +117,13 @@     , close     , I.lookup     , I.store-    , Network.Kademlia.joinNetwork+    , I.lookupNode+    , I.joinNetwork+    , dumpPeers+    , JoinResult(..)     , Serialize(..)+    , Node(..)+    , Peer(..)     ) where  import Network.Kademlia.Networking@@ -128,17 +141,11 @@ create :: (Serialize i, Ord i, Serialize a, Eq a, Eq i) =>     Int -> i -> IO (KademliaInstance i a) create port id = do-    h <- openOn (show port) id+    rq <- emptyReplyQueue+    h <- openOn (show port) id rq     inst <- newInstance id h-    start inst+    start inst rq     return inst---- | Make a KademliaInstance join the network the supplied Node is a part of-joinNetwork :: (Serialize i, Ord i, Eq i, Serialize a) => KademliaInstance i a-     -> (String, Int, i) -> IO ()-joinNetwork inst (host, port, i) = let peer = Peer host . fromIntegral $ port-                                       node = Node peer i-                                   in  I.joinNetwork inst node  -- | Stop a KademliaInstance by closing it close :: KademliaInstance i a -> IO ()
src/Network/Kademlia/Implementation.hs view
@@ -10,6 +10,8 @@     ( lookup     , store     , joinNetwork+    , JoinResult(..)+    , Network.Kademlia.Implementation.lookupNode     ) where  import Network.Kademlia.Networking@@ -27,9 +29,10 @@ import Data.Maybe (isJust, fromJust)  --- Lookup the value corresponding to a key in the DHT+-- | Lookup the value corresponding to a key in the DHT and return it, together+--   with the Node that was the first to answer the lookup lookup :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> i-       -> IO (Maybe a)+       -> IO (Maybe (a, Node i)) lookup inst id = runLookup go inst id     where go = startLookup sendS cancel checkSignal @@ -57,7 +60,7 @@                     liftIO . send (handle inst) cachePeer . STORE id $ value                  -- Return the value-                return . Just $ value+                return . Just $ (value, origin)            -- When receiving a RETURN_NODES command, throw the nodes into the           -- lookup loop and continue the lookup@@ -82,7 +85,7 @@                 finish           finishCheck _ = finish --- Store assign a value to a key and store it in the DHT+-- | Store assign a value to a key and store it in the DHT store :: (Serialize i, Serialize a, Eq i, Ord i) =>          KademliaInstance i a -> i -> a -> IO () store inst key val = runLookup go inst key@@ -106,38 +109,76 @@              unless (null polled) $ do                 let h = handle inst-                    -- Select the peer closest to the key-                    storePeer = peer . head . sortByDistanceTo polled $ key-                -- Send it a STORE command-                liftIO . send h storePeer . STORE key $ val+                    -- Don't select more than 7 peers+                    peerNum = if length polled > 7 then 7 else length polled+                    -- Select the peers closest to the key+                    storePeers =+                        map peer . take peerNum . sortByDistanceTo polled $ key +                -- Send them a STORE command+                forM_ storePeers $+                    \storePeer -> liftIO . send h storePeer . STORE key $ val++-- | The different possibel results of joinNetwork+data JoinResult = JoinSucces | NodeDown | IDClash deriving (Eq, Ord, Show)+ -- | Make a KademliaInstance join the network a supplied Node is in joinNetwork :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a-            -> Node i -> IO ()+            -> Node i -> IO JoinResult joinNetwork inst node = ownId >>= runLookup go inst     where go = do             -- Poll the supplied node             sendS node             -- Run a normal lookup from thereon out-            waitForReply cancel checkSignal+            waitForReply nodeDown checkSignal -          -- Do nothing upon failure or when the join operation has terminated-          cancel = return ()+          -- No answer to the first signal means, that that Node is down+          nodeDown = return NodeDown            -- Retrieve your own id           ownId =             fmap T.extractId . atomically . readTVar .  sTree . state $ inst -          -- Always add the nodes into the loop and continue the lookup-          checkSignal (Signal _ (RETURN_NODES _ nodes)) =-            continueLookup nodes sendS continue cancel+          -- Check wether the own id was encountered. If so, return a IDClash+          -- error, otherwise, continue the lookup.+          checkSignal (Signal _ (RETURN_NODES _ nodes)) = do+                tId <- gets targetId+                case find (\node -> nodeId node == tId) nodes of+                    Just _ -> return IDClash+                    _ -> continueLookup nodes sendS continue finish            -- Continuing always means waiting for the next signal-          continue = waitForReply cancel checkSignal+          continue = waitForReply finish checkSignal            -- Send a FIND_NODE command, looking up your own id           sendS node = liftIO ownId >>= flip sendSignal node . FIND_NODE +          -- Return a success, when the operation finished cleanly+          finish = return JoinSucces++-- | Lookup the Node corresponding to the supplied ID+lookupNode :: (Serialize i, Serialize a, Eq i, Ord i) => KademliaInstance i a -> i+           -> IO (Maybe (Node i))+lookupNode inst id = runLookup go inst id+    where go = startLookup sendS end checkSignal++          -- Return Nothing on lookup failure+          end = return Nothing++          -- Check wether the Node we are looking for was found. If so, return+          -- it, otherwise continue the lookup.+          checkSignal (Signal _ (RETURN_NODES _ nodes)) =+                case find (\(Node _ nId) -> nId == id) nodes of+                    Just node -> return . Just $ node+                    _ -> continueLookup nodes sendS continue end++          -- Continuing always means waiting for the next signal+          continue = waitForReply end checkSignal++          -- Send a FIND_NODE command looking for the Node corresponding to the+          -- id+          sendS = sendSignal (FIND_NODE id)+ -- | The state of a lookup data LookupState i a = LookupState {       inst :: KademliaInstance i a@@ -217,7 +258,6 @@             let node = fromJust . find (\n -> nodeId n == id) $ polled              -- Remove every trace of the node's existance-            liftIO . deleteNode inst $ id             modify $ \s -> s {                   pending = delete node sPending                 , known = delete node known
src/Network/Kademlia/Instance.hs view
@@ -13,8 +13,8 @@     , start     , newInstance     , insertNode-    , deleteNode     , lookupNode+    , dumpPeers     ) where  import Control.Concurrent@@ -22,13 +22,15 @@ import Control.Concurrent.STM import Control.Monad (void, forever, when, join, forM_, forever) import Control.Monad.Trans-import Control.Monad.Trans.State+import Control.Monad.Trans.State hiding (state) import Control.Monad.Trans.Reader import Control.Monad.IO.Class (liftIO)+import Control.Applicative ((<$>), (<*>)) import System.IO.Error (catchIOError) import qualified Data.Map as M-import Data.Maybe (catMaybes)+import Data.Maybe (catMaybes, isJust, fromJust) import Data.Function (on)+import Data.Map (toList)  import Network.Kademlia.Networking import qualified Network.Kademlia.Tree as T@@ -39,6 +41,7 @@ data KademliaInstance i a = KI {       handle :: KademliaHandle i a     , state :: KademliaState i a+    , expirationThreads :: TVar (M.Map i ThreadId)     }  -- | Representation of the data the KademliaProcess carries@@ -53,43 +56,130 @@ newInstance id handle = do     tree <- atomically . newTVar . T.create $ id     values <- atomically . newTVar $ M.empty-    return . KI handle . KS tree $ values+    threads <- atomically . newTVar $ M.empty+    return . KI handle (KS tree values) $ threads +-- | Insert a Node into the NodeTree insertNode :: (Serialize i, Ord i) => KademliaInstance i a -> Node i -> IO ()-insertNode (KI _ (KS sTree _)) node = atomically $ do+insertNode (KI _ (KS sTree _) _) node = atomically $ do     tree <- readTVar sTree     writeTVar sTree . T.insert tree $ node -deleteNode :: (Serialize i, Ord i) => KademliaInstance i a -> i -> IO ()-deleteNode (KI _ (KS sTree _)) id = atomically $ do+-- | Signal a Node's timeout and retur wether it should be repinged+timeoutNode :: (Serialize i, Ord i) => KademliaInstance i a -> i -> IO Bool+timeoutNode (KI _ (KS sTree _) _) id = atomically $ do     tree <- readTVar sTree-    writeTVar sTree . T.delete tree $ id+    let (newTree, pingAgain) = T.handleTimeout tree id+    writeTVar sTree newTree+    return pingAgain +-- | Lookup a Node in the NodeTree lookupNode :: (Serialize i, Ord i) => KademliaInstance i a -> i -> IO (Maybe (Node i))-lookupNode (KI _ (KS sTree _)) id = atomically $ do+lookupNode (KI _ (KS sTree _) _) id = atomically $ do     tree <- readTVar sTree     return . T.lookup tree $ id +-- | Return all the Nodes an Instance has encountered so far+dumpPeers :: KademliaInstance i a -> IO [Node i]+dumpPeers (KI _ (KS sTree _) _) = atomically $ do+    tree <- readTVar sTree+    return . T.toList $ tree++-- | Insert a value into the store insertValue :: (Ord i) => i -> a -> KademliaInstance i a -> IO ()-insertValue key value (KI _ (KS _ values)) = atomically $ do+insertValue key value (KI _ (KS _ values) _) = atomically $ do     vals <- readTVar values     writeTVar values $ M.insert key value vals +-- | Delete a value from the store+deleteValue :: (Ord i) => i -> KademliaInstance i a -> IO ()+deleteValue key (KI _ (KS _ values) _) = atomically $ do+    vals <- readTVar values+    writeTVar values $ M.delete key vals++-- | Lookup a value in the store lookupValue :: (Ord i) => i -> KademliaInstance i a -> IO (Maybe a)-lookupValue key (KI _ (KS _ values)) = atomically $ do+lookupValue key (KI _ (KS _ values) _) = atomically $ do     vals <- readTVar values     return . M.lookup key $ vals  -- | Start the background process for a KademliaInstance start :: (Serialize i, Ord i, Serialize a, Eq i, Eq a) =>-    KademliaInstance i a -> IO ()-start inst = do-        chan <- newChan-        startRecvProcess (handle inst) chan-        pingId <- forkIO . pingProcess inst $ chan+         KademliaInstance i a -> ReplyQueue i a -> IO ()+start inst rq = do+        startRecvProcess . handle $ inst+        let rChan = timeoutChan rq+            dChan = defaultChan rq+        receivingId <- forkIO . receivingProcess inst rq rChan $ dChan+        pingId <- forkIO . pingProcess inst $ dChan         spreadId <- forkIO . spreadValueProcess $ inst-        void . forkIO $ backgroundProcess inst chan [pingId, spreadId]+        void . forkIO $ backgroundProcess inst dChan [pingId, spreadId, receivingId] +-- | The central process all Replys go trough+receivingProcess :: (Serialize i, Serialize a, Eq i, Ord i) =>+       KademliaInstance i a -> ReplyQueue i a -> Chan (Reply i a)+    -> Chan (Reply i a)-> IO ()+receivingProcess inst rq replyChan registerChan = forever $ do+    reply <- readChan replyChan++    case reply of+        -- Handle a timed out node+        Timeout registration -> do+            let origin = replyOrigin registration+                h = handle inst+                newRegistration = registration { replyTypes = [R_PONG] }++            -- Mark the node as timed out+            pingAgain <- timeoutNode inst origin+            -- If the node should be repinged+            when pingAgain $ do+                result <- lookupNode inst origin+                case result of+                    Nothing -> return ()+                    Just node -> do+                        -- Ping the node+                        send h (peer node) PING+                        expect h newRegistration registerChan++        -- Store values in newly encountered nodes that you are the closest to+        Answer (Signal node cmd) -> do+            let originId = nodeId node+            tree <- retrieve sTree++            -- This node is not yet known+            when (not . isJust . T.lookup tree $ originId) $ do+                let closestKnown = T.findClosest tree originId 1+                    ownId = T.extractId tree+                    self = node { nodeId = ownId }+                    bucket = self:closestKnown+                    -- Find out closest known node+                    closestId = nodeId . head . sortByDistanceTo bucket $ originId++                -- This node can be assumed to be closest to the new node+                when (ownId == closestId) $ do+                    storedValues <- toList <$> retrieve values+                    let h = handle inst+                        p = peer node+                    -- Store all stored values in the new node+                    forM_ storedValues (send h p . uncurry STORE)++            case cmd of+                -- Ping unknown Nodes that were returned by RETURN_NODES.+                -- Pinging them first is neccessary to prevent disconnected+                -- nodes from spreading through the networks NodeTrees.+                (RETURN_NODES _ nodes) -> forM_ nodes $ \node -> do+                    result <- lookupNode inst . nodeId $ node+                    case result of+                        Nothing -> send (handle inst) (peer node) PING+                        _ -> return ()+                _ -> return ()++        _ -> return ()++    dispatch reply rq+    where retrieve f = atomically . readTVar . f . state $ inst++ -- | The actual process running in the background backgroundProcess :: (Serialize i, Ord i, Serialize a, Eq i, Eq a) =>     KademliaInstance i a -> Chan (Reply i a) -> [ThreadId] -> IO ()@@ -109,39 +199,41 @@              backgroundProcess inst chan threadIds -        -- Delete timed out nodes-        Timeout registration -> deleteNode inst . replyOrigin $ registration+        -- Kill all other processes and stop on Closed+        Closed -> do+            mapM_ killThread threadIds -        -- Kill pingProcess and stop on Closed-        Closed -> mapM_ killThread threadIds+            eThreads <- atomically . readTVar . expirationThreads $ inst+            mapM_ killThread $ map snd (M.toList eThreads) +        _ -> return ()+ -- | Ping all known nodes every five minutes to make sure they are still present pingProcess :: (Serialize i, Serialize a, Eq i) => KademliaInstance i a             -> Chan (Reply i a) -> IO ()-pingProcess (KI h (KS sTree _)) chan = forever $ do+pingProcess (KI h (KS sTree _) _) chan = forever $ do     threadDelay fiveMinutes      tree <- atomically . readTVar $ sTree-    forM_ (allNodes tree) $ \node -> do+    forM_ (T.toList tree) $ \node -> do         -- Send PING and expect a PONG         send h (peer node) PING         expect h (RR [R_PONG] (nodeId node)) $ chan      where fiveMinutes = 300000000-          allNodes = join . catMaybes . map snd --- | Store all values stored in the node in the 7 closest known nodes every day+-- | Store all values stored in the node in the 7 closest known nodes every hour spreadValueProcess :: (Serialize i, Serialize a, Eq i) => KademliaInstance i a                    -> IO ()-spreadValueProcess (KI h (KS sTree sValues)) = forever $ do-    threadDelay day+spreadValueProcess (KI h (KS sTree sValues) _) = forever $ do+    threadDelay hour      values <- atomically . readTVar $ sValues     tree <- atomically . readTVar $ sTree      mapMWithKey (sendRequests tree) $ values -    where day = 24 * 60 * 60 * 1000000+    where hour = 60 * 60 * 1000000           sendRequests tree key val = do             let closest = T.findClosest tree key 7             forM_ closest $ \node -> send h (peer node) (STORE key val)@@ -149,6 +241,24 @@           mapMWithKey :: (k -> v -> IO a) -> M.Map k v -> IO [a]           mapMWithKey f m = sequence . map snd . M.toList . M.mapWithKey f $ m +-- | Delete a value after a certain amount of time has passed+expirationProcess :: (Ord i) => KademliaInstance i a -> i -> IO ()+expirationProcess inst@(KI _ _ valueTs) key = do+    -- Map own ThreadId to the key+    myTId <- myThreadId+    oldTId <- atomically $ do+        threadIds <- readTVar valueTs+        writeTVar valueTs $ M.insert key myTId threadIds+        return . M.lookup key $ threadIds++    -- Kill the old timeout thread, if it exists+    when (isJust oldTId) (killThread . fromJust $ oldTId)++    threadDelay hour+    deleteValue key inst++    where hour = 60 * 60 * 1000000+ -- | Handles the differendt Kademlia Commands appropriately handleCommand :: (Serialize i, Eq i, Ord i, Serialize a) =>     Command i a -> Peer -> KademliaInstance i a -> IO ()@@ -156,8 +266,10 @@ handleCommand PING peer inst = send (handle inst) peer PONG -- Return a KBucket with the closest Nodes handleCommand (FIND_NODE id) peer inst = returnNodes peer id inst--- Insert the value into the values Map-handleCommand (STORE key value) _ inst = insertValue key value inst+-- Insert the value into the values store and start the expiration process+handleCommand (STORE key value) _ inst = do+    insertValue key value inst+    void . forkIO . expirationProcess inst $ key -- Return the value, if known, or the closest other known Nodes handleCommand (FIND_VALUE key) peer inst = do     result <- lookupValue key inst@@ -169,7 +281,7 @@ -- | Return a KBucket with the closest Nodes to a supplied Id returnNodes :: (Serialize i, Eq i, Ord i, Serialize a) =>     Peer -> i -> KademliaInstance i a -> IO ()-returnNodes peer id (KI h (KS sTree _)) = do+returnNodes peer id (KI h (KS sTree _) _) = do     tree <- atomically . readTVar $ sTree     let nodes = T.findClosest tree id 7     liftIO $ send h peer (RETURN_NODES id nodes)
src/Network/Kademlia/Networking.hs view
@@ -41,8 +41,9 @@  -- | Open a Kademlia connection on specified port and return a corresponding --   KademliaHandle-openOn :: (Serialize i, Serialize a) => String -> i -> IO (KademliaHandle i a)-openOn port id = withSocketsDo $ do+openOn :: (Serialize i, Serialize a) => String -> i -> ReplyQueue i a+       -> IO (KademliaHandle i a)+openOn port id rq = withSocketsDo $ do     -- Get addr to bind to     (serveraddr:_) <- getAddrInfo                  (Just (defaultHints {addrFlags = [AI_PASSIVE]}))@@ -54,7 +55,6 @@      chan <- newChan     tId <- forkIO . sendProcess sock id $ chan-    rq <- emptyReplyQueue     mvar <- newEmptyMVar      -- Return the handle@@ -82,8 +82,8 @@ -- --   This throws an exception if called a second time. startRecvProcess :: (Serialize i, Serialize a, Eq i, Eq a) => KademliaHandle i a-                 -> Chan (Reply i a) -> IO ()-startRecvProcess kh defaultChan = do+                 -> IO ()+startRecvProcess kh = do     tId <- forkIO $ (withSocketsDo . forever $ do         -- Read from socket         (received, addr) <- S.recvFrom (kSock kh) 1500@@ -95,18 +95,14 @@                 -- Try parsing the signal                 case parse p received of                     Left _    -> return ()-                    Right sig -> do-                        -- Try to dispatch the signal-                        success <- dispatch sig $ replyQueue kh--                        unless success $-                            -- Send it to the default channel-                            writeChan defaultChan $ Answer sig)+                    Right sig ->+                        -- Send the signal to the receivng process of instance+                        writeChan (timeoutChan . replyQueue $ kh) $ Answer sig)              -- Send Closed reply to all handlers             `finally` do                 flush . replyQueue $ kh-                writeChan defaultChan  Closed+                writeChan (timeoutChan . replyQueue $ kh) Closed      success <- tryPutMVar (recvThread kh) tId     unless success . ioError . userError $ "Receiving process already running"@@ -125,13 +121,13 @@ -- | Close the connection corresponding to a KademliaHandle closeK :: KademliaHandle i a -> IO () closeK kh = do-    -- Kill sendThread-    killThread . sendThread $ kh-     -- Kill recvThread     empty <- isEmptyMVar . recvThread $ kh     unless empty $ do         tId <- takeMVar . recvThread $ kh         killThread tId++    -- Kill sendThread+    killThread . sendThread $ kh      yield
src/Network/Kademlia/Protocol/Parsing.hs view
@@ -115,7 +115,7 @@     return $ Node peer id  -- | Parses a trailing k-bucket-parseKBucket :: (Serialize i) => Parse (KBucket i)+parseKBucket :: (Serialize i) => Parse [Node i] parseKBucket = liftM2 (:) parseNode parseKBucket                    `catchE` \_ -> return [] 
src/Network/Kademlia/ReplyQueue.hs view
@@ -12,7 +12,7 @@     ( ReplyType(..)     , ReplyRegistration(..)     , Reply(..)-    , ReplyQueue+    , ReplyQueue(..)     , emptyReplyQueue     , register     , dispatch@@ -22,7 +22,7 @@ import Control.Concurrent import Control.Concurrent.STM import Control.Concurrent.Chan-import Control.Monad (liftM, forM_)+import Control.Monad (liftM3, forM_) import Control.Monad.Trans.Maybe import Data.List (find, delete) @@ -36,20 +36,22 @@ data ReplyType i = R_PONG                  | R_RETURN_VALUE i                  | R_RETURN_NODES i-                   deriving (Eq)+                   deriving (Eq, Show)  -- | The representation of registered replies data ReplyRegistration i = RR {       replyTypes  :: [ReplyType i]     , replyOrigin :: i-    } deriving (Eq)+    } deriving (Eq, Show)  -- | Convert a Signal into its ReplyRegistration representation-toRegistration :: Signal i a -> Maybe (ReplyRegistration i)-toRegistration sig = case rType . command $ sig of-                        Nothing -> Nothing-                        Just rt -> Just (RR [rt] origin)-    where origin = nodeId . source $ sig+toRegistration :: Reply i a -> Maybe (ReplyRegistration i)+toRegistration Closed        = Nothing+toRegistration (Timeout reg) = Just reg+toRegistration (Answer sig)  = case rType . command $ sig of+            Nothing -> Nothing+            Just rt -> Just (RR [rt] (origin sig))+    where origin sig = nodeId . source $ sig            rType :: Command i a -> Maybe (ReplyType i)           rType  PONG               = Just  R_PONG@@ -66,77 +68,77 @@ data Reply i a = Answer (Signal i a)                | Timeout (ReplyRegistration i)                | Closed-                 deriving (Eq)+                 deriving (Eq, Show)  -- | The actual type representing a ReplyQueue-newtype ReplyQueue i a = RQ (TVar [(ReplyRegistration i, Chan (Reply i a), ThreadId)])+data ReplyQueue i a = RQ {+      queue :: (TVar [(ReplyRegistration i, Chan (Reply i a), ThreadId)])+    , timeoutChan :: Chan (Reply i a)+    , defaultChan :: Chan (Reply i a)+    } --- | Create an empty ReplyQueue+-- | Create a new ReplyQueue emptyReplyQueue :: IO (ReplyQueue i a)-emptyReplyQueue = atomically . liftM RQ $ newTVar []+emptyReplyQueue = liftM3 RQ (atomically . newTVar $ []) newChan $ newChan  -- | Register a channel as handler for a reply register :: (Eq i) => ReplyRegistration i -> ReplyQueue i a -> Chan (Reply i a)          -> IO ()-register reg (RQ rq) chan = do-    tId <- timeoutThread chan reg (RQ rq)+register reg rq chan = do+    tId <- timeoutThread reg rq     atomically $ do-        queue <- readTVar $ rq-        writeTVar rq $ queue ++ [(reg, chan, tId)]+        rQueue <- readTVar . queue $ rq+        writeTVar (queue rq) $ rQueue ++ [(reg, chan, tId)] -timeoutThread :: (Eq i) => Chan (Reply i a) -> ReplyRegistration i-              -> ReplyQueue i a -> IO ThreadId-timeoutThread chan reg (RQ rq) = forkIO $ do+timeoutThread :: (Eq i) => ReplyRegistration i -> ReplyQueue i a -> IO ThreadId+timeoutThread reg rq = forkIO $ do     -- Wait 5 seconds     threadDelay 5000000      -- Remove the ReplyRegistration from the ReplyQueue     myTId <- myThreadId-    atomically $ do-        queue <- readTVar $ rq-        case find (\(_, _, tId) -> tId == myTId) queue of-            Just rqElem -> writeTVar rq $ delete rqElem queue-            _ -> return ()      -- Send Timeout signal-    writeChan chan . Timeout $ reg+    writeChan (timeoutChan rq) . Timeout $ reg --- | Try to send a received Signal over the registered handler channel and---   return wether it succeeded-dispatch :: (Eq i) => Signal i a -> ReplyQueue i a -> IO Bool-dispatch sig (RQ rq) = do+-- | Dispatch a reply over a registered handler. If there is no handler,+--   dispatch it to the default one.+dispatch :: (Eq i) => Reply i a -> ReplyQueue i a -> IO ()+dispatch reply rq = do+    -- Try to find a registration matching the reply     result <- atomically $ do-        queue <- readTVar $ rq-        case toRegistration sig of-            Just regA -> case find (matches regA) queue of-                Just reg -> do+        rQueue <- readTVar . queue $ rq+        case toRegistration reply of+            Just repReg -> case find (matches repReg) rQueue of+                Just registration -> do                     -- Remove registration from queue-                    writeTVar rq $ delete reg queue-                    return . Just $ reg+                    writeTVar (queue rq) $ delete registration rQueue+                    return . Just $ registration                  Nothing -> return Nothing-            Nothing  -> return Nothing+            Nothing -> return Nothing      case result of         Just (_, chan, tId) -> do             -- Kill the timeout thread             killThread tId -            -- Send the signal-            writeChan chan $ Answer sig-            return True-        _ -> return False+            -- Send the reply+            writeChan chan reply +        -- Send the reply over the default channel+        Nothing -> writeChan (defaultChan rq) reply+     where matches regA (regB, _, _) = matchRegistrations regA regB  -- | Send Closed signal to all handlers and empty ReplyQueue flush :: ReplyQueue i a -> IO ()-flush (RQ rq) = do-    queue <- atomically $ do-        queue <- readTVar $ rq-        writeTVar rq $ []-        return queue+flush rq = do+    rQueue <- atomically $ do+        rQueue <- readTVar . queue $ rq+        writeTVar (queue rq) []+        return rQueue -    forM_ queue $ \(_, chan, tId) -> do+    forM_ rQueue $ \(_, chan, tId) -> do         killThread tId         writeChan chan Closed
src/Network/Kademlia/Tree.hs view
@@ -14,189 +14,218 @@     , insert     , lookup     , delete-    , refresh+    , handleTimeout     , findClosest     , extractId+    , toList+    , fold     ) where +import Prelude hiding (lookup) import Network.Kademlia.Types-import qualified Data.List as L (delete, find)-import Prelude hiding (lookup, split)-import Control.Monad (liftM)-import Control.Arrow (first, second)-import Data.Function (on)---- | Type used for building the Node Storage Tree-type NodeTree i = [(Bool, Maybe (KBucket i))]+import qualified Data.List as L (find, delete) --- | Structure used for easier modification of the NodeTree-type Zipper i = (NodeTree i, NodeTree i)+data NodeTree i = NodeTree ByteStruct (NodeTreeElem i) --- | Move the Zipper along an Id-seek :: (Serialize i) => NodeTree i -> i -> Zipper i-seek tree id = go tree $ toByteStruct id-    where go [] _ = ([], [])-          go (pair@(bit, bucket):rest) (b:bs)-            | ends rest = ([], pair:rest)-            | bit == b  = first (pair:) $ go rest bs-            | otherwise = ([], pair:rest)+data NodeTreeElem i = Split (NodeTreeElem i) (NodeTreeElem i)+                    | Bucket ([(Node i, Int)], [Node i]) --- | Cheks wether a NodeTree ends-ends :: NodeTree i -> Bool-ends ((_, Just _):_)  = False-ends ((_, Nothing):_) = True+type NodeTreeFunction i a = Int -> Bool -> ([(Node i, Int)], [Node i]) -> a --- | Apply a function to the KBucket a Node with a given Id would be in-applyTo :: (Serialize i, Eq i) =>-           (KBucket i -> a) -- ^ Function to apply at matched position-        -> a                -- ^ Default value-        -> NodeTree i       -- ^ NodeTree to apply to-        -> i                -- ^ Position to apply at-        -> a-applyTo f end tree id = case seek tree id of-        (_, [])                 -> end-        (_, (_, Nothing):_)     -> end-        (_, (_, Just bucket):_) -> f bucket+-- | Modify the position in the tree where the supplied id would be+modifyAt :: (Serialize i) =>+            NodeTree i -> i -> NodeTreeFunction i (NodeTreeElem i)+         -> NodeTree i+modifyAt (NodeTree idStruct elem) id f =+    let targetStruct = toByteStruct id+        newElems     = go idStruct targetStruct 0 True elem+    in  NodeTree idStruct newElems+    where -- This function is partial, but we know that there will alwasys be a+          -- bucket at the end. Therefore, we don't have to check for empty+          -- ByteStructs+          --+          -- Apply the function to the position of the bucket+          go _ _ depth valid (Bucket b) = f depth valid b+          -- If the bit is a 0, go left+          go (i:is) (False:ts) depth valid (Split left right) =+               let new = go is ts (depth + 1) (valid && not i) left+               in  Split new right+          -- Otherwise, continue to the right+          go (i:is) (True:ts) depth valid (Split left right) =+               let new = go is ts (depth + 1) (valid && i) right+               in  Split left new --- | Modify a NodeTree at the position a Node with a given Id would have-modifyTreeAt :: (Serialize i, Eq i) =>-                ((Bool, Maybe (KBucket i)) -> (Bool, Maybe (KBucket i)))-                -- ^ Function to apply to corresponding TreeNode-             -> NodeTree i -- ^ NodeTree to modify-             -> i          -- ^ Position to modify at-             -> NodeTree i-modifyTreeAt f tree id = case seek tree id of-        (beg, [])       -> beg-        (beg, pair:end) -> beg ++ f pair : end+-- | Modify and apply a function at the position in the tree where the+--   supplied id would be+bothAt :: (Serialize i) =>+            NodeTree i -> i -> NodeTreeFunction i (NodeTreeElem i, a)+         -> (NodeTree i, a)+bothAt (NodeTree idStruct elem) id f =+    let targetStruct    = toByteStruct id+        (newElems, val) = go idStruct targetStruct 0 True elem+    in  (NodeTree idStruct newElems, val)+    where -- This function is partial, but we know that there will alwasys be a+          -- bucket at the end. Therefore, we don't have to check for empty+          -- ByteStructs+          --+          -- Apply the function to the position of the bucket+          go _ _ depth valid (Bucket b) = f depth valid b+          -- If the bit is a 0, go left+          go (i:is) (False:ts) depth valid (Split left right) =+               let (new, val) = go is ts (depth + 1) (valid && not i) left+               in  (Split new right, val)+          -- Otherwise, continue to the right+          go (i:is) (True:ts) depth valid (Split left right) =+               let (new, val) = go is ts (depth + 1) (valid && i) right+               in  (Split left new, val) --- | Modify the KBucket a node of a given Id would be in-modifyKBucket :: (Serialize i, Eq i) =>-                 (KBucket i -> KBucket i) -- ^ Modification funciton-              -> NodeTree i -- ^ Node tree to modify-              -> i          -- ^ Postition to modify at-              -> NodeTree i-modifyKBucket f = modifyTreeAt (second . fmap $ f)+-- | Apply a function to the bucket the supplied id would be located in+applyAt :: (Serialize i) => NodeTree i -> i -> NodeTreeFunction i a -> a+applyAt (NodeTree idStruct elem) id f =+    let targetStruct = toByteStruct id+    in go idStruct targetStruct 0 True elem+    where -- This function is partial for the same reason as in modifyAt+          --+          -- Apply the function+          go _ _ depth valid (Bucket b) = f depth valid b+          -- If the bit is a 0, go left+          go (i:is) (False:ts) depth valid (Split left _) =+               go is ts (depth + 1) (valid && not i) left+          -- Otherwise, continue to the right+          go (i:is) (True:ts) depth valid (Split _ right) =+               go is ts (depth + 1) (valid && i) right --- | Create a NodeTree corresponding to the Owner-Node's Id+-- | Create a NodeTree corresponding to the id create :: (Serialize i) => i -> NodeTree i-create id = zip (toByteStruct id) (repeat Nothing)---- | Insert a node into a NodeTree-insert :: (Serialize i, Eq i, Ord i) => NodeTree i -> Node i -> NodeTree i-insert tree node = case seek tree . nodeId $ node of-        -- The tree is empty, create first KBucket-        (_, (b, Nothing):xs)       -> (b, Just [node]):xs--        -- Normal case-        (beg, (b, Just bucket):xs)-            -- At least refresh the Node, as it has been active-            | node `elem` bucket -> refresh tree . nodeId $ node-            -- The last bucket may always be split-            | full bucket && ends xs -> let new = split tree . extractId $ tree-                                        in insert new node-            -- If the bucket is full and can't be split, the Node isn't inserted-            | full bucket -> tree-            -- Just insert the Node-            | otherwise -> beg ++ (b, Just $ node:bucket):xs+create id = NodeTree (toByteStruct id) . Bucket $ ([], []) -    where full b = length b >= 7+-- | Lookup a node within a NodeTree+lookup :: (Serialize i, Eq i) => NodeTree i -> i -> Maybe (Node i)+lookup tree id = applyAt tree id f+    where f _ _ = L.find (idMatches id) . map fst . fst --- Extract original Id from NodeTree-extractId :: (Serialize i) => NodeTree i -> i-extractId tree = fromByteStruct bs-    where bs = foldr (\x id -> fst x:id) [] tree+-- | Delete a Node corresponding to a supplied Id from a NodeTree+delete :: (Serialize i, Eq i) => NodeTree i -> i -> NodeTree i+delete tree id = modifyAt tree id f+    where f _ _ (nodes, cache) =+              let deleted = filter (not . idMatches id . fst) $ nodes+              in Bucket (deleted, cache) --- | Split the last bucket------   This function does some quite unsafe pattern matching for the sake of not---   ending up even longer than it already is. It is only used internally and---   all the assumptions made by those patterns are provable, so it's ok.-split :: (Serialize i, Ord i) => NodeTree i -> i -> NodeTree i-split tree id = let (begin, (b, Just bucket):xs) = seek tree id-                    (this, next) = doSplit bucket-                in begin ++ (b, Just this) : injectBucket next xs+-- | Handle a timed out node by incrementing its timeoutCount and deleting it+--  if the count exceeds the limit. Also, return wether it's reasonable to ping+--  the node again.+handleTimeout :: (Serialize i, Eq i) => NodeTree i -> i -> (NodeTree i, Bool)+handleTimeout tree id = bothAt tree id f+    where f _ _ (nodes, cache) = case L.find (idMatches id . fst) nodes of+            -- Delete a node that exceeded the limit. Don't contact it again+            --   as it is now considered dead+            Just x@(_, 4) -> (Bucket (L.delete x $ nodes, cache), False)+            -- Increment the timeoutCount+            Just x@(n, timeoutCount) ->+                 (Bucket ((n, timeoutCount + 1) : L.delete x nodes, cache), True)+            -- Don't contact an unknown node a second time+            Nothing -> (Bucket (nodes, cache), False) -    where doSplit []        = ([], [])-          doSplit (node:ns) =-            -- More matching bytes than the index means that a node can be-            -- moved to a later bucket.-            if countMatching (toByteStruct . nodeId $ node)-                             (toByteStruct id)               > index-              then second (node:) $ doSplit ns-              else first  (node:) $ doSplit ns+-- | Refresh the node corresponding to a supplied Id by placing it at the first+--   index of it's KBucket and reseting its timeoutCount, then return a Bucket+--   NodeTreeElem+refresh :: (Serialize i, Eq i) => Node i -> ([(Node i, Int)], [Node i]) -> NodeTreeElem i+refresh node (nodes, cache) =+         Bucket (case L.find (idMatches (nodeId node) . fst) nodes of+            Just x@(n, _) -> (n, 0) : L.delete x nodes+            _             -> nodes+            , cache) -          index = let (beg, _) = seek tree id in length beg-          countMatching [] [] = 0-          countMatching (a:as) (b:bs)-            | a == b    = 1 + countMatching as bs-            | otherwise = 0+-- | Insert a node into a NodeTree+insert :: (Serialize i, Eq i) => NodeTree i -> Node i -> NodeTree i+insert tree node = if applyAt tree (nodeId node) needsSplit+                   -- Split the tree before inserting, when it makes sense+                   then let splitTree = split tree . nodeId $ node+                        in insert splitTree node+                   -- Insert the node+                   else modifyAt tree (nodeId node) doInsert -          injectBucket bucket ((b, _):xs) = (b, Just bucket):xs+    where needsSplit depth valid (nodes, _) =+            let maxDepth = (length . toByteStruct . nodeId $ node) - 1+            in  -- A new node will be inserted+                node `notElem` map fst nodes &&+                -- The bucket is full+                length nodes >= 7 &&+                -- The bucket may be split+                (depth < 5 || valid) && depth <= maxDepth --- | Lookup a node within a NodeTree-lookup :: (Serialize i, Eq i) => NodeTree i -> i -> Maybe (Node i)-lookup tree id = applyTo f Nothing tree id-    where f = L.find $ idMatches id+          doInsert _ _ b@(nodes, cache)+            -- Refresh an already existing node+            | node `elem` map fst nodes = refresh node b+            -- Simply insert the node, if the bucket isn't full+            | length nodes < 7 = Bucket ((node, 0):nodes, cache)+            -- Move the node to the first spot, if it's already cached+            | node `elem` cache = Bucket (nodes, node : L.delete node cache)+            -- Cache the node and drop older ones, if necessary+            | otherwise = Bucket (nodes, node : take 4 cache) --- | Delete a Node corresponding to a supplied Id from a NodeTree-delete :: (Serialize i, Eq i) => NodeTree i -> i -> NodeTree i-delete tree id = modifyKBucket f tree id-    where f = filter $ not . idMatches id+-- | Split the KBucket the specified id would reside in into two and return a+--   Split NodeTreeElem+split :: (Serialize i) => NodeTree i -> i -> NodeTree i+split tree splitId = modifyAt tree splitId f+    where f depth _ (nodes, cache) =+            let (leftNodes, rightNodes) = splitBucket depth fst nodes+                (leftCache, rightCache) = splitBucket depth id cache+            in  Split+                   (Bucket (leftNodes, leftCache))+                   (Bucket (rightNodes, rightCache)) --- | Refresh the node corresponding to a supplied Id by placing it at the first---   index of it's KBucket-refresh :: (Serialize i, Eq i) => NodeTree i -> i -> NodeTree i-refresh tree id = modifyKBucket f tree id-    where f bucket = case L.find (idMatches id) bucket of-                Just node -> node : L.delete node bucket-                _         -> bucket+          -- Recursivly split the nodes into two buckets+          splitBucket _ _ []     = ([], [])+          splitBucket i f (n:ns) = let bs = toByteStruct . nodeId . f $ n+                                       bit = bs !! i+                                       (left, right) = splitBucket i f ns+                                   in if bit+                                      then (left, n:right)+                                      else (n:left, right)  -- | Find the k closest Nodes to a given Id------   Uset to implemenet RETURN_NODES-findClosest :: (Serialize i, Eq i) => NodeTree i -> i -> Int -> KBucket i-findClosest tree id n = case seek tree id of-    -- The tree is empty-    (_, (_, Nothing):xs) -> []--    -- Normal case-    (beg, (_, Just bk):xs)-        -- The bucket contains enough Nodes on its own-        | length bk == n -> bk-        -- We need to retrieve Nodes from other buckets as well-        | otherwise -> let-            missing = n - length bk-            in if ends xs-                    -- If it's the last one, take nodes from higher up in-                    -- the hierarchy-                    then let higher = next missing $ reverse beg-                         in take n . flip sortByDistanceTo id $ bk ++ higher-                    -- Else retrieve the missing amount of Nodes by calling-                    -- findClosest with an Id whose first differing bit doesn't-                    -- differ.-                    -- (Sounds complicated, but the tests prove that it actually-                    -- works this way)-                    else let treeId = extractId tree-                             newId  = id `alignedTo` treeId-                             other  = findClosest tree newId missing-                         in bk ++ other-    where -- Pick the n closest Nodes from the tree-          next _ [] = []-          next n ((_, Nothing):xs) = next n xs-          next n ((_, Just bk):xs)-            | length bk == n = bk-            | length bk <  n = bk ++ next (n - length bk) xs-            -- Take the n closest Nodes-            | otherwise = take n . sortByDistanceTo bk $ id+findClosest :: (Serialize i) => NodeTree i -> i -> Int -> [Node i]+findClosest (NodeTree idStruct elem) id n =+    let targetStruct = toByteStruct id+    in go idStruct targetStruct elem n+    where -- This function is partial for the same reason as in modifyAt+          --+          -- Take the n closest nodes+          go _ _ (Bucket (nodes, _)) n+            | length nodes <= n = map fst nodes+            | otherwise = take n . sortByDistanceTo (map fst nodes) $ id+          -- Take the closest nodes from the left child first, if those aren't+          -- enough, take the rest from the right+          go (i:is) (False:ts) (Split left right) n =+            let result = go is ts left n+            in if length result == n+                then result+                else result ++ go is ts right n+          -- Take the closest nodes from the right child first, if those aren't+          -- enough, take the rest from the left+          go (i:is) (True:ts) (Split left right) n =+            let result = go is ts right n+            in if length result == n+                then result+                else result ++ go is ts left n -          -- Change the first differing bit of idA to match idB-          idA `alignedTo` idB = fromByteStruct . alignF idA $ idB-          alignF = align `on` toByteStruct-          align [] [] = []-          align (a:as) (b:bs)-            | a == b    = a : align as bs-            | otherwise = b : as+-- Extract original Id from NodeTree+extractId :: (Serialize i) => NodeTree i -> i+extractId (NodeTree id _) = fromByteStruct id  -- | Helper function used for KBucket manipulation idMatches :: (Eq i) => i -> Node i -> Bool idMatches id node = id == nodeId node++-- | Turn the NodeTree into a list of nodes+toList :: NodeTree i -> [Node i]+toList (NodeTree _ elems) = go elems+    where go (Split left right) = go left ++ go right+          go (Bucket b) = map fst . fst $ b++-- | Fold over the buckets+fold :: ([Node i] -> a -> a) -> a -> NodeTree i -> a+fold f init (NodeTree _ elems) = go init elems+    where go a (Split left right) = let a' = go a left in go a' right+          go a (Bucket b) = f (map fst . fst $ b) a
src/Network/Kademlia/Types.hs view
@@ -10,7 +10,6 @@     ( Peer(..)     , toPeer     , Node(..)-    , KBucket     , sortByDistanceTo     , Serialize(..)     , Signal(..)@@ -39,12 +38,8 @@     , nodeId :: i     } deriving (Eq, Ord, Show) --- | Aliases to make the code more readable by using the same names as the---   papers-type KBucket i = [Node i]- -- | Sort a bucket by the closeness of its nodes to a give Id-sortByDistanceTo :: (Serialize i) => KBucket i -> i -> KBucket i+sortByDistanceTo :: (Serialize i) => [Node i] -> i -> [Node i] sortByDistanceTo bucket id = unpack . sort . pack $ bucket     where pack bk = zip bk $ map f bk           f = distance id . nodeId@@ -103,7 +98,7 @@                  | PONG                  | STORE        i a                  | FIND_NODE    i-                 | RETURN_NODES i (KBucket i)+                 | RETURN_NODES i [Node i]                  | FIND_VALUE   i                  | RETURN_VALUE i a                    deriving (Eq, Show)
+ test/Implementation.hs view
@@ -0,0 +1,122 @@+{-|+Module      : Implementation+Description : Tests for Network.Kademlia.Implementation++Tests specific to Network.Kademlia.Implementation.+-}++module Implementation where++import Test.HUnit hiding (assert)+import Test.QuickCheck+import Test.QuickCheck.Monadic+import TestTypes++import qualified Network.Kademlia as K+import qualified Network.Kademlia.Tree as T+import Network.Kademlia.Types+import Network.Kademlia.Instance++import Control.Monad+import Control.Applicative+import Control.Concurrent.STM++import Data.Maybe (isJust, fromJust)+import qualified Data.ByteString.Char8 as C++constructNetwork :: IdBunch IdType -> PropertyM IO [KademliaInstance IdType String]+constructNetwork idBunch = run $ do+    let entryNode = Node (Peer "127.0.0.1" 1123) (head . getIds $ idBunch)+    instances <- zipWithM K.create [1123..] (getIds idBunch)+                        :: IO [KademliaInstance IdType String]++    forM_ (tail instances) (`K.joinNetwork` entryNode)+    return instances++joinCheck :: IdBunch IdType -> Property+joinCheck idBunch = monadicIO $ do+    instances <- constructNetwork idBunch+    present <- run $ do+        mapM_ K.close instances+        mapM filled instances+    assert . and $ present++    where filled inst = do+            tree <- atomically . readTVar . sTree . state $ inst+            return $ (length . T.toList $ tree) >= 7++-- | Make sure ID clashes are detected properly+idClashCheck :: IdType -> IdType -> Property+idClashCheck idA idB = monadicIO $ do+    let peers = map (Peer "127.0.0.1") [1123..]+        ids = [idA, idB, idA]+        entryNode = Node (Peer "127.0.0.1" 1124) idB++    joinResult <- run $ do+        insts@[kiA, _, kiB] <- zipWithM K.create [1123..] ids+                            :: IO [KademliaInstance IdType String]++        K.joinNetwork kiA $ entryNode+        joinResult <- K.joinNetwork kiB $ entryNode++        mapM_ K.close insts++        return joinResult++    assert $ joinResult == K.IDClash+++-- | Make sure an offline peer is detected+nodeDownCheck :: Assertion+nodeDownCheck = do+    let entryNode = Node (Peer "127.0.0.1" 1124) idB+    inst <- K.create 1123 idA :: IO (KademliaInstance IdType String)+    joinResult <- K.joinNetwork inst entryNode+    K.close inst++    assertEqual "" joinResult K.NodeDown++    where idA = IT . C.pack $ "hello"+          idB = IT . C.pack $ "herro"++storeAndLookupCheck :: IdBunch IdType -> IdBunch IdType -> Property+storeAndLookupCheck ids keys = monadicIO $ do+    let keyVal = zip (getIds keys) vals+    instances <- constructNetwork ids++    success <- run $ do+        mapM_ (doStore instances) keyVal++        success <- forM instances $ \inst ->+            and <$> mapM (tryLookup inst) keyVal++        mapM_ K.close instances++        return success++    assert . and $ success++    where vals = take 20 . map (replicate 5) $ ['a'..]+          doStore instances (key, val) = K.store (head instances) key val+          tryLookup inst (key, val) = do+            result <- K.lookup inst key+            case result of+                Just (v, _) -> return $ v == val+                _ -> return False++lookupNodesCheck :: IdBunch IdType -> Property+lookupNodesCheck ids = monadicIO $ do+    instances <- constructNetwork ids++    success <- run $ do+        success <- forM instances $ \inst ->+            and <$> (mapM (tryLookup inst) . getIds $ ids)++        mapM_ K.close instances++        return success++    assert . and $ success++    where tryLookup inst id = check id <$> K.lookupNode inst id+          check id result = isJust result && id == (nodeId . fromJust $ result)
+ test/Instance.hs view
@@ -0,0 +1,132 @@+{-|+Module      : Instance+Description : Tests for Network.Kademlia.Instance++Tests specific to Network.Kademlia.Instance.+-}++module Instance where++import Test.HUnit hiding (assert)+import Test.QuickCheck+import Test.QuickCheck.Monadic++import Network.Kademlia.Instance as I+import Network.Kademlia+import Network.Kademlia.Networking+import Network.Kademlia.Types+import Network.Kademlia.ReplyQueue+import qualified Data.ByteString.Char8 as C+import Control.Concurrent.Chan+import Control.Monad (liftM2)+import Data.Maybe (isJust, fromJust)++import TestTypes++-- | The default set of peers+peers :: (Peer, Peer)+peers = let pA = Peer "127.0.0.1" 1122+            pB = Peer "127.0.0.1" 1123+        in (pA, pB)++-- | A set of randomly generated Ids+ids :: (Monad m) => PropertyM m (IdType, IdType)+ids = liftM2 (,) (pick arbitrary) (pick arbitrary)++-- | Checks wether PINGs are handled appropriately+handlesPingCheck :: Assertion+handlesPingCheck = do+    let (pA, pB) = peers++    let (Right (idA, _)) = fromBS . C.replicate 32 $ 'a'+                           :: Either String (IdType, C.ByteString)+    let (Right (idB, _)) = fromBS . C.replicate 32 $ 'b'+                           :: Either String (IdType, C.ByteString)++    rq <- emptyReplyQueue++    khA <- openOn "1122" idA rq :: IO (KademliaHandle IdType String)+    kiB <- create 1123 idB   :: IO (KademliaInstance IdType String)++    startRecvProcess khA++    send khA pB PING+    (Answer sig) <- readChan . timeoutChan $ rq :: IO (Reply IdType String)++    closeK khA+    close kiB++    assertEqual "" (command sig) PONG+    assertEqual "" (peer . source $ sig) pB+    assertEqual "" (nodeId . source $ sig) idB++    return ()++-- | Make sure a stored value can be retrieved+storeAndFindValueCheck :: IdType -> String -> Property+storeAndFindValueCheck key value = monadicIO $ do+    let (pA, pB) = peers+    (idA, idB) <- ids++    receivedCmd <- run $ do+        rq <- emptyReplyQueue++        khA <- openOn "1122" idA rq+        kiB <- create 1123 idB :: IO (KademliaInstance IdType String)++        startRecvProcess khA++        send khA pB $ STORE key value+        send khA pB $ FIND_VALUE key++        -- There is a race condition, so the instance will sometimes try to store+        -- the value in the handle, before replying with a RETURN_VALUE+        (Answer sig) <- readChan . timeoutChan $ rq :: IO (Reply IdType String)+        sig <- case command sig of+                STORE _ _ -> do+                    (Answer sig) <- readChan . timeoutChan $ rq :: IO (Reply IdType String)+                    return sig+                _ -> return sig++        closeK khA+        close kiB++        return . command $ sig++    let cmd = RETURN_VALUE key value :: Command IdType String++    monitor . counterexample $ "Commands inequal: " ++ show cmd ++ " /= " ++ show receivedCmd+    assert $ cmd == receivedCmd++    return ()++-- | Assert that a peer is put into the NodeTree on first encounter+trackingKnownPeersCheck :: Property+trackingKnownPeersCheck = monadicIO $ do+    let (_, pB) = peers+    (idA, idB) <- ids++    (node, kiB) <- run $ do+        rq <- emptyReplyQueue :: IO (ReplyQueue IdType String)++        khA <- openOn "1122" idA rq+        kiB <- create 1123 idB :: IO (KademliaInstance IdType String)++        startRecvProcess khA++        send khA pB $ PING+        readChan . timeoutChan $ rq++        node <- I.lookupNode kiB idA++        closeK khA+        close kiB++        return (node, kiB)++    assert . isJust $ node++    nodes <- run . dumpPeers $ kiB+    assert $ nodes == [fromJust node]++    return ()
+ test/Networking.hs view
@@ -0,0 +1,94 @@+{-|+Module      : Networking+Description : Tests for Network.Kademlia.Networking++Tests specific to Network.Kademlia.Networking.+-}++module Networking where++import Test.QuickCheck+import Test.QuickCheck.Monadic++import Network.Kademlia.Networking+import Network.Kademlia.Types+import Network.Kademlia.ReplyQueue+import Control.Monad+import Control.Concurrent.Chan+import Control.Concurrent.STM+import qualified Data.ByteString.Char8 as C+import Data.Maybe (isJust)++import TestTypes++valueSet :: (Monad m) => PropertyM m (Peer, Peer, IdType, IdType)+valueSet = do+    let pA = Peer "127.0.0.1" 1122+        pB = Peer "127.0.0.1" 1123++    idA <- pick (arbitrary :: Gen IdType)+    idB <- pick (arbitrary :: Gen IdType)++    return (pA, pB, idA, idB)++-- | Make sure sending and receiving works+sendCheck :: Command IdType String -> Property+sendCheck cmd = monadicIO $ do+    (pA, pB, idA, idB) <- valueSet++    sig <- run $ do+        rqA <- emptyReplyQueue+        rqB <- emptyReplyQueue++        khA <- openOn "1122" idA rqA+        khB <- (openOn "1123" idB rqB :: IO (KademliaHandle IdType String))++        startRecvProcess khB++        send khA pB cmd+        (Answer sig) <- readChan . timeoutChan $ rqB :: IO (Reply IdType String)++        closeK khA+        closeK khB++        return sig++    assert $ command sig == cmd+    assert $ (peer . source $ sig) == pA+    assert $ (nodeId . source $ sig) == idA++    return ()++-- | Make sure expect works the way it's supposed to+expectCheck :: Signal IdType String -> IdType -> Property+expectCheck sig idA = monadicIO $ do+    let rtM = rType . command $ sig+    pre . isJust $ rtM+    let (Just rt) = rtM+        rr = RR [rt] . nodeId . source $ sig++    replySig <- run $ do+        rqA <- emptyReplyQueue++        khA <- openOn "1122" idA rqA++        startRecvProcess khA++        testChan <- newChan :: IO (Chan (Reply IdType String))+        expect khA rr testChan+        dispatch (Answer sig) rqA++        (Answer replySig) <- readChan testChan :: IO (Reply IdType String)++        closeK khA++        return replySig++    assert $ replySig == sig++-- | Convert a command into a ReplyType+rType :: Command i a -> Maybe (ReplyType i)+rType  PONG               = Just  R_PONG+rType (RETURN_VALUE id _) = Just (R_RETURN_VALUE id)+rType (RETURN_NODES id _) = Just (R_RETURN_NODES id)+rType _ = Nothing
+ test/Protocol.hs view
@@ -0,0 +1,35 @@+{-|+Module      : Protocol+Description : Test for Network.Kademlia.Protocol++Tests specific to Network.Kademlia.Protocol.+-}++module Protocol+    ( parseCheck+    , lengthCheck+    , IdType(..)+    ) where++import Test.QuickCheck++import qualified Data.ByteString as B+import Network.Kademlia.Types+import Network.Kademlia.Protocol+import TestTypes++-- | A signal is the same as its serialized form parsed+parseCheck :: Signal IdType String -> Property+parseCheck s = test . parse (peer . source $ s) . serialize id . command $ s+    where id = nodeId . source $ s+          test (Left err) = counterexample "Parsing failed" False+          test (Right s') = counterexample+            ("Signals differ:\nIn:  " ++ show s ++ "\nOut: "+                 ++ show s' ++ "\n") $ s === s'++-- | A serialized signal's length is no longer than the max. UDP packet size+--   (or at least what I believe to be the max UDP packet size)+lengthCheck :: Signal IdType String -> Property+lengthCheck s = counterexample err $ length <= 1500+    where length = B.length . serialize (nodeId . source $ s) . command $ s+          err = "Serialized signal is too long: " ++ show length ++ " bytes"
+ test/ReplyQueue.hs view
@@ -0,0 +1,87 @@+{-|+Module      : ReplyQueue+Description : Tests for Network.Kademlia.ReplyQueue++Tests specific to Network.Kademlia.ReplyQueue+-}++module ReplyQueue where++import Test.QuickCheck+import Test.QuickCheck.Monadic++import Control.Concurrent.Chan+import Control.Concurrent.STM+import Data.Maybe (isJust)++import Network.Kademlia.ReplyQueue+import Network.Kademlia.Types++import TestTypes++-- | Check wether registered reply handlers a used+repliesCheck :: Signal IdType String -> Signal IdType String -> Property+repliesCheck sig1 sig2 = monadicIO $ do+    let reg1 = toRegistration sig1+    let reg2 = toRegistration sig2++    pre $ isJust reg1 && isJust reg2++    let (Just replyReg1) = reg1+    let (Just replyReg2) = reg2++    contents <- run $ do+        rq <- emptyReplyQueue+        chan <- newChan :: IO (Chan (Reply IdType String))++        register replyReg1 rq chan+        register replyReg2 rq chan++        dispatch (Answer sig1) rq+        dispatch (Answer sig2) rq++        contents <- getChanContents chan++        return contents++    assert . not . null $ contents++    let [reply1, reply2] = take 2 contents++    assert $ reply1 /= Closed+    assert $ reply2 /= Closed++    let (Answer unwrapped1) = reply1+    let (Answer unwrapped2) = reply2++    assert $ unwrapped1 == sig1+    assert $ unwrapped2 == sig2++-- | Check wether registered reply handlers are removed after usage+removedCheck :: Signal IdType String -> Property+removedCheck sig = monadicIO $ do+    let reg = toRegistration sig+    case reg of+        -- Discard the test case+        Nothing -> pre False+        Just reg -> do+            removed <- run $ do+                rq <- emptyReplyQueue+                chan <- newChan :: IO (Chan (Reply IdType String))+                register reg rq chan+                dispatch (Answer sig) rq+                fmap null (atomically . readTVar . queue $ rq)+            assert removed++-- | Convert a Signal into its ReplyRegistration representation+toRegistration :: Signal i a -> Maybe (ReplyRegistration i)+toRegistration sig = case rType . command $ sig of+                        Nothing -> Nothing+                        Just rt -> Just (RR [rt] origin)+    where origin = nodeId . source $ sig++          rType :: Command i a -> Maybe (ReplyType i)+          rType  PONG               = Just  R_PONG+          rType (RETURN_VALUE id _) = Just (R_RETURN_VALUE id)+          rType (RETURN_NODES id _) = Just (R_RETURN_NODES id)+          rType _ = Nothing
+ test/Test.hs view
@@ -0,0 +1,112 @@+{-|+Module      : Tests+Description : Tests for the modules++A few tests using QuickCheck and Tasty to make sure everything works+the way it's supposed to.+-}++module Main where++import Test.Tasty+import Test.Tasty.QuickCheck as QC+import Test.Tasty.HUnit as HU++import Types+import Protocol+import Networking+import Tree+import Instance+import ReplyQueue+import Implementation++main = defaultMain tests++tests :: TestTree+tests = testGroup "Tests" [quickCheckTests, hUnitTests]++quickCheckTests = testGroup "QuickCheck" [+      typeChecks+    , protocolChecks+    , networkingChecks+    , treeChecks+    , instanceChecks+    , replyQueueChecks+    , implementationChecks+    ]++typeChecks = testGroup "Network.Kademlia.Types" [+      QC.testProperty "ByteString to ByteStruct conversion works"+         toByteStructCheck+    , QC.testProperty "ByteStruct to ByteString conversion works"+         fromByteStructCheck+    ]++protocolChecks = testGroup "Network.Kademlia.Protocol" [+      QC.testProperty "Protocol Serializing and Parsing works"+         parseCheck+    , QC.testProperty "Protocol messages are within the max UDP packet size"+         lengthCheck+    ]++networkingChecks = testGroup "Network.Kademlia.Networking" [+      QC.testProperty "Sending and Receiving works"+         sendCheck+    , QC.testProperty "Expecting works the way it's supposed to"+         expectCheck+    ]++treeChecks = testGroup "Network.Kademlia.Tree" [+      QC.testProperty "Inserting into the Tree works"+         insertCheck+    , QC.testProperty "Deleting from the Tree works"+         deleteCheck+    , QC.testProperty "Splitting works as expected"+         splitCheck+    , QC.testProperty "Buckets are within the size limit"+         bucketSizeCheck+    , QC.testProperty "Refreshing works as expected"+         refreshCheck+    , QC.testProperty "Finding closest works"+         findClosestCheck+    ]++instanceChecks = testGroup "Network.Kademlia.Instance" [+      QC.testProperty "Storing and Retrieving values works"+         storeAndFindValueCheck+    , QC.testProperty "Peers are put into the tree on first encounter"+         trackingKnownPeersCheck+    ]++replyQueueChecks = testGroup "Network.Kademlia.ReplyQueue" [+      QC.testProperty "Registering replies works"+          repliesCheck+    , QC.testProperty "Registrations are removed after being dispatched"+         removedCheck+    ]++implementationChecks = testGroup "Network.Kademlia.Implementation" [+      QC.testProperty "Joining the Network works"+         joinCheck+    , QC.testProperty "ID clashes are detected"+         idClashCheck+    , QC.testProperty "Storing and looking up values works"+        storeAndLookupCheck+    , QC.testProperty "Looking up Nodes works"+        lookupNodesCheck+    ]++hUnitTests = testGroup "HUnit" [+      instanceCases+    , implementationCases+    ]++instanceCases = testGroup "Network.Kademlia.Instance" [+      HU.testCase "PINGs are automaticly handled"+         handlesPingCheck+    ]++implementationCases = testGroup "Network.Kademlia.Implementation" [+      HU.testCase "Trying to join over an offline Node is detected"+         nodeDownCheck+    ]
+ test/TestTypes.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}++{-|+Module      : TestTypes+Description : Types and Generators needed for general testing+-}++module TestTypes where++import Test.QuickCheck++import Control.Monad (liftM, liftM2, liftM3)+import Control.Arrow (first)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as C+import Network.Socket (PortNumber)+import Data.Word(Word16)+import Data.List (nubBy)+import Data.Function (on)++import Network.Kademlia.Types++newtype IdType = IT { getBS :: B.ByteString } deriving (Eq, Ord)++-- Custom show instance+instance Show IdType where+    show = show . C.unpack . getBS++-- A simple 5-byte ByteString+instance Serialize IdType where+    toBS = getBS+    fromBS bs = if B.length bs >= 5+        then Right $ first IT . B.splitAt 5 $ bs+        else Left "ByteString to short."++instance Serialize String where+    toBS = C.pack . show+    fromBS s =+        case (reads :: ReadS String) . C.unpack $ s of+            []               -> Left "Failed to parse string."+            (result, rest):_ -> Right (result, C.pack rest)++instance Arbitrary IdType where+    arbitrary = do+        str <- vectorOf 5 arbitrary+        return $ IT $ C.pack str++instance Arbitrary PortNumber where+    arbitrary = liftM fromIntegral (arbitrary :: Gen Word16)++instance Arbitrary Peer where+    arbitrary = do+        host <- arbitrary `suchThat` \s -> ' ' `notElem` s && not (null s)+                                           && length s < 20+        port <- arbitrary+        return $ Peer host port++instance (Arbitrary i, Arbitrary v) => Arbitrary (Command i v) where+    arbitrary = oneof [+          return PING+        , return PONG+        , liftM2 STORE arbitrary arbitrary+        , liftM FIND_NODE arbitrary+        , liftM2 RETURN_NODES arbitrary $ vectorOf 15 arbitrary+        , liftM FIND_VALUE arbitrary+        , liftM2 RETURN_VALUE arbitrary arbitrary+        ]++instance (Arbitrary i, Arbitrary v) => Arbitrary (Signal i v) where+    arbitrary = liftM2 Signal arbitrary arbitrary++instance (Arbitrary i) => Arbitrary (Node i) where+    arbitrary = liftM2 Node arbitrary arbitrary++-- | This enables me to specifiy a new Arbitrary instance+newtype NodeBunch i = NB {+      nodes :: [Node i]+    } deriving (Show)++-- | Make sure all Ids are unique+instance (Arbitrary i, Eq i) => Arbitrary (NodeBunch i) where+    arbitrary = liftM NB $ vectorOf 20 arbitrary `suchThat` individualIds+        where individualIds = individual ((==) `on` nodeId)++individual :: (a -> a -> Bool) -> [a] -> Bool+individual eq s = length s == (length . clear $ s)+    where clear = nubBy eq++-- | This is needed for the Implementation tests+newtype IdBunch i = IB {+      getIds :: [i]+    } deriving (Show)++instance (Arbitrary i, Eq i) => Arbitrary (IdBunch i) where+    arbitrary = liftM IB $ vectorOf 20 arbitrary `suchThat` individual (==)
+ test/Tree.hs view
@@ -0,0 +1,86 @@+{-|+Module      : Tree+Description : Tests for Network.Kademlia.Tree++Tests specific to Network.Kademlia.Tree.+-}++module Tree where++import Test.QuickCheck++import qualified Network.Kademlia.Tree as T+import Network.Kademlia.Types+import Control.Monad (liftM)+import Data.List (sortBy)+import Data.Maybe (isJust)++import TestTypes++-- | Helper method for lookup checking+lookupCheck :: (Serialize i, Eq i) => T.NodeTree i -> Node i -> Bool+lookupCheck tree node = T.lookup tree (nodeId node) == Just node++-- | Check wether an inserted Node is retrievable+insertCheck :: IdType -> Node IdType -> Bool+insertCheck id node = lookupCheck tree node+    where tree = T.insert (T.create id) node++-- | Make sure a deleted Node can't be retrieved anymore+deleteCheck :: IdType -> Node IdType -> Bool+deleteCheck id node = not . lookupCheck tree $ node+    where tree = T.delete origin . nodeId $ node+          origin = T.insert (T.create id) node++withTree :: (T.NodeTree IdType -> [Node IdType] -> a) ->+            NodeBunch IdType -> IdType -> a+withTree f bunch id = f tree $ nodes bunch+    where tree = foldr (flip T.insert) (T.create id) $ nodes bunch++splitCheck :: NodeBunch IdType -> IdType -> Property+splitCheck = withTree f+    where f tree nodes = conjoin . foldr (foldingFunc tree) [] $ nodes++          tree `contains` node = node `elem` T.toList tree++          foldingFunc tree node props = prop : props+            where prop =+                    counterexample ("Failed to find " ++ show node) $+                  -- There is the possibiliy that nodes weren't inserted+                  -- because of full buckets.+                    lookupCheck tree node || not (tree `contains` node)++-- | Make sure the bucket sizes end up correct+bucketSizeCheck :: NodeBunch IdType -> IdType -> Bool+bucketSizeCheck = withTree $ \tree _ -> T.fold foldingFunc True tree+    where foldingFunc _ False = False+          foldingFunc b  _    = length b <= 7++-- | Make sure refreshed Nodes are actually refreshed+refreshCheck :: NodeBunch IdType -> IdType -> Bool+refreshCheck = withTree f+    where f tree nodes = T.fold foldingFunc True refreshed+            where refreshed = T.insert tree node+                  node = last nodes+                  foldingFunc _  False = False+                  foldingFunc b _      = node `notElem` b+                                         || head b == node++-- | Make sure findClosest returns the Node with the closest Ids of all nodes+--   in the tree.+findClosestCheck :: IdType -> NodeBunch IdType -> IdType -> Property+findClosestCheck id = withTree f+    where f tree nodes = conjoin . foldr g [] $ manualClosest+           where g node props = counterexample (text node) (prop node):props+                  where prop node = node `elem` treeClosest+                        text node = "Failed to find: " ++ show node++                 treeClosest = T.findClosest tree id 7++                 contained = filter contains nodes+                 contains node = isJust . T.lookup tree . nodeId $ node++                 manualClosest = map fst . take 7 . sort $ packed+                 packed = zip contained $ map distanceF contained+                 distanceF = distance id . nodeId+                 sort = sortBy $ \(_, a) (_, b) -> compare a b
+ test/Types.hs view
@@ -0,0 +1,28 @@+{-|+Module      : Types+Description : Tests for Network.Kademlia.Types++Tests specific to Network.Kademlia.Types.+-}++module Types where++import Test.QuickCheck++import qualified Data.ByteString as B+import Data.Bits (testBit)++import TestTypes+import Network.Kademlia.Types++-- | Checks wether toByteStruct converts corretctly+toByteStructCheck :: IdType -> Bool+toByteStructCheck id = foldl foldingFunc True [0..length converted - 1]+    where converted = toByteStruct id+          words = B.unpack . toBS $ id+          foldingFunc b i = b && (converted !! i == access words i)+          access ws i = testBit (ws !! (i `div` 8)) (i `mod` 8)++-- | Checks wether fromByteStruct converts corretctly+fromByteStructCheck :: IdType -> Bool+fromByteStructCheck id = id == (fromByteStruct . toByteStruct $ id)