packages feed

haskoin-node-2.4.0: src/Haskoin/Node/Peer/Request.hs

-- |
-- Module      : Haskoin.Node.Peer.Request
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- Receiving the answer to a request for blocks or transactions, as an
-- explicit state machine. 'Haskoin.Node.Peer.getData' feeds it the messages
-- that a peer sends after the request.
--
-- = Why it works this way
--
-- Right after a getdata message, the node sends the peer a ping. Peers answer
-- messages in order, so every requested item must arrive, in the order
-- requested, before the matching pong. That gives clear rules:
--
-- * The next requested item moves the request forward.
--
-- * A requested item that is not the next one means the peer skipped
--   something, so the request fails.
--
-- * A notfound message naming a requested item, or the pong arriving early,
--   means the peer will not send everything, so the request fails.
--
-- * Anything else is unrelated traffic and is ignored. Peers send pings,
--   inventory and other chatter whenever they like, and failing a long
--   download because of one of them would only make the application retry it.
--
-- Keeping these rules in a pure function means each one can be tested on its
-- own, without a connection.
module Haskoin.Node.Peer.Request
  ( Request,
    RequestStep (..),
    newRequest,
    stepRequest,
  )
where

import Data.Text (Text)
import Data.Word (Word64)
import Haskoin

-- | A request in progress: the nonce of the ping that follows it, the items
-- still expected, and the items received so far, newest first.
data Request = Request
  { nonce :: !Word64,
    remaining :: ![InvVector],
    received :: ![Either Tx Block]
  }
  deriving stock (Eq, Show)

-- | Outcome of feeding a message to a request.
data RequestStep
  = -- | more items are expected
    Continue !Request
  | -- | every item arrived, in the order requested; transactions are 'Left'
    -- and blocks 'Right'
    Complete ![Either Tx Block]
  | -- | the peer will not send what was requested
    Failed !Text
  deriving stock (Eq, Show)

-- | Start a request for the given items, followed by a ping with the given
-- nonce. A request for nothing is complete at once.
newRequest :: Word64 -> [InvVector] -> RequestStep
newRequest n ivs = check Request {nonce = n, remaining = ivs, received = []}

-- | Feed a message from the peer to a request.
stepRequest :: Request -> Message -> RequestStep
stepRequest r message =
  case (r.remaining, message) of
    ([], _) -> Complete (reverse r.received)
    (next : _, MTx tx)
      | isTx next tx -> accept (Left tx)
      | any (`isTx` tx) r.remaining -> Failed "transaction out of order"
    (next : _, MBlock b)
      | isBlock next b -> accept (Right b)
      | any (`isBlock` b) r.remaining -> Failed "block out of order"
    (_, MNotFound (NotFound ivs))
      | any (`elem` r.remaining) ivs -> Failed "item not found"
    (_, MPong (Pong n))
      | n == r.nonce -> Failed "incomplete response"
    _ -> Continue r
  where
    accept x =
      check r {remaining = drop 1 r.remaining, received = x : r.received}

check :: Request -> RequestStep
check r
  | null r.remaining = Complete (reverse r.received)
  | otherwise = Continue r

-- | Whether a transaction is the one an inventory vector asks for.
isTx :: InvVector -> Tx -> Bool
isTx iv tx =
  iv.invType `elem` [InvTx, InvWitnessTx] && iv.invHash == (txHash tx).get

-- | Whether a block is the one an inventory vector asks for.
isBlock :: InvVector -> Block -> Bool
isBlock iv b =
  iv.invType `elem` [InvBlock, InvWitnessBlock]
    && iv.invHash == (headerHash b.header).get