packages feed

kademlia-1.0.0.0: src/Network/Kademlia/Tree.hs

{-|
Module      : Network.Kademlia.Tree
Description : Implementation of the Node Storage Tree

Network.Kademlia.Tree implements the Node Storage Tree used to store
and look up the known nodes.

This module is designed to be used as a qualified import.
-}

module Network.Kademlia.Tree
    ( NodeTree
    , create
    , insert
    , lookup
    , delete
    , refresh
    , findClosest
    , extractId
    ) where

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))]

-- | Structure used for easier modification of the NodeTree
type Zipper i = (NodeTree i, NodeTree 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)

-- | Cheks wether a NodeTree ends
ends :: NodeTree i -> Bool
ends ((_, Just _):_)  = False
ends ((_, Nothing):_) = True

-- | 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 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 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)

-- | Create a NodeTree corresponding to the Owner-Node's 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

    where full b = length b >= 7

-- Extract original Id from NodeTree
extractId :: (Serialize i) => NodeTree i -> i
extractId tree = fromByteStruct bs
    where bs = foldr (\x id -> fst x:id) [] tree

-- | 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

    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

          index = let (beg, _) = seek tree id in length beg
          countMatching [] [] = 0
          countMatching (a:as) (b:bs)
            | a == b    = 1 + countMatching as bs
            | otherwise = 0

          injectBucket bucket ((b, _):xs) = (b, Just bucket):xs

-- | 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

-- | 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

-- | 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

-- | 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

          -- 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

-- | Helper function used for KBucket manipulation
idMatches :: (Eq i) => i -> Node i -> Bool
idMatches id node = id == nodeId node