/ src / Network / Cardano / Protocol / Handshake.hs
Handshake.hs
  1  {-# LANGUAGE OverloadedStrings #-}
  2  {-# LANGUAGE RankNTypes #-}
  3  
  4  module Network.Cardano.Protocol.Handshake
  5    ( -- * Configuration and results
  6      HandshakeConfig (..)
  7    , HandshakeResult (..)
  8    , HandshakeError (..)
  9  
 10      -- * Protocol implementations
 11    , handshakeInitiator
 12    , handshakeResponder
 13  
 14      -- * Helpers
 15    , buildVersionTable
 16    , buildVersionData
 17    , findCompatibleVersion
 18    ) where
 19  
 20  import Data.ByteString (ByteString)
 21  import Data.Map.Strict (Map)
 22  import qualified Data.Map.Strict as Map
 23  import qualified Data.Set as Set
 24  
 25  import Hyper (Co, yieldC)
 26  import Network.Cardano.Protocol.Handshake.Codec
 27  import Network.Cardano.Protocol.Handshake.Types
 28  
 29  -- | Handshake protocol as initiator.
 30  --
 31  -- The initiator:
 32  -- 1. Sends MsgProposeVersions with supported versions
 33  -- 2. Waits for MsgAcceptVersion or MsgRefuse
 34  --
 35  -- Returns the negotiated version or an error.
 36  handshakeInitiator
 37    :: (Monad m)
 38    => HandshakeConfig
 39    -> Co r ByteString ByteString m (Either HandshakeError HandshakeResult)
 40  handshakeInitiator config = do
 41    -- Step 1: Send ProposeVersions
 42    let versions = buildVersionTable config
 43        proposal = MsgProposeVersions versions
 44    response <- yieldC (encodeHandshake proposal)
 45  
 46    -- Step 2: Process response
 47    pure $ case decodeHandshake response of
 48      Left err ->
 49        Left $ HandshakeDecodeError err
 50  
 51      Right (MsgAcceptVersion ver vdata) ->
 52        Right $ HandshakeResult ver vdata
 53  
 54      Right (MsgRefuse reason) ->
 55        Left $ HandshakeRefused reason
 56  
 57      Right (MsgQueryReply _) ->
 58        Left $ HandshakeUnexpectedMessage "Unexpected MsgQueryReply to proposal"
 59  
 60      Right (MsgProposeVersions _) ->
 61        Left $ HandshakeUnexpectedMessage "Unexpected MsgProposeVersions in response"
 62  
 63  -- | Handshake protocol as responder.
 64  --
 65  -- The responder:
 66  -- 1. Waits for MsgProposeVersions
 67  -- 2. Sends MsgAcceptVersion (if compatible) or MsgRefuse
 68  --
 69  -- Returns the negotiated version or an error.
 70  handshakeResponder
 71    :: (Monad m)
 72    => HandshakeConfig
 73    -> Co r ByteString ByteString m (Either HandshakeError HandshakeResult)
 74  handshakeResponder config = do
 75    -- Step 1: Wait for ProposeVersions by yielding an empty marker
 76    -- (The first yield gets us into the coroutine, the response will be the proposal)
 77    -- We use a trick: yield an empty response first, which the mux will discard,
 78    -- then we get the actual proposal as our "response"
 79    -- Actually, the responder needs to receive first. Let's model this differently:
 80    -- The responder yields its initial "nothing" and gets back the proposal.
 81    incoming <- yieldC mempty
 82  
 83    case decodeHandshake incoming of
 84      Left err -> do
 85        -- Send decode error refuse and return error
 86        let refuse = MsgRefuse $ RefuseHandshakeDecodeError V7 "Decode error"
 87        _ <- yieldC (encodeHandshake refuse)
 88        pure $ Left $ HandshakeDecodeError err
 89  
 90      Right (MsgProposeVersions theirVersions) -> do
 91        -- Step 2: Find compatible version
 92        case findCompatibleVersion config theirVersions of
 93          Nothing -> do
 94            -- No common version, send refuse
 95            let refuse = MsgRefuse $ RefuseVersionMismatch (hcSupportedVersions config)
 96            _ <- yieldC (encodeHandshake refuse)
 97            pure $ Left HandshakeNoCommonVersion
 98  
 99          Just (ver, ourData) -> do
100            -- Accept the version
101            let accept = MsgAcceptVersion ver ourData
102            _ <- yieldC (encodeHandshake accept)
103            -- Return our data as result (in real impl we'd use their data)
104            pure $ Right $ HandshakeResult ver ourData
105  
106      Right other -> do
107        let refuse = MsgRefuse $ RefuseRefused V7 "Unexpected message type"
108        _ <- yieldC (encodeHandshake refuse)
109        pure $ Left $ HandshakeUnexpectedMessage $ "Expected MsgProposeVersions, got: " ++ show other
110  
111  -- | Build a version table from configuration.
112  buildVersionTable :: HandshakeConfig -> Map VersionNumber VersionData
113  buildVersionTable config =
114    Map.fromList [(v, buildVersionData config v) | v <- hcSupportedVersions config]
115  
116  -- | Build version data for a specific version.
117  buildVersionData :: HandshakeConfig -> VersionNumber -> VersionData
118  buildVersionData config ver
119    | ver <= V10 = VersionDataV7_10
120        { vdNetworkMagic  = hcNetworkMagic config
121        , vdInitiatorOnly = hcInitiatorOnly config
122        }
123    | otherwise = VersionDataV11_13
124        { vdNetworkMagic'  = hcNetworkMagic config
125        , vdInitiatorOnly' = hcInitiatorOnly config
126        , vdPeerSharing    = hcPeerSharing config
127        , vdQuery          = False
128        }
129  
130  -- | Find the highest mutually supported version.
131  findCompatibleVersion
132    :: HandshakeConfig
133    -> Map VersionNumber VersionData
134    -> Maybe (VersionNumber, VersionData)
135  findCompatibleVersion config theirVersions =
136    let ours   = Set.fromList (hcSupportedVersions config)
137        theirs = Map.keysSet theirVersions
138        common = Set.intersection ours theirs
139    in if Set.null common
140         then Nothing
141         else let ver = Set.findMax common
142              in Just (ver, buildVersionData config ver)