haskoin-node-2.4.0: src/Haskoin/Node/PeerMgr/Lifecycle.hs
-- |
-- Module : Haskoin.Node.PeerMgr.Lifecycle
-- Copyright : (c) Haskoin Developers
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- The life of a connection to a peer, as an explicit state machine: the
-- handshake, then keeping the peer alive with pings, then disconnecting.
--
-- "Haskoin.Node.PeerMgr" keeps a 'PeerState' for every connection. It feeds
-- 'stepPeer' what happens (a message arrived, a pong came back, the clock
-- ticked) and carries out the 'PeerAction's that come back.
--
-- = Why it works this way
--
-- * __Every stage is named.__ A connection is 'Handshaking', 'Online' or
-- 'Disconnecting'. Before, a peer that had been told to disconnect looked
-- exactly like an online one until its process died: it was still offered
-- to applications, still had its messages routed, and could be told to
-- disconnect again. 'Disconnecting' makes that window explicit, and every
-- input in it is ignored.
--
-- * __Time is an input.__ The rules about silence, pings and age depend on
-- the clock. Instead of reading it, 'stepPeer' takes the time in 'Tick',
-- 'Heard' and 'GotPong'. Every timing rule can then be tested by passing
-- times, without waiting.
--
-- * __Randomness is an input.__ A ping needs a fresh nonce. 'Tick' carries
-- one, so that the transition stays a pure function.
--
-- * __One reason per disconnection.__ 'DisconnectReason' records why the node
-- gave up on a peer, so that the peer manager can decide whether to try the
-- address again soon, later, or not for a long time (see
-- "Haskoin.Node.PeerMgr.Addresses").
module Haskoin.Node.PeerMgr.Lifecycle
( PeerState (..),
Stage (..),
Limits (..),
PeerInput (..),
PeerAction (..),
DisconnectReason (..),
newPeerState,
stepPeer,
isOnline,
stageVersion,
pendingPing,
)
where
import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime)
import Data.Word (Word64)
import Haskoin (Version)
import Haskoin.Node.PeerMgr.Handshake
-- | Stage of a connection.
data Stage
= -- | the version handshake is in progress
Handshaking !Handshake
| -- | the handshake is complete; holds the version of the peer and the ping
-- awaiting an answer, with the time it was sent and its nonce
Online !Version !(Maybe (UTCTime, Word64))
| -- | the peer was told to disconnect, for the given reason
Disconnecting !DisconnectReason
deriving stock (Eq, Show)
-- | Why the node disconnects from a peer.
data DisconnectReason
= -- | the peer broke the handshake rules, or its version was refused
Rejected !HandshakeError
| -- | the handshake did not complete in time
HandshakeTimeout
| -- | the peer sent nothing for too long
Silent
| -- | the peer did not answer a ping in time
NoPong
| -- | the peer has been connected for the maximum time; connections rotate
TooOld
deriving stock (Eq, Show)
-- | State of a connection.
data PeerState = PeerState
{ -- | stage of the connection
stage :: !Stage,
-- | when the connection was opened
connected :: !UTCTime,
-- | when the peer last sent a message
lastHeard :: !UTCTime,
-- | round-trip times of the last eleven pings, newest first
pings :: ![NominalDiffTime],
-- | whether the peer was announced as connected, so that its
-- disconnection must be announced too
announced :: !Bool
}
deriving stock (Eq, Show)
-- | Time limits for a connection.
data Limits = Limits
{ -- | longest silence, unanswered ping or handshake
timeout :: !NominalDiffTime,
-- | longest connection, after which it is rotated
maxLife :: !NominalDiffTime
}
deriving stock (Eq, Show)
-- | What happened to a connection.
data PeerInput
= -- | the peer sent a message at the given time
Heard !UTCTime
| -- | the peer sent a handshake message
GotHandshake !HandshakeEvent
| -- | the version of the peer was refused (see 'checkVersion')
Refused !HandshakeError
| -- | the peer answered a ping with the given nonce at the given time
GotPong !UTCTime !Word64
| -- | the clock ticked; holds the time, and a nonce for a ping if one is due
Tick !UTCTime !Word64
deriving stock (Eq, Show)
-- | What the peer manager must do after a transition.
data PeerAction
= -- | acknowledge the version of the peer
ReplyVerAck
| -- | publish that the peer is online
Announce
| -- | send a ping with the given nonce
SendPing !Word64
| -- | tell the peer process to disconnect
Disconnect !DisconnectReason
deriving stock (Eq, Show)
-- | A connection opened at the given time, with our version sent.
newPeerState :: UTCTime -> PeerState
newPeerState now =
PeerState
{ stage = Handshaking AwaitingBoth,
connected = now,
lastHeard = now,
pings = [],
announced = False
}
-- | Apply an input to a connection.
--
-- On a 'Tick', the checks come in this order: the connection is too old, the
-- peer has been silent too long, the handshake took too long, a ping went
-- unanswered too long. If none applies and no ping is pending, a ping is sent.
stepPeer :: Limits -> PeerInput -> PeerState -> (PeerState, [PeerAction])
stepPeer limits input st =
case (st.stage, input) of
(Disconnecting _, _) -> (st, [])
(_, Heard t) -> (st {lastHeard = t}, [])
(_, Refused e) -> disconnect (Rejected e)
(Handshaking h, GotHandshake e) -> handshake h e
(Online _ _, GotHandshake (GotVersion _)) -> disconnect (Rejected DuplicateVersion)
(Online _ _, GotHandshake GotVerAck) -> disconnect (Rejected DuplicateVerAck)
(Online v (Just (sent, n)), GotPong t n')
| n == n' ->
( st
{ stage = Online v Nothing,
pings = take 11 (t `diffUTCTime` sent : st.pings)
},
[]
)
(_, GotPong _ _) -> (st, [])
(stage, Tick now nonce)
| now `since` st.connected > limits.maxLife -> disconnect TooOld
| now `since` st.lastHeard > limits.timeout -> disconnect Silent
| Handshaking _ <- stage,
now `since` st.connected > limits.timeout ->
disconnect HandshakeTimeout
| Online _ (Just (sent, _)) <- stage,
now `since` sent > limits.timeout ->
disconnect NoPong
| Online v Nothing <- stage ->
(st {stage = Online v (Just (now, nonce))}, [SendPing nonce])
| otherwise -> (st, [])
where
since = diffUTCTime
disconnect r = (st {stage = Disconnecting r}, [Disconnect r])
handshake h e =
case stepHandshake h e of
Left err -> disconnect (Rejected err)
Right (Established v, acts) ->
(st {stage = Online v Nothing, announced = True}, map action acts)
Right (h', acts) -> (st {stage = Handshaking h'}, map action acts)
action = \case
SendVerAck -> ReplyVerAck
AnnounceConnected -> Announce
-- | Whether the handshake is complete and the peer is not being disconnected.
isOnline :: PeerState -> Bool
isOnline st = case st.stage of
Online _ _ -> True
_ -> False
-- | Version of the peer, once received.
stageVersion :: PeerState -> Maybe Version
stageVersion st = case st.stage of
Handshaking h -> handshakeVersion h
Online v _ -> Just v
Disconnecting _ -> Nothing
-- | The ping awaiting an answer: when it was sent, and its nonce.
pendingPing :: PeerState -> Maybe (UTCTime, Word64)
pendingPing st = case st.stage of
Online _ p -> p
_ -> Nothing