haskoin-node-1.1.3: src/Haskoin/Node/PeerMgr.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE NoFieldSelectors #-}
module Haskoin.Node.PeerMgr
( PeerMgrConfig (..),
PeerEvent (..),
OnlinePeer (..),
PeerMgr,
withPeerMgr,
peerMgrBest,
peerMgrVersion,
peerMgrPing,
peerMgrPong,
peerMgrAddrs,
peerMgrVerAck,
getPeers,
getOnlinePeer,
ticklePeer,
buildVersion,
myVersion,
toSockAddr,
toHostService,
)
where
import Control.Applicative ((<|>))
import Control.Arrow
import Control.Monad
( forM_,
forever,
guard,
unless,
void,
when,
(<=<),
)
import Control.Monad.Except
( ExceptT (..),
runExceptT,
throwError,
)
import Control.Monad.Logger
( MonadLogger,
MonadLoggerIO,
logDebugS,
logErrorS,
logInfoS,
logWarnS,
)
import Control.Monad.Reader
( MonadReader,
ReaderT (ReaderT),
ask,
asks,
runReaderT,
)
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import Data.Bits ((.&.))
import Data.Function (on)
import Data.List (dropWhileEnd, elemIndex, find, nub, sort)
import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.String.Conversions (cs)
import Data.Time.Clock
( NominalDiffTime,
UTCTime,
addUTCTime,
diffUTCTime,
getCurrentTime,
)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.Word (Word32, Word64)
import Haskoin
( BlockHeight,
Message (..),
Network (..),
NetworkAddress (..),
Ping (..),
Pong (..),
VarString (..),
Version (..),
hostToSockAddr,
nodeNetwork,
sockToHostAddress,
)
import Haskoin.Node.Peer
import NQE
( Child,
Inbox,
Mailbox,
Publisher,
Strategy (..),
Supervisor,
addChild,
inboxToMailbox,
newInbox,
newMailbox,
publish,
receive,
receiveMatch,
send,
sendSTM,
withSupervisor,
)
import Network.Socket
( AddrInfo (..),
AddrInfoFlag (..),
Family (..),
SockAddr (..),
SocketType (..),
defaultHints,
getAddrInfo,
)
import System.Random (randomIO, randomRIO)
import UnliftIO
( Async,
MonadIO,
MonadUnliftIO,
STM,
SomeException,
TVar,
atomically,
catch,
liftIO,
link,
modifyTVar,
newTVarIO,
readTVar,
readTVarIO,
withAsync,
withRunInIO,
writeTVar,
)
import UnliftIO.Concurrent (threadDelay)
type MonadManager m = (MonadIO m, MonadReader PeerMgr m)
data PeerMgrConfig = PeerMgrConfig
{ maxPeers :: !Int,
peers :: ![String],
discover :: !Bool,
address :: !NetworkAddress,
net :: !Network,
pub :: !(Publisher PeerEvent),
timeout :: !NominalDiffTime,
maxPeerLife :: !NominalDiffTime,
connect :: !(SockAddr -> WithConnection)
}
data PeerMgr = PeerMgr
{ config :: !PeerMgrConfig,
supervisor :: !Supervisor,
mailbox :: !(Mailbox PeerMgrMessage),
best :: !(TVar BlockHeight),
addresses :: !(TVar (Set SockAddr)),
peers :: !(TVar [OnlinePeer])
}
data PeerMgrMessage
= Connect !SockAddr
| CheckPeer !Peer
| PeerDied !Child !(Maybe SomeException)
| ManagerBest !BlockHeight
| PeerVerAck !Peer
| PeerVersion !Peer !Version
| PeerPing !Peer !Word64
| PeerPong !Peer !Word64
| PeerAddrs !Peer ![NetworkAddress]
-- | Data structure representing an online peer.
data OnlinePeer = OnlinePeer
{ address :: !SockAddr,
verack :: !Bool,
online :: !Bool,
version :: !(Maybe Version),
async :: !(Async ()),
mailbox :: !Peer,
nonce :: !Word64,
ping :: !(Maybe (UTCTime, Word64)),
pings :: ![NominalDiffTime],
connected :: !UTCTime,
tickled :: !UTCTime
}
instance Eq OnlinePeer where
(==) = (==) `on` f
where
f OnlinePeer {mailbox = p} = p
instance Ord OnlinePeer where
compare = compare `on` f
where
f OnlinePeer {pings = pings} = fromMaybe 60 (median pings)
withPeerMgr ::
(MonadUnliftIO m, MonadLoggerIO m) =>
PeerMgrConfig ->
(PeerMgr -> m a) ->
m a
withPeerMgr cfg action = do
inbox <- newInbox
let mgr = inboxToMailbox inbox
withSupervisor (Notify (death mgr)) $ \sup -> do
bb <- newTVarIO 0
kp <- newTVarIO Set.empty
ob <- newTVarIO []
runReaderT
(go inbox)
PeerMgr
{ config = cfg,
supervisor = sup,
mailbox = mgr,
best = bb,
addresses = kp,
peers = ob
}
where
death mgr (a, ex) = PeerDied a ex `sendSTM` mgr
go inbox =
withAsync (peerManager inbox) $ \a ->
withConnectLoop $
link a >> ReaderT action
peerManager ::
( MonadUnliftIO m,
MonadManager m,
MonadLoggerIO m
) =>
Inbox PeerMgrMessage ->
m ()
peerManager inb = do
$(logDebugS) "PeerMgr" "Awaiting best block"
putBestBlock <=< receiveMatch inb $ \case
ManagerBest b -> Just b
_ -> Nothing
$(logDebugS) "PeerMgr" "Starting peer manager actor"
forever $ do
$(logDebugS) "PeerMgr" "Awaiting event..."
dispatch =<< receive inb
putBestBlock :: (MonadManager m) => BlockHeight -> m ()
putBestBlock bb = do
b <- asks (.best)
atomically $ writeTVar b bb
getBestBlock :: (MonadManager m) => m BlockHeight
getBestBlock =
asks (.best) >>= readTVarIO
getNetwork :: (MonadManager m) => m Network
getNetwork =
asks (.config.net)
loadPeers :: (MonadUnliftIO m, MonadManager m) => m ()
loadPeers = do
loadStaticPeers
loadNetSeeds
loadStaticPeers :: (MonadUnliftIO m, MonadManager m) => m ()
loadStaticPeers = do
net <- asks (.config.net)
xs <- asks (.config.peers)
mapM_ newPeer . concat =<< mapM (toSockAddr net) xs
loadNetSeeds :: (MonadUnliftIO m, MonadManager m) => m ()
loadNetSeeds =
asks (.config.discover) >>= \discover ->
when discover $ do
net <- getNetwork
ss <- concat <$> mapM (toSockAddr net) net.seeds
mapM_ newPeer ss
logConnectedPeers :: (MonadManager m, MonadLoggerIO m) => m ()
logConnectedPeers = do
m <- asks (.config.maxPeers)
l <- length <$> getConnectedPeers
$(logInfoS) "PeerMgr" $
"Peers connected: " <> cs (show l) <> "/" <> cs (show m)
getOnlinePeers :: (MonadManager m) => m [OnlinePeer]
getOnlinePeers =
asks (.peers) >>= readTVarIO
getConnectedPeers :: (MonadManager m) => m [OnlinePeer]
getConnectedPeers =
filter (.online) <$> getOnlinePeers
managerEvent :: (MonadManager m) => PeerEvent -> m ()
managerEvent e =
publish e =<< asks (.config.pub)
dispatch ::
( MonadUnliftIO m,
MonadManager m,
MonadLoggerIO m
) =>
PeerMgrMessage ->
m ()
dispatch (PeerVersion p v) = do
$(logDebugS) "PeerMgr" $
"Received peer " <> p.label <> " version: " <> cs (show v)
b <- asks (.peers)
e <- runExceptT $ do
o <- ExceptT . atomically $ setPeerVersion b p v
when o.online $ announcePeer p
case e of
Right () -> do
$(logDebugS) "PeerMgr" $
"Sending version ack to peer: " <> p.label
MVerAck `sendMessage` p
Left x -> do
$(logErrorS) "PeerMgr" $
"Version rejected for peer "
<> p.label
<> ": "
<> cs (show x)
killPeer x p
dispatch (PeerVerAck p) = do
b <- asks (.peers)
atomically (setPeerVerAck b p) >>= \case
Just o -> do
$(logDebugS) "PeerMgr" $
"Received version ack from peer: "
<> p.label
when o.online $
announcePeer p
Nothing -> do
$(logErrorS) "PeerMgr" $
"Received verack from unknown peer: "
<> p.label
killPeer UnknownPeer p
dispatch (PeerAddrs p nas) = do
$(logDebugS) "PeerMgr" $
"Received addresses from peer " <> p.label
discover <- asks (.config.discover)
when discover $ do
let sas = map (hostToSockAddr . (.address)) nas
forM_ (zip [(1 :: Int) ..] sas) $ \(i, a) -> do
$(logDebugS) "PeerMgr" $
"Got peer address "
<> cs (show i)
<> "/"
<> cs (show (length sas))
<> ": "
<> cs (show a)
<> " from peer "
<> p.label
newPeer a
dispatch (PeerPong p n) = do
b <- asks (.peers)
$(logDebugS) "PeerMgr" $
"Received pong "
<> cs (show n)
<> " from: "
<> p.label
now <- liftIO getCurrentTime
atomically (gotPong b n now p)
dispatch (PeerPing p n) = do
$(logDebugS) "PeerMgr" $
"Responding to ping "
<> cs (show n)
<> " from: "
<> p.label
MPong (Pong n) `sendMessage` p
dispatch (ManagerBest h) = do
$(logDebugS) "PeerMgr" $
"Setting best block to " <> cs (show h)
putBestBlock h
dispatch (Connect sa) = do
connectPeer sa
dispatch (PeerDied a e) = do
processPeerOffline a e
dispatch (CheckPeer p) = do
$(logDebugS) "PeerMgr" $
"Housekeeping for peer " <> p.label
checkPeer p
ticklePeer :: ( MonadLoggerIO m) => PeerMgr -> Peer -> m ()
ticklePeer m p = do
$(logDebugS) "PeerMgr" $ "Tickle peer " <> p.label
t <- liftIO getCurrentTime
atomically $ modifyPeer m.peers p $ \o -> o {tickled = t}
checkPeer :: (MonadManager m, MonadLoggerIO m) => Peer -> m ()
checkPeer p = do
busy <- getBusy p
b <- asks (.peers)
mp <- asks (.peers) >>= atomically . flip findPeer p
case mp of
Nothing -> return ()
Just o -> do
now <- liftIO getCurrentTime
maxLife <- asks (.config.maxPeerLife)
let expired = maxLife `addUTCTime` o.connected
timeout <- asks (.config.timeout)
let deadline = timeout `addUTCTime` o.tickled
if
| now > expired -> do
$(logErrorS) "PeerMgr" $
"Disconnecting old peer "
<> p.label
<> " online since "
<> cs (show o.connected)
killPeer PeerTooOld p
| now > deadline -> do
$(logWarnS) "PeerMgr" $ "Peer timeout: " <> p.label
killPeer PeerTimeout p
| isNothing o.ping ->
sendPing p
| otherwise -> return ()
sendPing :: (MonadManager m, MonadLoggerIO m) => Peer -> m ()
sendPing p = do
b <- asks (.peers)
atomically (findPeer b p) >>= \case
Nothing ->
$(logWarnS) "PeerMgr" $
"Will not ping unknown peer: " <> p.label
Just o
| o.online -> do
n <- liftIO randomIO
now <- liftIO getCurrentTime
atomically (setPeerPing b n now p)
$(logDebugS) "PeerMgr" $
"Sending ping "
<> cs (show n)
<> " to: "
<> p.label
MPing (Ping n) `sendMessage` p
| otherwise -> return ()
processPeerOffline ::
(MonadManager m, MonadLoggerIO m) =>
Child ->
Maybe SomeException ->
m ()
processPeerOffline a e = do
b <- asks (.peers)
atomically (findPeerAsync b a) >>= \case
Nothing -> log_unknown e
Just o -> do
let p = o.mailbox
if o.online
then do
log_disconnected p e
managerEvent $ PeerDisconnected p
else log_not_connect p e
atomically $ removePeer b p
logConnectedPeers
where
log_unknown Nothing =
$(logErrorS)
"PeerMgr"
"Disconnected unknown peer"
log_unknown (Just x) =
$(logErrorS) "PeerMgr" $
"Unknown peer died: " <> cs (show x)
log_disconnected p Nothing =
$(logWarnS) "PeerMgr" $
"Disconnected peer: " <> p.label
log_disconnected p (Just x) =
$(logErrorS) "PeerMgr" $
"Peer " <> p.label <> " died: " <> cs (show x)
log_not_connect p Nothing =
$(logWarnS) "PeerMgr" $
"Could not connect to peer " <> p.label
log_not_connect p (Just x) =
$(logErrorS) "PeerMgr" $
"Could not connect to peer "
<> p.label
<> ": "
<> cs (show x)
announcePeer :: (MonadManager m, MonadLoggerIO m) => Peer -> m ()
announcePeer p = do
b <- asks (.peers)
atomically (findPeer b p) >>= \case
Just OnlinePeer {online = True} -> do
$(logInfoS) "PeerMgr" $
"Connected to peer " <> p.label
managerEvent $ PeerConnected p
logConnectedPeers
Just OnlinePeer {online = False} ->
return ()
Nothing ->
$(logErrorS) "PeerMgr" $
"Not announcing disconnected peer: "
<> p.label
getNewPeer :: (MonadUnliftIO m, MonadManager m) => m (Maybe SockAddr)
getNewPeer =
runMaybeT $ lift loadPeers >> go
where
go = do
b <- asks (.addresses)
ks <- readTVarIO b
guard . not $ Set.null ks
let xs = Set.toList ks
a <- liftIO $ randomRIO (0, length xs - 1)
let p = xs !! a
o <- asks (.peers)
m <- atomically $ do
modifyTVar b $ Set.delete p
findPeerAddress o p
maybe (return p) (const go) m
connectPeer ::
( MonadUnliftIO m,
MonadManager m,
MonadLoggerIO m
) =>
SockAddr ->
m ()
connectPeer sa = do
os <- asks (.peers)
atomically (findPeerAddress os sa) >>= \case
Just _ ->
$(logErrorS) "PeerMgr" $
"Attempted to connect to peer twice: " <> cs (show sa)
Nothing -> do
$(logInfoS) "PeerMgr" $ "Connecting to " <> cs (show sa)
PeerMgrConfig
{ address = ad,
net = net
} <-
asks (.config)
sup <- asks (.supervisor)
conn <- asks (.config.connect)
pub <- asks (.config.pub)
nonce <- liftIO randomIO
bb <- getBestBlock
now <- liftIO getCurrentTime
let rmt = NetworkAddress (srv net) (sockToHostAddress sa)
unix = floor (utcTimeToPOSIXSeconds now)
ver = buildVersion net nonce bb ad rmt unix
text = cs (show sa)
(inbox, mailbox) <- newMailbox
let pc =
PeerConfig
{ pub = pub,
net = net,
label = text,
connect = conn sa
}
busy <- newTVarIO False
p <- wrapPeer pc busy mailbox
a <- withRunInIO $ \io ->
sup `addChild` io (launch pc busy inbox p)
MVersion ver `sendMessage` p
b <- asks (.peers)
atomically $
insertPeer
b
OnlinePeer
{ address = sa,
verack = False,
online = False,
version = Nothing,
async = a,
mailbox = p,
nonce = nonce,
pings = [],
ping = Nothing,
connected = now,
tickled = now
}
where
srv net
| net.segWit = 8
| otherwise = 0
launch pc busy inbox p =
ask >>= \mgr ->
withPeerLoop p mgr $ \a ->
link a >> peer pc busy inbox
withPeerLoop ::
(MonadUnliftIO m, MonadLogger m) =>
Peer ->
PeerMgr ->
(Async a -> m a) ->
m a
withPeerLoop p mgr =
withAsync . forever $ do
let timeout = mgr.config.timeout
ms = floor (timeout * 1000 * 1000)
r <- liftIO $ randomRIO (ms `div` 4, ms `div` 2)
threadDelay r
managerCheck p mgr
withConnectLoop ::
(MonadUnliftIO m, MonadManager m) =>
m a ->
m a
withConnectLoop act =
withAsync go $ \a ->
link a >> act
where
go = forever $ do
l <- length <$> getOnlinePeers
x <- asks (.config.maxPeers)
when (l < x) $
getNewPeer >>= mapM_ (\sa -> ask >>= managerConnect sa)
delay <-
liftIO $
randomRIO
( 100 * 1000,
10 * 500 * 1000
)
threadDelay delay
newPeer :: (MonadIO m, MonadManager m) => SockAddr -> m ()
newPeer sa = do
b <- asks (.addresses)
o <- asks (.peers)
atomically $
findPeerAddress o sa >>= \case
Just _ -> return ()
Nothing -> modifyTVar b $ Set.insert sa
gotPong :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM ()
gotPong b nonce now p = void . runMaybeT $ do
o <- MaybeT (findPeer b p)
(time, old_nonce) <- MaybeT (return o.ping)
guard $ nonce == old_nonce
let diff = now `diffUTCTime` time
lift $
insertPeer
b
o
{ ping = Nothing,
pings = sort $ take 11 $ diff : o.pings
}
setPeerPing :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM ()
setPeerPing b nonce now p =
modifyPeer b p $ \o -> o {ping = Just (now, nonce)}
setPeerVersion ::
TVar [OnlinePeer] ->
Peer ->
Version ->
STM (Either PeerException OnlinePeer)
setPeerVersion b p v = runExceptT $ do
when (v.services .&. nodeNetwork == 0) $
throwError NotNetworkPeer
ops <- lift $ readTVar b
when (any ((v.nonce ==) . (.nonce)) ops) $
throwError PeerIsMyself
lift (findPeer b p) >>= \case
Nothing -> throwError UnknownPeer
Just o -> do
let n =
o
{ version = Just v,
online = o.verack
}
lift $ insertPeer b n
return n
setPeerVerAck :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer)
setPeerVerAck b p = runMaybeT $ do
o <- MaybeT $ findPeer b p
let n =
o
{ verack = True,
online = isJust o.version
}
lift $ insertPeer b n
return n
findPeer :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer)
findPeer b p =
find ((== p) . (.mailbox))
<$> readTVar b
insertPeer :: TVar [OnlinePeer] -> OnlinePeer -> STM ()
insertPeer b o =
modifyTVar b $ \x -> sort . nub $ o : x
modifyPeer ::
TVar [OnlinePeer] ->
Peer ->
(OnlinePeer -> OnlinePeer) ->
STM ()
modifyPeer b p f =
findPeer b p >>= \case
Nothing -> return ()
Just o -> insertPeer b $ f o
removePeer :: TVar [OnlinePeer] -> Peer -> STM ()
removePeer b p =
modifyTVar b $
filter ((/= p) . (.mailbox))
findPeerAsync ::
TVar [OnlinePeer] ->
Async () ->
STM (Maybe OnlinePeer)
findPeerAsync b a =
find ((== a) . (.async))
<$> readTVar b
findPeerAddress ::
TVar [OnlinePeer] ->
SockAddr ->
STM (Maybe OnlinePeer)
findPeerAddress b a =
find ((== a) . (.address))
<$> readTVar b
getPeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer]
getPeers = runReaderT getConnectedPeers
getOnlinePeer ::
(MonadIO m) =>
Peer ->
PeerMgr ->
m (Maybe OnlinePeer)
getOnlinePeer p =
runReaderT $ asks (.peers) >>= atomically . (`findPeer` p)
managerCheck :: (MonadIO m) => Peer -> PeerMgr -> m ()
managerCheck p mgr =
CheckPeer p `send` mgr.mailbox
managerConnect :: (MonadIO m) => SockAddr -> PeerMgr -> m ()
managerConnect sa mgr =
Connect sa `send` mgr.mailbox
peerMgrBest :: (MonadIO m) => BlockHeight -> PeerMgr -> m ()
peerMgrBest bh mgr =
ManagerBest bh `send` mgr.mailbox
peerMgrVerAck :: (MonadIO m) => Peer -> PeerMgr -> m ()
peerMgrVerAck p mgr =
PeerVerAck p `send` mgr.mailbox
peerMgrVersion ::
(MonadIO m) =>
Peer ->
Version ->
PeerMgr ->
m ()
peerMgrVersion p ver mgr =
PeerVersion p ver `send` mgr.mailbox
peerMgrPing ::
(MonadIO m) =>
Peer ->
Word64 ->
PeerMgr ->
m ()
peerMgrPing p nonce mgr =
PeerPing p nonce `send` mgr.mailbox
peerMgrPong ::
(MonadIO m) =>
Peer ->
Word64 ->
PeerMgr ->
m ()
peerMgrPong p nonce mgr =
PeerPong p nonce `send` mgr.mailbox
peerMgrAddrs ::
(MonadIO m) =>
Peer ->
[NetworkAddress] ->
PeerMgr ->
m ()
peerMgrAddrs p addrs mgr =
PeerAddrs p addrs `send` mgr.mailbox
toHostService :: String -> (Maybe String, Maybe String)
toHostService str =
let host = case m6 of
Just (x, _) -> Just x
Nothing -> case takeWhile (/= ':') str of
[] -> Nothing
xs -> Just xs
srv = case m6 of
Just (_, y) -> s y
Nothing -> s str
s xs =
case dropWhile (/= ':') xs of
[] -> Nothing
_ : ys -> Just ys
m6 = case str of
(x : xs)
| x == '[' -> do
i <- elemIndex ']' xs
return $ second tail $ splitAt i xs
| x == ':' -> do
return (str, "")
_ -> Nothing
in (host, srv)
toSockAddr :: (MonadUnliftIO m) => Network -> String -> m [SockAddr]
toSockAddr net str =
go `catch` e
where
go = fmap (map addrAddress) $ liftIO $ getAddrInfo Nothing host srv
(host, srv) =
second (<|> Just (show net.defaultPort)) $
toHostService str
e :: (Monad m) => SomeException -> m [SockAddr]
e _ = return []
median :: (Ord a, Fractional a) => [a] -> Maybe a
median ls
| null ls =
Nothing
| even (length ls) =
Just . (/ 2) . sum . take 2 $
drop (length ls `div` 2 - 1) ls'
| otherwise =
Just (ls' !! (length ls `div` 2))
where
ls' = sort ls
buildVersion ::
Network ->
Word64 ->
BlockHeight ->
NetworkAddress ->
NetworkAddress ->
Word64 ->
Version
buildVersion net nonce height loc rmt time =
Version
{ version = myVersion,
services = loc.services,
timestamp = time,
addrRecv = rmt,
addrSend = loc,
nonce = nonce,
userAgent = VarString net.userAgent,
startHeight = height,
relay = True
}
myVersion :: Word32
myVersion = 70012