diff options
author | joe <joe@jerkface.net> | 2017-06-09 01:10:19 -0400 |
---|---|---|
committer | joe <joe@jerkface.net> | 2017-06-09 01:10:19 -0400 |
commit | 13b2eb08cb4651a913849d96f516ed97bad53003 (patch) | |
tree | d9855b98c496ac3cc4e1fe4ba36b1c1fd1e91a13 /src/Network/DHT/Routing.hs | |
parent | ecde95b20167e02092f6a359eac865ba9155614c (diff) |
Rename Network.BitTorrent.DHT.Routing -> Network.DHT.Routing
Diffstat (limited to 'src/Network/DHT/Routing.hs')
-rw-r--r-- | src/Network/DHT/Routing.hs | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/src/Network/DHT/Routing.hs b/src/Network/DHT/Routing.hs new file mode 100644 index 00000000..c0a431fa --- /dev/null +++ b/src/Network/DHT/Routing.hs | |||
@@ -0,0 +1,595 @@ | |||
1 | -- | | ||
2 | -- Copyright : (c) Sam Truzjan 2013 | ||
3 | -- License : BSD3 | ||
4 | -- Maintainer : pxqr.sta@gmail.com | ||
5 | -- Stability : experimental | ||
6 | -- Portability : portable | ||
7 | -- | ||
8 | -- Every node maintains a routing table of known good nodes. The | ||
9 | -- nodes in the routing table are used as starting points for | ||
10 | -- queries in the DHT. Nodes from the routing table are returned in | ||
11 | -- response to queries from other nodes. | ||
12 | -- | ||
13 | -- For more info see: | ||
14 | -- <http://www.bittorrent.org/beps/bep_0005.html#routing-table> | ||
15 | -- | ||
16 | {-# LANGUAGE CPP #-} | ||
17 | {-# LANGUAGE RecordWildCards #-} | ||
18 | {-# LANGUAGE BangPatterns #-} | ||
19 | {-# LANGUAGE ViewPatterns #-} | ||
20 | {-# LANGUAGE TypeOperators #-} | ||
21 | {-# LANGUAGE DeriveGeneric #-} | ||
22 | {-# LANGUAGE ScopedTypeVariables #-} | ||
23 | {-# LANGUAGE StandaloneDeriving, FlexibleContexts, MultiParamTypeClasses, FlexibleInstances #-} | ||
24 | {-# OPTIONS_GHC -fno-warn-orphans #-} | ||
25 | module Network.DHT.Routing | ||
26 | ( -- * Table | ||
27 | Table | ||
28 | , Info(..) | ||
29 | |||
30 | -- * Attributes | ||
31 | , BucketCount | ||
32 | , defaultBucketCount | ||
33 | , BucketSize | ||
34 | , defaultBucketSize | ||
35 | , NodeCount | ||
36 | |||
37 | -- * Query | ||
38 | , Network.DHT.Routing.null | ||
39 | , Network.DHT.Routing.full | ||
40 | , thisId | ||
41 | , shape | ||
42 | , Network.DHT.Routing.size | ||
43 | , Network.DHT.Routing.depth | ||
44 | , compatibleNodeId | ||
45 | |||
46 | -- * Lookup | ||
47 | , K | ||
48 | , defaultK | ||
49 | , TableKey (..) | ||
50 | , kclosest | ||
51 | |||
52 | -- * Construction | ||
53 | , Network.DHT.Routing.nullTable | ||
54 | , Event(..) | ||
55 | , CheckPing(..) | ||
56 | , Network.DHT.Routing.insert | ||
57 | |||
58 | -- * Conversion | ||
59 | , Network.DHT.Routing.TableEntry | ||
60 | , Network.DHT.Routing.toList | ||
61 | |||
62 | -- * Routing | ||
63 | , Timestamp | ||
64 | ) where | ||
65 | |||
66 | import Control.Applicative as A | ||
67 | import Control.Arrow | ||
68 | import Control.Monad | ||
69 | import Data.Function | ||
70 | import Data.Functor.Identity | ||
71 | import Data.List as L hiding (insert) | ||
72 | import Data.Maybe | ||
73 | import Data.Monoid | ||
74 | import Data.Wrapper.PSQ as PSQ | ||
75 | import Data.Serialize as S hiding (Result, Done) | ||
76 | import qualified Data.Sequence as Seq | ||
77 | import Data.Time | ||
78 | import Data.Time.Clock.POSIX | ||
79 | import Data.Word | ||
80 | import GHC.Generics | ||
81 | import Text.PrettyPrint as PP hiding ((<>)) | ||
82 | import Text.PrettyPrint.HughesPJClass (pPrint,Pretty) | ||
83 | import qualified Data.ByteString as BS | ||
84 | import Data.Bits | ||
85 | |||
86 | import Network.BitTorrent.Address | ||
87 | |||
88 | |||
89 | {----------------------------------------------------------------------- | ||
90 | -- Routing monad | ||
91 | -----------------------------------------------------------------------} | ||
92 | |||
93 | -- | Last time the node was responding to our queries. | ||
94 | -- | ||
95 | -- Not all nodes that we learn about are equal. Some are \"good\" and | ||
96 | -- some are not. Many nodes using the DHT are able to send queries | ||
97 | -- and receive responses, but are not able to respond to queries | ||
98 | -- from other nodes. It is important that each node's routing table | ||
99 | -- must contain only known good nodes. A good node is a node has | ||
100 | -- responded to one of our queries within the last 15 minutes. A | ||
101 | -- node is also good if it has ever responded to one of our queries | ||
102 | -- and has sent us a query within the last 15 minutes. After 15 | ||
103 | -- minutes of inactivity, a node becomes questionable. Nodes become | ||
104 | -- bad when they fail to respond to multiple queries in a row. Nodes | ||
105 | -- that we know are good are given priority over nodes with unknown | ||
106 | -- status. | ||
107 | -- | ||
108 | type Timestamp = POSIXTime | ||
109 | |||
110 | {----------------------------------------------------------------------- | ||
111 | Bucket | ||
112 | -----------------------------------------------------------------------} | ||
113 | -- TODO: add replacement cache to the bucket | ||
114 | -- | ||
115 | -- When a k-bucket is full and a new node is discovered for that | ||
116 | -- k-bucket, the least recently seen node in the k-bucket is | ||
117 | -- PINGed. If the node is found to be still alive, the new node is | ||
118 | -- place in a secondary list, a replacement cache. The replacement | ||
119 | -- cache is used only if a node in the k-bucket stops responding. In | ||
120 | -- other words: new nodes are used only when older nodes disappear. | ||
121 | |||
122 | -- | Timestamp - last time this node is pinged. | ||
123 | type NodeEntry dht ip u = Binding (NodeInfo dht ip u) Timestamp | ||
124 | |||
125 | -- TODO instance Pretty where | ||
126 | |||
127 | -- | Number of nodes in a bucket. | ||
128 | type BucketSize = Int | ||
129 | |||
130 | -- | Maximum number of 'NodeInfo's stored in a bucket. Most clients | ||
131 | -- use this value. | ||
132 | defaultBucketSize :: BucketSize | ||
133 | defaultBucketSize = 8 | ||
134 | |||
135 | data QueueMethods m elem fifo = QueueMethods | ||
136 | { pushBack :: elem -> fifo -> m fifo | ||
137 | , popFront :: fifo -> m (Maybe elem, fifo) | ||
138 | , emptyQueue :: m fifo | ||
139 | } | ||
140 | |||
141 | {- | ||
142 | fromQ :: Functor m => | ||
143 | ( a -> b ) | ||
144 | -> ( b -> a ) | ||
145 | -> QueueMethods m elem a | ||
146 | -> QueueMethods m elem b | ||
147 | fromQ embed project QueueMethods{..} = | ||
148 | QueueMethods { pushBack = \e -> fmap embed . pushBack e . project | ||
149 | , popFront = fmap (second embed) . popFront . project | ||
150 | , emptyQueue = fmap embed emptyQueue | ||
151 | } | ||
152 | -} | ||
153 | |||
154 | seqQ :: QueueMethods Identity (NodeInfo dht ip u) (Seq.Seq (NodeInfo dht ip u)) | ||
155 | seqQ = QueueMethods | ||
156 | { pushBack = \e fifo -> pure (fifo Seq.|> e) | ||
157 | , popFront = \fifo -> case Seq.viewl fifo of | ||
158 | e Seq.:< fifo' -> pure (Just e, fifo') | ||
159 | Seq.EmptyL -> pure (Nothing, Seq.empty) | ||
160 | , emptyQueue = pure Seq.empty | ||
161 | } | ||
162 | |||
163 | type BucketQueue dht ip u = Seq.Seq (NodeInfo dht ip u) | ||
164 | |||
165 | bucketQ :: QueueMethods Identity (NodeInfo dht ip u) (BucketQueue dht ip u) | ||
166 | bucketQ = seqQ | ||
167 | |||
168 | -- | Bucket is also limited in its length — thus it's called k-bucket. | ||
169 | -- When bucket becomes full, we should split it in two lists by | ||
170 | -- current span bit. Span bit is defined by depth in the routing | ||
171 | -- table tree. Size of the bucket should be choosen such that it's | ||
172 | -- very unlikely that all nodes in bucket fail within an hour of | ||
173 | -- each other. | ||
174 | -- | ||
175 | data Bucket dht ip u = Bucket { bktNodes :: !(PSQ (NodeInfo dht ip u) Timestamp) | ||
176 | , bktQ :: !(BucketQueue dht ip u) | ||
177 | } deriving Generic | ||
178 | |||
179 | deriving instance (Show ip, Show u, Show (NodeId dht)) => Show (Bucket dht ip u) | ||
180 | |||
181 | |||
182 | getGenericNode :: ( Serialize (NodeId dht) | ||
183 | , Serialize ip | ||
184 | , Serialize u | ||
185 | ) => Get (NodeInfo dht ip u) | ||
186 | getGenericNode = do | ||
187 | nid <- get | ||
188 | naddr <- get | ||
189 | u <- get | ||
190 | return NodeInfo | ||
191 | { nodeId = nid | ||
192 | , nodeAddr = naddr | ||
193 | , nodeAnnotation = u | ||
194 | } | ||
195 | |||
196 | putGenericNode :: ( Serialize (NodeId dht) | ||
197 | , Serialize ip | ||
198 | , Serialize u | ||
199 | ) => NodeInfo dht ip u -> Put | ||
200 | putGenericNode (NodeInfo nid naddr u) = do | ||
201 | put nid | ||
202 | put naddr | ||
203 | put u | ||
204 | |||
205 | instance (Eq ip, Ord (NodeId dht), Serialize (NodeId dht), Serialize ip, Serialize u) => Serialize (Bucket dht ip u) where | ||
206 | get = Bucket . psqFromPairList <$> getListOf ( (,) <$> getGenericNode <*> get ) <*> pure (runIdentity $ emptyQueue bucketQ) | ||
207 | put = putListOf (\(ni,stamp) -> putGenericNode ni >> put stamp) . psqToPairList . bktNodes | ||
208 | |||
209 | |||
210 | psqFromPairList :: (Ord p, Ord k) => [(k, p)] -> OrdPSQ k p () | ||
211 | psqFromPairList xs = PSQ.fromList $ map (\(a,b) -> a :-> b) xs | ||
212 | |||
213 | psqToPairList :: OrdPSQ t t1 () -> [(t, t1)] | ||
214 | psqToPairList psq = map (\(a :-> b) -> (a,b)) $ PSQ.toList psq | ||
215 | |||
216 | -- | Update interval, in seconds. | ||
217 | delta :: NominalDiffTime | ||
218 | delta = 15 * 60 | ||
219 | |||
220 | -- | Should maintain a set of stable long running nodes. | ||
221 | -- | ||
222 | -- Note: pings are triggerd only when a bucket is full. | ||
223 | insertBucket :: (Eq ip, Alternative f, Ord (NodeId dht)) => Timestamp -> Event dht ip u -> Bucket dht ip u | ||
224 | -> f ([CheckPing dht ip u], Bucket dht ip u) | ||
225 | insertBucket curTime (TryInsert info) bucket | ||
226 | -- just update timestamp if a node is already in bucket | ||
227 | | already_have | ||
228 | = pure ( [], map_ns $ PSQ.insertWith max info curTime ) | ||
229 | -- bucket is good, but not full => we can insert a new node | ||
230 | | PSQ.size (bktNodes bucket) < defaultBucketSize | ||
231 | = pure ( [], map_ns $ PSQ.insert info curTime ) | ||
232 | -- If there are any questionable nodes in the bucket have not been | ||
233 | -- seen in the last 15 minutes, the least recently seen node is | ||
234 | -- pinged. If any nodes in the bucket are known to have become bad, | ||
235 | -- then one is replaced by the new node in the next insertBucket | ||
236 | -- iteration. | ||
237 | | not (L.null stales) | ||
238 | = pure ( [CheckPing stales] | ||
239 | , bucket { -- Update timestamps so that we don't redundantly ping. | ||
240 | bktNodes = updateStamps curTime stales $ bktNodes bucket | ||
241 | -- Update queue with the pending NodeInfo in case of ping fail. | ||
242 | , bktQ = runIdentity $ pushBack bucketQ info $ bktQ bucket } ) | ||
243 | -- When the bucket is full of good nodes, the new node is simply discarded. | ||
244 | -- We must return 'A.empty' here to ensure that bucket splitting happens | ||
245 | -- inside 'modifyBucket'. | ||
246 | | otherwise = A.empty | ||
247 | where | ||
248 | -- We (take 1) to keep a 1-to-1 correspondence between pending pings and | ||
249 | -- waiting nodes in the bktQ. This way, we don't have to worry about what | ||
250 | -- to do with failed pings for which there is no ready replacements. | ||
251 | stales = -- One stale: | ||
252 | do (n :-> t) <- maybeToList $ PSQ.findMin (bktNodes bucket) | ||
253 | guard (t < curTime - delta) | ||
254 | return n | ||
255 | -- All stale: | ||
256 | -- map key $ PSQ.atMost (curTime - delta) $ bktNodes bucket | ||
257 | |||
258 | already_have = maybe False (const True) $ PSQ.lookup info (bktNodes bucket) | ||
259 | |||
260 | map_ns f = bucket { bktNodes = f (bktNodes bucket) } | ||
261 | -- map_q f = bucket { bktQ = runIdentity $ f (bktQ bucket) } | ||
262 | |||
263 | insertBucket curTime (PingResult bad_node got_response) bucket | ||
264 | = pure ([], Bucket (upd $ bktNodes bucket) popped) | ||
265 | where | ||
266 | (top, popped) = runIdentity $ popFront bucketQ (bktQ bucket) | ||
267 | upd | got_response = id | ||
268 | | Just info <- top = \nodes -> | ||
269 | fromMaybe nodes $ do | ||
270 | _ <- PSQ.lookup bad_node nodes -- Insert only if there's a removal. | ||
271 | let nodes' = PSQ.delete bad_node nodes | ||
272 | pure $ PSQ.insert info curTime nodes' | ||
273 | | otherwise = id | ||
274 | |||
275 | updateStamps :: ( Eq ip | ||
276 | , Ord (NodeId dht) | ||
277 | ) => Timestamp -> [NodeInfo dht ip u] -> PSQ (NodeInfo dht ip u) Timestamp -> PSQ (NodeInfo dht ip u) Timestamp | ||
278 | updateStamps curTime stales nodes = foldl' (\q n -> PSQ.insert n curTime q) nodes stales | ||
279 | |||
280 | |||
281 | type BitIx = Word | ||
282 | |||
283 | partitionQ :: Monad f => QueueMethods f elem b -> (elem -> Bool) -> b -> f (b, b) | ||
284 | partitionQ imp test q0 = do | ||
285 | pass0 <- emptyQueue imp | ||
286 | fail0 <- emptyQueue imp | ||
287 | let flipfix a b f = fix f a b | ||
288 | flipfix q0 (pass0,fail0) $ \rec q qs -> do | ||
289 | (mb,q') <- popFront imp q | ||
290 | case mb of | ||
291 | Nothing -> return qs | ||
292 | Just e -> do qs' <- select (pushBack imp e) qs | ||
293 | rec q' qs' | ||
294 | where | ||
295 | select :: Functor f => (b -> f b) -> (b, b) -> f (b, b) | ||
296 | select f = if test e then \(a,b) -> flip (,) b <$> f a | ||
297 | else \(a,b) -> (,) a <$> f b | ||
298 | |||
299 | split :: forall dht ip u. | ||
300 | ( Eq ip | ||
301 | , Ord (NodeId dht) | ||
302 | , FiniteBits (NodeId dht) | ||
303 | ) => BitIx -> Bucket dht ip u -> (Bucket dht ip u, Bucket dht ip u) | ||
304 | split i b = (Bucket ns qs, Bucket ms rs) | ||
305 | where | ||
306 | (ns,ms) = (PSQ.fromList *** PSQ.fromList) . partition (spanBit . key) . PSQ.toList $ bktNodes b | ||
307 | (qs,rs) = runIdentity $ partitionQ bucketQ spanBit $ bktQ b | ||
308 | {- | ||
309 | spanBit :: forall (dht :: * -> *) addr u. | ||
310 | FiniteBits (Network.DatagramServer.Types.NodeId dht) => | ||
311 | NodeInfo dht addr u -> Bool | ||
312 | -} | ||
313 | spanBit :: NodeInfo dht addr u -> Bool | ||
314 | spanBit entry = testIdBit (nodeId entry) i | ||
315 | |||
316 | {----------------------------------------------------------------------- | ||
317 | -- Table | ||
318 | -----------------------------------------------------------------------} | ||
319 | |||
320 | -- | Number of buckets in a routing table. | ||
321 | type BucketCount = Int | ||
322 | |||
323 | defaultBucketCount :: BucketCount | ||
324 | defaultBucketCount = 20 | ||
325 | |||
326 | data Info dht ip u = Info | ||
327 | { myBuckets :: Table dht ip u | ||
328 | , myNodeId :: NodeId dht | ||
329 | , myAddress :: SockAddr | ||
330 | } | ||
331 | deriving Generic | ||
332 | |||
333 | deriving instance (Eq ip, Eq u, Eq (NodeId dht)) => Eq (Info dht ip u) | ||
334 | deriving instance (Show ip, Show u, Show (NodeId dht)) => Show (Info dht ip u) | ||
335 | |||
336 | -- instance (Eq ip, Serialize ip) => Serialize (Info ip) | ||
337 | |||
338 | -- | The routing table covers the entire 'NodeId' space from 0 to 2 ^ | ||
339 | -- 160. The routing table is subdivided into 'Bucket's that each cover | ||
340 | -- a portion of the space. An empty table has one bucket with an ID | ||
341 | -- space range of @min = 0, max = 2 ^ 160@. When a node with ID \"N\" | ||
342 | -- is inserted into the table, it is placed within the bucket that has | ||
343 | -- @min <= N < max@. An empty table has only one bucket so any node | ||
344 | -- must fit within it. Each bucket can only hold 'K' nodes, currently | ||
345 | -- eight, before becoming 'Full'. When a bucket is full of known good | ||
346 | -- nodes, no more nodes may be added unless our own 'NodeId' falls | ||
347 | -- within the range of the 'Bucket'. In that case, the bucket is | ||
348 | -- replaced by two new buckets each with half the range of the old | ||
349 | -- bucket and the nodes from the old bucket are distributed among the | ||
350 | -- two new ones. For a new table with only one bucket, the full bucket | ||
351 | -- is always split into two new buckets covering the ranges @0..2 ^ | ||
352 | -- 159@ and @2 ^ 159..2 ^ 160@. | ||
353 | -- | ||
354 | data Table dht ip u | ||
355 | -- most nearest bucket | ||
356 | = Tip (NodeId dht) BucketCount (Bucket dht ip u) | ||
357 | |||
358 | -- left biased tree branch | ||
359 | | Zero (Table dht ip u) (Bucket dht ip u) | ||
360 | |||
361 | -- right biased tree branch | ||
362 | | One (Bucket dht ip u) (Table dht ip u) | ||
363 | deriving Generic | ||
364 | |||
365 | instance (Eq ip, Eq (NodeId dht)) => Eq (Table dht ip u) where | ||
366 | (==) = (==) `on` Network.DHT.Routing.toList | ||
367 | |||
368 | instance Serialize NominalDiffTime where | ||
369 | put = putWord32be . fromIntegral . fromEnum | ||
370 | get = (toEnum . fromIntegral) <$> getWord32be | ||
371 | |||
372 | deriving instance (Show ip, Show u, Show (NodeId dht)) => Show (Table dht ip u) | ||
373 | |||
374 | -- | Normally, routing table should be saved between invocations of | ||
375 | -- the client software. Note that you don't need to store /this/ | ||
376 | -- 'NodeId' since it is already included in routing table. | ||
377 | instance (Eq ip, Serialize ip, Ord (NodeId dht), Serialize (NodeId dht), Serialize u) => Serialize (Table dht ip u) | ||
378 | |||
379 | -- | Shape of the table. | ||
380 | instance Pretty (Table dht ip u) where | ||
381 | pPrint t | ||
382 | | bucketCount < 6 = hcat $ punctuate ", " $ L.map PP.int ss | ||
383 | | otherwise = brackets $ | ||
384 | PP.int (L.sum ss) <> " nodes, " <> | ||
385 | PP.int bucketCount <> " buckets" | ||
386 | where | ||
387 | bucketCount = L.length ss | ||
388 | ss = shape t | ||
389 | |||
390 | -- | Empty table with specified /spine/ node id. | ||
391 | nullTable :: Eq ip => NodeId dht -> BucketCount -> Table dht ip u | ||
392 | nullTable nid n = Tip nid (bucketCount (pred n)) (Bucket PSQ.empty (runIdentity $ emptyQueue bucketQ)) | ||
393 | where | ||
394 | bucketCount x = max 0 (min 159 x) | ||
395 | |||
396 | -- | Test if table is empty. In this case DHT should start | ||
397 | -- bootstrapping process until table becomes 'full'. | ||
398 | null :: Table dht ip u -> Bool | ||
399 | null (Tip _ _ b) = PSQ.null $ bktNodes b | ||
400 | null _ = False | ||
401 | |||
402 | -- | Test if table have maximum number of nodes. No more nodes can be | ||
403 | -- 'insert'ed, except old ones becomes bad. | ||
404 | full :: Table dht ip u -> Bool | ||
405 | full (Tip _ n _) = n == 0 | ||
406 | full (Zero t b) = PSQ.size (bktNodes b) == defaultBucketSize && full t | ||
407 | full (One b t) = PSQ.size (bktNodes b) == defaultBucketSize && full t | ||
408 | |||
409 | -- | Get the /spine/ node id. | ||
410 | thisId :: Table dht ip u -> NodeId dht | ||
411 | thisId (Tip nid _ _) = nid | ||
412 | thisId (Zero table _) = thisId table | ||
413 | thisId (One _ table) = thisId table | ||
414 | |||
415 | -- | Number of nodes in a bucket or a table. | ||
416 | type NodeCount = Int | ||
417 | |||
418 | -- | Internally, routing table is similar to list of buckets or a | ||
419 | -- /matrix/ of nodes. This function returns the shape of the matrix. | ||
420 | shape :: Table dht ip u -> [BucketSize] | ||
421 | shape = map (PSQ.size . bktNodes) . toBucketList | ||
422 | |||
423 | -- | Get number of nodes in the table. | ||
424 | size :: Table dht ip u -> NodeCount | ||
425 | size = L.sum . shape | ||
426 | |||
427 | -- | Get number of buckets in the table. | ||
428 | depth :: Table dht ip u -> BucketCount | ||
429 | depth = L.length . shape | ||
430 | |||
431 | lookupBucket :: ( FiniteBits (NodeId dht) | ||
432 | ) => NodeId dht -> Table dht ip u -> [Bucket dht ip u] | ||
433 | lookupBucket nid = go 0 [] | ||
434 | where | ||
435 | go i bs (Zero table bucket) | ||
436 | | testIdBit nid i = bucket : toBucketList table ++ bs | ||
437 | | otherwise = go (succ i) (bucket:bs) table | ||
438 | go i bs (One bucket table) | ||
439 | | testIdBit nid i = go (succ i) (bucket:bs) table | ||
440 | | otherwise = bucket : toBucketList table ++ bs | ||
441 | go _ bs (Tip _ _ bucket) = bucket : bs | ||
442 | |||
443 | compatibleNodeId :: forall dht ip u. | ||
444 | ( Serialize (NodeId dht) | ||
445 | , FiniteBits (NodeId dht) | ||
446 | ) => Table dht ip u -> IO (NodeId dht) | ||
447 | compatibleNodeId tbl = genBucketSample prefix br | ||
448 | where | ||
449 | br = bucketRange (L.length (shape tbl) - 1) True | ||
450 | nodeIdSize = finiteBitSize (undefined :: NodeId dht) `div` 8 | ||
451 | bs = BS.pack $ take nodeIdSize $ tablePrefix tbl ++ repeat 0 | ||
452 | prefix = either error id $ S.decode bs | ||
453 | |||
454 | tablePrefix :: Table dht ip u -> [Word8] | ||
455 | tablePrefix = map (packByte . take 8 . (++repeat False)) | ||
456 | . chunksOf 8 | ||
457 | . tableBits | ||
458 | where | ||
459 | packByte = foldl1' (.|.) . zipWith bitmask [7,6 .. 0] | ||
460 | bitmask ix True = bit ix | ||
461 | bitmask _ _ = 0 | ||
462 | |||
463 | tableBits :: Table dht ip u -> [Bool] | ||
464 | tableBits (One _ tbl) = True : tableBits tbl | ||
465 | tableBits (Zero tbl _) = False : tableBits tbl | ||
466 | tableBits (Tip _ _ _) = [] | ||
467 | |||
468 | chunksOf :: Int -> [e] -> [[e]] | ||
469 | chunksOf i ls = map (take i) (build (splitter ls)) where | ||
470 | splitter :: [e] -> ([e] -> a -> a) -> a -> a | ||
471 | splitter [] _ n = n | ||
472 | splitter l c n = l `c` splitter (drop i l) c n | ||
473 | |||
474 | build :: ((a -> [a] -> [a]) -> [a] -> [a]) -> [a] | ||
475 | build g = g (:) [] | ||
476 | |||
477 | -- | Count of closest nodes in find_node request. | ||
478 | type K = Int | ||
479 | |||
480 | -- | Default 'K' is equal to 'defaultBucketSize'. | ||
481 | defaultK :: K | ||
482 | defaultK = 8 | ||
483 | |||
484 | class TableKey dht k where | ||
485 | toNodeId :: k -> NodeId dht | ||
486 | |||
487 | instance TableKey dht (NodeId dht) where | ||
488 | toNodeId = id | ||
489 | |||
490 | -- | Get a list of /K/ closest nodes using XOR metric. Used in | ||
491 | -- 'find_node' and 'get_peers' queries. | ||
492 | kclosest :: ( Eq ip | ||
493 | , Ord (NodeId dht) | ||
494 | , FiniteBits (NodeId dht) | ||
495 | ) => TableKey dht a => K -> a -> Table dht ip u -> [NodeInfo dht ip u] | ||
496 | kclosest k (toNodeId -> nid) tbl = take k $ rank nodeId nid (L.concat bucket) | ||
497 | ++ rank nodeId nid (L.concat everyone) | ||
498 | where | ||
499 | (bucket,everyone) = | ||
500 | L.splitAt 1 | ||
501 | . L.map (L.map PSQ.key . PSQ.toList . bktNodes) | ||
502 | . lookupBucket nid | ||
503 | $ tbl | ||
504 | |||
505 | {----------------------------------------------------------------------- | ||
506 | -- Routing | ||
507 | -----------------------------------------------------------------------} | ||
508 | |||
509 | splitTip :: ( Eq ip | ||
510 | , Ord (NodeId dht) | ||
511 | , FiniteBits (NodeId dht) | ||
512 | ) => NodeId dht -> BucketCount -> BitIx -> Bucket dht ip u -> Table dht ip u | ||
513 | splitTip nid n i bucket | ||
514 | | testIdBit nid i = (One zeros (Tip nid (pred n) ones)) | ||
515 | | otherwise = (Zero (Tip nid (pred n) zeros) ones) | ||
516 | where | ||
517 | (ones, zeros) = split i bucket | ||
518 | |||
519 | -- | Used in each query. | ||
520 | -- | ||
521 | -- TODO: Kademlia non-empty subtrees should should split if they have less than | ||
522 | -- k nodes in them. Which subtrees I mean is illustrated in Fig 1. of Kademlia | ||
523 | -- paper. The rule requiring additional splits is in section 2.4. | ||
524 | modifyBucket | ||
525 | :: forall xs dht ip u. | ||
526 | ( Eq ip | ||
527 | , Ord (NodeId dht) | ||
528 | , FiniteBits (NodeId dht) | ||
529 | ) => | ||
530 | NodeId dht -> (Bucket dht ip u -> Maybe (xs, Bucket dht ip u)) -> Table dht ip u -> Maybe (xs,Table dht ip u) | ||
531 | modifyBucket nodeId f = go (0 :: BitIx) | ||
532 | where | ||
533 | go :: BitIx -> Table dht ip u -> Maybe (xs, Table dht ip u) | ||
534 | go !i (Zero table bucket) | ||
535 | | testIdBit nodeId i = second (Zero table) <$> f bucket | ||
536 | | otherwise = second (`Zero` bucket) <$> go (succ i) table | ||
537 | go !i (One bucket table ) | ||
538 | | testIdBit nodeId i = second (One bucket) <$> go (succ i) table | ||
539 | | otherwise = second (`One` table) <$> f bucket | ||
540 | go !i (Tip nid n bucket) | ||
541 | | n == 0 = second (Tip nid n) <$> f bucket | ||
542 | | otherwise = second (Tip nid n) <$> f bucket | ||
543 | <|> go i (splitTip nid n i bucket) | ||
544 | |||
545 | -- | Triggering event for atomic table update | ||
546 | data Event dht ip u = TryInsert { foreignNode :: NodeInfo dht ip u } | ||
547 | | PingResult { foreignNode :: NodeInfo dht ip u | ||
548 | , ponged :: Bool | ||
549 | } | ||
550 | deriving instance Eq (NodeId dht) => Eq (Event dht ip u) | ||
551 | deriving instance ( Show ip | ||
552 | , Show (NodeId dht) | ||
553 | , Show u | ||
554 | ) => Show (Event dht ip u) | ||
555 | |||
556 | eventId :: Event dht ip u -> NodeId dht | ||
557 | eventId (TryInsert NodeInfo{..}) = nodeId | ||
558 | eventId (PingResult NodeInfo{..} _) = nodeId | ||
559 | |||
560 | -- | Actions requested by atomic table update | ||
561 | data CheckPing dht ip u = CheckPing [NodeInfo dht ip u] | ||
562 | |||
563 | deriving instance Eq (NodeId dht) => Eq (CheckPing dht ip u) | ||
564 | deriving instance ( Show ip | ||
565 | , Show (NodeId dht) | ||
566 | , Show u | ||
567 | ) => Show (CheckPing dht ip u) | ||
568 | |||
569 | |||
570 | -- | Atomic 'Table' update | ||
571 | insert :: ( Eq ip | ||
572 | , Applicative m | ||
573 | , Ord (NodeId dht) | ||
574 | , FiniteBits (NodeId dht) | ||
575 | ) => Timestamp -> Event dht ip u -> Table dht ip u -> m ([CheckPing dht ip u], Table dht ip u) | ||
576 | insert tm event tbl = pure $ fromMaybe ([],tbl) $ modifyBucket (eventId event) (insertBucket tm event) tbl | ||
577 | |||
578 | |||
579 | {----------------------------------------------------------------------- | ||
580 | -- Conversion | ||
581 | -----------------------------------------------------------------------} | ||
582 | |||
583 | type TableEntry dht ip u = (NodeInfo dht ip u, Timestamp) | ||
584 | |||
585 | tableEntry :: NodeEntry dht ip u -> TableEntry dht ip u | ||
586 | tableEntry (a :-> b) = (a, b) | ||
587 | |||
588 | -- | Non-empty list of buckets. | ||
589 | toBucketList :: Table dht ip u -> [Bucket dht ip u] | ||
590 | toBucketList (Tip _ _ b) = [b] | ||
591 | toBucketList (Zero t b) = b : toBucketList t | ||
592 | toBucketList (One b t) = b : toBucketList t | ||
593 | |||
594 | toList :: Eq ip => Table dht ip u -> [[TableEntry dht ip u]] | ||
595 | toList = L.map (L.map tableEntry . PSQ.toList . bktNodes) . toBucketList | ||