packages feed

haskoin-node 2.1.2 → 2.2.0

raw patch · 7 files changed

+466/−90 lines, 7 filesdep ~haskoin-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: haskoin-core

API changes (from Hackage documentation)

+ Haskoin.Node: [pending] :: Peer -> !TVar Int
+ Haskoin.Node: chainSync :: MonadIO m => Chain -> m ()
+ Haskoin.Node: isRoutable :: SockAddr -> Bool
+ Haskoin.Node: maxAddrCount :: Word64
+ Haskoin.Node: maxHeadersCount :: Word64
+ Haskoin.Node: maxPendingMessages :: Int
+ Haskoin.Node: peerMessageDone :: MonadIO m => Peer -> m ()
+ Haskoin.Node: peerMgrSync :: MonadIO m => PeerMgr -> m ()
- Haskoin.Node: Peer :: !Mailbox PeerMessage -> !Publisher PeerEvent -> !Text -> !TVar Bool -> Peer
+ Haskoin.Node: Peer :: !Mailbox PeerMessage -> !Publisher PeerEvent -> !Text -> !TVar Bool -> !TVar Int -> Peer
- Haskoin.Node: peer :: MonadUnliftIO m => PeerConfig -> TVar Bool -> Inbox PeerMessage -> m ()
+ Haskoin.Node: peer :: MonadUnliftIO m => PeerConfig -> Peer -> Inbox PeerMessage -> m ()
- Haskoin.Node: wrapPeer :: PeerConfig -> TVar Bool -> Mailbox PeerMessage -> Peer
+ Haskoin.Node: wrapPeer :: PeerConfig -> TVar Bool -> TVar Int -> Mailbox PeerMessage -> Peer

Files

CHANGELOG.md view
@@ -4,6 +4,51 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2026-11-04++### Changed++- Breaking: `Peer` has a `pending` field, `wrapPeer` takes its `TVar Int`,+  and `peer` takes the `Peer` that `wrapPeer` built instead of the busy+  `TVar`.+- Messages from a peer apply backpressure. A peer may have at most+  `maxPendingMessages` messages waiting for the node; after that the node+  stops reading its socket until it catches up. Address, header, version,+  verack, ping and pong messages are handled to completion before the next+  one is taken. A fast peer could fill the node's unbounded queues before.+- Outgoing messages are logged by command at debug level, not in full.++### Fixed++- Bitcoin Cash headers sync again. This package now requires haskoin-core+  1.5.1, which evaluates the ASERT difficulty rule at the parent block. With+  haskoin-core 1.5.0 the node stopped at mainnet block 975899 with+  `Incorrect difficulty bits`, because that release checked the bits of each+  header with a formula that used the header's own timestamp.+- Network magic and payload size are checked right after the message header,+  before the payload is read. The size limit is the network's+  `maxMessageSize` instead of a fixed 32 MiB. An oversized message used to be+  logged and its payload then parsed as the next header.+- Peers that send more than 2000 headers or more than 1000 addresses in one+  message are disconnected before the message is decoded.+- Address and header messages from a peer are ignored until it completes the+  handshake.+- A second version or verack from a peer disconnects it. Each one used to+  announce the peer as connected again.+- Relayed addresses that are not public unicast addresses (loopback,+  private, link-local, multicast, documentation and similar ranges, or port+  0) are not added to the address pool. `isRoutable` is exported.+- A peer that does not answer a ping within the timeout is disconnected, even+  if it keeps sending other messages.+- Only headers from the syncing peer reset its sync timeout. Another peer's+  headers could keep a silent syncing peer from ever timing out.+- The peer manager rechecks `maxPeers` when it handles a queued connection,+  so the connect loop can no longer go over the limit while the manager is+  busy.+- Purging an old chain database with a column family deleted the same keys+  from the default column family too. The purge now deletes keys as it walks+  them instead of building a list of all of them in memory.+ ## [2.1.2] - 2026-11-04  ### Fixed
haskoin-node.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           haskoin-node-version:        2.1.2+version:        2.2.0 synopsis:       P2P library for Bitcoin and Bitcoin Cash description:    Please see the README on GitHub at <https://github.com/jprupp/haskoin-node#readme> category:       Bitcoin, Finance, Network@@ -47,7 +47,7 @@     , data-default     , fast-logger     , hashable-    , haskoin-core >=1.5.0+    , haskoin-core >=1.5.1     , mtl     , network     , nqe >=0.6.3@@ -84,7 +84,7 @@     , data-default     , fast-logger     , hashable-    , haskoin-core >=1.5.0+    , haskoin-core >=1.5.1     , haskoin-node     , hspec     , mtl
src/Haskoin/Node.hs view
@@ -105,6 +105,11 @@     _ -> return ()   publish (ChainEvent event) output +-- | Route events from peers to the peer manager and chain. A message from a+-- peer is handled to completion before the peer may send another one past its+-- small allowance of pending messages (see 'maxPendingMessages'), so that the+-- node's queues cannot grow faster than it can work. Address and header+-- messages are only taken from peers that completed the handshake. peerEvents ::   (MonadIO m) =>   Chain -> PeerMgr -> Inbox PeerEvent -> Publisher NodeEvent -> m ()@@ -118,20 +123,26 @@     PeerMessage p msg -> do       case msg of         MVersion v ->-          peerMgrVersion mgr p v+          peerMgrVersion mgr p v >> peerMgrSync mgr         MVerAck ->-          peerMgrVerAck mgr p+          peerMgrVerAck mgr p >> peerMgrSync mgr         MPing (Ping n) ->-          peerMgrPing mgr p n+          peerMgrPing mgr p n >> peerMgrSync mgr         MPong (Pong n) ->-          peerMgrPong mgr p n+          peerMgrPong mgr p n >> peerMgrSync mgr         MAddr (Addr ns) ->-          peerMgrAddrs mgr p (map snd ns)+          whenOnline p $ peerMgrAddrs mgr p (map snd ns) >> peerMgrSync mgr         MHeaders (Headers hs) ->-          chainHeaders ch p (map fst hs)+          whenOnline p $ chainHeaders ch p (map fst hs) >> chainSync ch         _ -> return ()       ticklePeer mgr p+      peerMessageDone p   publish (PeerEvent event) output+  where+    whenOnline p act =+      getOnlinePeer mgr p >>= \case+        Just OnlinePeer {online = True} -> act+        _ -> return ()  -- | Launch node process in the foreground. withNode ::
src/Haskoin/Node/Chain.hs view
@@ -32,6 +32,7 @@     chainIsSynced,     chainBlockMain,     chainHeaders,+    chainSync,   ) where @@ -80,6 +81,7 @@   | ChainPeerConnected !Peer   | ChainPeerDisconnected !Peer   | ChainPing+  | ChainSyncMsg !(Listen ())  -- | Events originating from chain syncing process. data ChainEvent@@ -245,7 +247,7 @@         ("Could not connect headers from peer " <> p.label)       killPeer p     Just done -> do-      setLastReceived ch.state+      setLastReceived ch.state p       best <- runReaderT getBestBlockHeader ch.config       when (pbest.header /= best.header) $         chainEvent ch.config (ChainBestBlock best)@@ -315,6 +317,8 @@   logDebug ch.config.logger ("Disconnected peer " <> p.label)   finishPeer ch p   syncNewPeer ch+chainConfigMessage _ (ChainSyncMsg r) =+  atomically (r ()) chainConfigMessage ch ChainPing = do   logDebug ch.config.logger "Internal clock event"   let to = ch.config.timeout@@ -348,7 +352,7 @@ initChainDB :: (MonadIO m) => ChainConfig -> m () initChainDB cfg@ChainConfig {db, cf, net} = liftIO $ do   ver <- retrieveCommon db cf ChainDataVersionKey-  when (ver /= Just dataVersion) $ purgeChainDB cfg >>= writeBatch db+  when (ver /= Just dataVersion) $ purgeChainDB cfg   case cf of     Nothing -> insert db ChainDataVersionKey dataVersion     Just cf' -> insertCF db cf' ChainDataVersionKey dataVersion@@ -358,26 +362,29 @@       setBestBlockHeader (genesisNode net)  -- | Purge database of elements having keys that may conflict with those used in--- this module.-purgeChainDB :: (MonadIO m) => ChainConfig -> m [R.BatchOp]+-- this module. Keys are deleted from the column family that holds the chain,+-- one at a time as the iterator walks them, so that memory use does not grow+-- with the size of the database.+purgeChainDB :: (MonadIO m) => ChainConfig -> m () purgeChainDB ChainConfig {db, cf} = liftIO $ do   with_iter $ \it -> do     R.iterSeek it (B.singleton 0x90)-    recurse_delete it+    go it   where     with_iter = case cf of       Nothing -> R.withIter db       Just cf' -> R.withIterCF db cf'-    recurse_delete it =+    del k = case cf of+      Nothing -> R.delete db k+      Just cf' -> R.deleteCF db cf' k+    go it =       R.iterKey it >>= \case         Just k-          | B.head k == 0x90 || B.head k == 0x91 -> do-              case cf of-                Nothing -> R.delete db k-                Just cf' -> R.deleteCF db cf' k+          | B.take 1 k `elem` [B.singleton 0x90, B.singleton 0x91] -> do+              del k               R.iterNext it-              (R.Del k :) <$> recurse_delete it-        _ -> return []+              go it+        _ -> return ()  -- | Import a bunch of continuous headers. Returns 'True' if the number of -- headers is less than 2000, which means that there are no more headers to sync@@ -465,11 +472,15 @@   where     z = "0000000000000000000000000000000000000000000000000000000000000000" --- | Set the time of last received data to now if a syncing peer is active.-setLastReceived :: (MonadIO m) => TVar ChainState -> m ()-setLastReceived st = do+-- | Set the time of last received data to now if the given peer is the one+-- being synced against. Headers from other peers must not keep a silent+-- syncing peer from timing out.+setLastReceived :: (MonadIO m) => TVar ChainState -> Peer -> m ()+setLastReceived st p = do   now <- liftIO getCurrentTime-  let f ChainSync {..} = ChainSync {timestamp = now, ..}+  let f c@ChainSync {..}+        | peer == p = ChainSync {timestamp = now, ..}+        | otherwise = c   atomically (modifyTVar st (\s -> s {syncing = f <$> s.syncing}))  -- | Add a new peer to the queue of peers to sync against.@@ -608,3 +619,7 @@ chainHeaders :: (MonadIO m) => Chain -> Peer -> [BlockHeader] -> m () chainHeaders ch p hs =   ChainHeaders p hs `send` ch.mailbox++-- | Wait until the chain process has handled every message sent to it before.+chainSync :: (MonadIO m) => Chain -> m ()+chainSync ch = ChainSyncMsg `query` ch.mailbox
src/Haskoin/Node/Peer.hs view
@@ -28,6 +28,10 @@     getBusy,     setBusy,     setFree,+    peerMessageDone,+    maxPendingMessages,+    maxHeadersCount,+    maxAddrCount,   ) where @@ -41,10 +45,10 @@ import Data.Function (on) import Data.List (union) import Data.Maybe (isJust)-import Data.Serialize (decode, runGet, runPut)+import Data.Serialize (decode, get, runGet, runPut) import Data.String.Conversions (cs) import Data.Text (Text)-import Data.Word (Word32)+import Data.Word (Word32, Word64) import Haskoin import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever)@@ -78,7 +82,9 @@   { mailbox :: !(Mailbox PeerMessage),     pub :: !(Publisher PeerEvent),     label :: !Text,-    busy :: !(TVar Bool)+    busy :: !(TVar Bool),+    -- | messages from this peer that the node has not finished handling+    pending :: !(TVar Int)   }  instance Eq Peer where@@ -95,44 +101,73 @@ wrapPeer ::   PeerConfig ->   TVar Bool ->+  TVar Int ->   Mailbox PeerMessage ->   Peer-wrapPeer cfg busy mbox =+wrapPeer cfg busy pending mbox =   Peer     { mailbox = mbox,       pub = cfg.pub,       label = cfg.label,-      busy = busy+      busy = busy,+      pending = pending     } --- | Run peer process in current thread.+-- | Most messages from one peer that may wait for the node to handle them.+-- When a peer gets this far ahead, the node stops reading from its socket+-- until it catches up, so that a peer that sends faster than the node can+-- handle fills its own TCP window instead of the node's memory.+maxPendingMessages :: Int+maxPendingMessages = 16++-- | Most block headers a @headers@ message may carry. Bitcoin Core never+-- sends more and disconnects peers that do.+maxHeadersCount :: Word64+maxHeadersCount = 2000++-- | Most addresses an @addr@ or @addrv2@ message may carry.+maxAddrCount :: Word64+maxAddrCount = 1000++-- | Tell the peer process that the node has finished handling one of the+-- messages that it published, so that it may read another one.+peerMessageDone :: (MonadIO m) => Peer -> m ()+peerMessageDone p = atomically $ modifyTVar' p.pending (max 0 . subtract 1)++-- | Run peer process in current thread. The 'Peer' must be the one that+-- 'wrapPeer' built with the mailbox of this inbox. peer ::   (MonadUnliftIO m) =>   PeerConfig ->-  TVar Bool ->+  Peer ->   Inbox PeerMessage ->   m ()-peer cfg@PeerConfig {label, net, connect, pub, logger} busy inbox = do-  let p = wrapPeer cfg busy (inboxToMailbox inbox)-  withRunInIO $ \run -> connect (run . peer_session p)+peer cfg@PeerConfig {label, net, connect, pub, logger} p inbox =+  withRunInIO $ \run -> connect (run . peer_session)   where     go = do       logDebug logger $ label <> " awaiting event..."       msg <- receive inbox       dispatchMessage cfg msg >>= bool (return ()) go-    peer_session p ad = do+    peer_session ad = do       let ins = transPipe liftIO ad.inboundConduit           ons = transPipe liftIO ad.outboundConduit           src =             runConduit $               ins                 .| inPeerConduit cfg-                .| mapM_C (send_msg p)+                .| mapM_C send_msg           snk = outPeerConduit net .| ons       withAsync src $ \as -> do         link as         runConduit (go .| snk)-    send_msg p msg = publish (PeerMessage p msg) pub+    -- Wait until the node has room for another message from this peer.+    send_msg msg = do+      atomically $ do+        n <- readTVar p.pending+        checkSTM (n < maxPendingMessages)+        writeTVar p.pending (n + 1)+      publish (PeerMessage p msg) pub  -- | Internal function to dispatch peer messages. dispatchMessage ::@@ -141,48 +176,71 @@   PeerMessage ->   ConduitT i Message m Bool dispatchMessage PeerConfig {label, logger} (SendMessage msg) = do-  logDebug logger (label <> " sending: " <> cs (show msg))+  logDebug logger (label <> " sending cmd " <> cs (show (msgType msg)))   yield msg   return True dispatchMessage PeerConfig {label, logger} KillPeer = do   logInfo logger (label <> " disconnecting")   return False --- | Internal conduit to parse messages coming from peer.+-- | Internal conduit to parse messages coming from peer. The header is checked+-- before the payload is read, so that a peer cannot make the node buffer a+-- payload that it will reject anyway. A peer that sends anything invalid is+-- disconnected. inPeerConduit ::   (MonadIO m) => PeerConfig -> ConduitT ByteString Message m ()-inPeerConduit pc@PeerConfig {label, net, logger} = loopForever $ do+inPeerConduit PeerConfig {label, net, logger} = loopForever $ do   logDebug logger (label <> " awaiting message...")   x <- takeCE 24 .| foldC   when (B.null x) $ do     logError logger (label <> " sent empty message header")     error "Peer sent empty message header"-  case decode x of-    Left e -> do-      logError logger (label <> " sent invalid message header")-      error "Peer sent invalid message header"-    Right (MessageHeader _ cmd len _)-      | len > 32 * 2 ^ (20 :: Int) ->-          logWarn logger $-            label-              <> " wants to send "-              <> cs (show len)-              <> " bytes (too large) for cmd "-              <> cs (show cmd)-      | otherwise -> do-          logDebug logger (label <> " sent cmd " <> cs (show cmd))-          y <- takeCE (fromIntegral len) .| foldC-          case runGet (getMessage net) $ x `B.append` y of-            Left e -> do-              logError-                logger-                (label <> " sent invalid payload for cmd " <> cs (show cmd))-              error "Peer sent invalid payload"-            Right msg -> do-              logDebug-                logger-                (label <> " sent payload for cmd " <> cs (show cmd))-              yield msg+  MessageHeader mgc cmd len _ <- case decode x of+    Left _ -> bad "invalid message header"+    Right h -> return h+  when (mgc /= net.magic) $+    bad ("wrong network magic " <> cs (show mgc))+  when (len > net.maxMessageSize) $+    bad+      ( "too large payload of "+          <> cs (show len)+          <> " bytes for cmd "+          <> cs (show cmd)+      )+  logDebug logger (label <> " sent cmd " <> cs (show cmd))+  y <- takeCE (fromIntegral len) .| foldC+  when (B.length y /= fromIntegral len) $+    bad ("truncated payload for cmd " <> cs (show cmd))+  forM_ (countLimit cmd) $ \limit ->+    case runGet getCount y of+      Right n+        | n > limit ->+            bad+              ( "too many items ("+                  <> cs (show n)+                  <> ") in cmd "+                  <> cs (show cmd)+              )+      _ -> return ()+  case runGet (getMessage net) $ x `B.append` y of+    Left _ -> bad ("invalid payload for cmd " <> cs (show cmd))+    Right msg -> do+      logDebug+        logger+        (label <> " sent payload for cmd " <> cs (show cmd))+      yield msg+  where+    bad :: (MonadIO m) => Text -> m a+    bad e = do+      logError logger (label <> " sent " <> e)+      error ("Peer sent " <> cs e)+    getCount = do+      VarInt n <- get+      return n+    countLimit MCHeaders = Just maxHeadersCount+    countLimit MCAddr = Just maxAddrCount+    countLimit MCAddrV2 = Just maxAddrCount+    countLimit _ = Nothing  -- | Outgoing peer conduit to serialize and send messages. outPeerConduit :: (Monad m) => Network -> ConduitT Message ByteString m ()
src/Haskoin/Node/PeerMgr.hs view
@@ -23,6 +23,7 @@     peerMgrPong,     peerMgrAddrs,     peerMgrVerAck,+    peerMgrSync,     getPeers,     getOnlinePeer,     ticklePeer,@@ -30,6 +31,7 @@     myVersion,     toSockAddr,     toHostService,+    isRoutable,   ) where @@ -45,7 +47,7 @@ import Data.String.Conversions (cs) import Data.Time.Clock import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)-import Data.Word (Word32, Word64)+import Data.Word (Word32, Word64, Word8) import Haskoin import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever)@@ -88,6 +90,7 @@   | PeerPing !Peer !Word64   | PeerPong !Peer !Word64   | PeerAddrs !Peer ![NetworkAddress]+  | Sync !(Listen ())  -- | Data structure representing an online peer. data OnlinePeer = OnlinePeer@@ -222,18 +225,32 @@     Nothing -> do       logWarn         mgr.config.logger-        ("Received verack from unknown peer " <> p.label)+        ("Verack rejected for peer " <> p.label)       killPeer p dispatch mgr (PeerAddrs p nas)-  | mgr.config.discover = do-      let sas = map (hostToSockAddr . (.address)) nas-          len = length sas-      logDebug-        mgr.config.logger-        ("Received " <> cs (show len) <> " addresses from peer " <> p.label)-      forM_ sas (newPeer mgr)+  | mgr.config.discover =+      getOnlinePeer mgr p >>= \case+        Just OnlinePeer {online = True} -> do+          let sas = filter isRoutable $ map (hostToSockAddr . (.address)) nas+          logDebug+            mgr.config.logger+            ( "Received "+                <> cs (show (length nas))+                <> " addresses from peer "+                <> p.label+                <> ", "+                <> cs (show (length sas))+                <> " routable"+            )+          forM_ sas (newPeer mgr)+        _ ->+          logDebug+            mgr.config.logger+            ("Ignoring addresses from peer before handshake " <> p.label)   | otherwise =       logDebug mgr.config.logger ("Ignoring addresses from peer " <> p.label)+dispatch _ (Sync r) =+  atomically (r ()) dispatch mgr (PeerPong p n) = do   logDebug     mgr.config.logger@@ -268,7 +285,9 @@     Just o -> do       now <- liftIO getCurrentTime       let expired = mgr.config.maxPeerLife `addUTCTime` o.connected-      let deadline = mgr.config.timeout `addUTCTime` o.tickled+          deadline = mgr.config.timeout `addUTCTime` o.tickled+          -- Other traffic must not keep a peer that ignores our ping.+          pingDeadline = (\(t, _) -> mgr.config.timeout `addUTCTime` t) <$> o.ping       if         | now > expired -> do             logError mgr.config.logger ("Peer " <> p.label <> " is too old")@@ -276,6 +295,9 @@         | now > deadline -> do             logError mgr.config.logger ("Peer " <> p.label <> " timed out")             killPeer p+        | maybe False (now >) pingDeadline -> do+            logError mgr.config.logger ("Peer " <> p.label <> " did not answer ping")+            killPeer p         | isNothing o.ping -> sendPing mgr p         | otherwise -> return ()     Nothing -> do@@ -344,7 +366,15 @@  connectPeer :: (MonadUnliftIO m) => PeerMgr -> SockAddr -> m () connectPeer mgr sa = do+  n <- length <$> getOnlinePeers mgr   atomically (findPeerAddress mgr.peers sa) >>= \case+    _+      | n >= mgr.config.maxPeers ->+          -- The connect loop may have queued this before the last free slot+          -- was taken.+          logDebug+            mgr.config.logger+            ("Not connecting to peer " <> cs (show sa) <> ": no free slots")     Just _ ->       logWarn mgr.config.logger ("Duplicate connection to peer " <> cs (show sa))     Nothing -> do@@ -366,9 +396,10 @@                 logger = withSource "Peer" mgr.config.logger               }       busy <- newTVarIO False-      let p = wrapPeer pc busy mailbox+      pending <- newTVarIO 0+      let p = wrapPeer pc busy pending mailbox       a <- withRunInIO $ \io ->-        mgr.supervisor `addChild` io (launch pc busy inbox p)+        mgr.supervisor `addChild` io (launch pc inbox p)       MVersion ver `sendMessage` p       atomically $         insertPeer@@ -390,8 +421,8 @@     srv net       | net.segWit = 8       | otherwise = 0-    launch pc busy inbox p =-      withPeerLoop mgr p (\a -> link a >> peer pc busy inbox)+    launch pc inbox p =+      withPeerLoop mgr p (\a -> link a >> peer pc p inbox)  withPeerLoop :: (MonadUnliftIO m) => PeerMgr -> Peer -> (Async a -> m a) -> m a withPeerLoop mgr p =@@ -452,6 +483,8 @@           else             findPeer b p >>= \case               Nothing -> return Nothing -- peer not found+              Just OnlinePeer {version = Just _} ->+                return Nothing -- duplicate version               Just o -> do                 let n = o {version = Just v, online = o.verack}                 insertPeer b n@@ -460,6 +493,7 @@ setPeerVerAck :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer) setPeerVerAck b p =   findPeer b p >>= \case+    Just OnlinePeer {verack = True} -> return Nothing -- duplicate verack     Just o -> do       let o' = o {verack = True, online = isJust o.version}       insertPeer b o'@@ -519,6 +553,54 @@  peerMgrAddrs :: (MonadIO m) => PeerMgr -> Peer -> [NetworkAddress] -> m () peerMgrAddrs mgr p addrs = PeerAddrs p addrs `send` mgr.mailbox++-- | Wait until the peer manager has handled every message sent to it before.+peerMgrSync :: (MonadIO m) => PeerMgr -> m ()+peerMgrSync mgr = Sync `query` mgr.mailbox++-- | Whether an address that a peer relayed is worth connecting to: a public+-- unicast address with a port. Loopback, private, link-local, shared,+-- documentation, multicast and reserved ranges are refused, so that a peer+-- cannot make the node connect to services on its own host or network.+-- Static peers in the configuration are not checked.+isRoutable :: SockAddr -> Bool+isRoutable (SockAddrInet port h) =+  port /= 0 && routable4 (hostAddressToTuple h)+isRoutable (SockAddrInet6 port _ h _) =+  port /= 0 && case hostAddress6ToTuple h of+    (0, 0, 0, 0, 0, 0xffff, hi, lo) ->+      routable4+        ( fromIntegral (hi `div` 256),+          fromIntegral (hi `mod` 256),+          fromIntegral (lo `div` 256),+          fromIntegral (lo `mod` 256)+        )+    (a, b, _, _, _, _, _, _)+      | a == 0 -> False -- unspecified, loopback, IPv4-compatible+      | a .&. 0xfe00 == 0xfc00 -> False -- unique local+      | a .&. 0xffc0 == 0xfe80 -> False -- link-local+      | a .&. 0xffc0 == 0xfec0 -> False -- site-local+      | a .&. 0xff00 == 0xff00 -> False -- multicast+      | a == 0x2001 && b == 0x0db8 -> False -- documentation+      | otherwise -> True+isRoutable _ = False++routable4 :: (Word8, Word8, Word8, Word8) -> Bool+routable4 (a, b, c, _)+  | a == 0 = False -- this network+  | a == 10 = False -- private+  | a == 100 && b .&. 0xc0 == 64 = False -- shared address space+  | a == 127 = False -- loopback+  | a == 169 && b == 254 = False -- link-local+  | a == 172 && b .&. 0xf0 == 16 = False -- private+  | a == 192 && b == 0 && c == 0 = False -- protocol assignments+  | a == 192 && b == 0 && c == 2 = False -- documentation+  | a == 192 && b == 168 = False -- private+  | a == 198 && b .&. 0xfe == 18 = False -- benchmarking+  | a == 198 && b == 51 && c == 100 = False -- documentation+  | a == 203 && b == 0 && c == 113 = False -- documentation+  | a >= 224 = False -- multicast and reserved+  | otherwise = True  toHostService :: String -> (Maybe String, Maybe String) toHostService str =
test/Haskoin/NodeSpec.hs view
@@ -6,6 +6,7 @@ {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-} {-# LANGUAGE NoFieldSelectors #-}  module Haskoin.NodeSpec (spec) where@@ -22,12 +23,13 @@ import Data.List (find) import Data.Maybe (isJust, listToMaybe, mapMaybe) import Data.Serialize (decode, get, runGet, runPut)+import Data.Time.Clock (NominalDiffTime) import Data.Time.Clock.POSIX (getPOSIXTime) import Database.RocksDB qualified as R import Haskoin import Haskoin.Node import NQE-import Network.Socket (AddrInfo (addrAddress), SockAddr (..))+import Network.Socket (AddrInfo (addrAddress), SockAddr (..), tupleToHostAddress, tupleToHostAddress6) import System.Random (randomIO) import Test.Hspec import Test.Hspec.QuickCheck@@ -38,19 +40,40 @@   { testMgr :: PeerMgr,     testChain :: Chain,     nodeEvents :: Inbox NodeEvent,+    testDB :: R.DB,     -- | how many getheaders messages the mock peer has received     getHeadersCount :: TVar Int   } --- | Connect to a mock peer that serves these blocks.+-- | What the mock peer sends back for each message: a message to serialize+-- for the test network, or raw bytes.+type React = Message -> [Either ByteString Message]++-- | How to set up a test node.+data TestOpts = TestOpts+  { react :: React,+    -- | runs on the database before the node starts+    setup :: R.DB -> IO (),+    timeout :: NominalDiffTime+  }++defOpts :: [Block] -> TestOpts+defOpts blocks =+  TestOpts+    { react = map Right . mockPeerReact blocks,+      setup = \_ -> return (),+      timeout = 120+    }++-- | Connect to a mock peer that reacts to messages like this. dummyPeerConnect ::-  [Block] ->+  React ->   TVar Int ->   Network ->   NetworkAddress ->   SockAddr ->   WithConnection-dummyPeerConnect blocks count net ad sa f = do+dummyPeerConnect react count net ad sa f = do   r <- newInbox   s <- newInbox   let s' = inboxToMailbox s@@ -70,10 +93,10 @@         forever (receive r >>= yield)           .| inc           .| iterMC count_getheaders-          .| concatMapC (mockPeerReact blocks)+          .| concatMapC react           .| outc           .| awaitForever (`send` s)-    outc = mapMC $ \msg -> return $ runPut (putMessage net msg)+    outc = mapMC $ return . either id (runPut . putMessage net)     count_getheaders = \case       MGetHeaders _ -> atomically (modifyTVar' count (+ 1))       _ -> return ()@@ -149,6 +172,114 @@             testMerkle b = buildMerkleRoot (ths b) `shouldBe` Right b.header.merkle         testMerkle b1         testMerkle b2+  describe "untrusted peers" $ do+    it "imports a full batch of 2000 headers" $+      withTestNodeOpts+        (defOpts allBlocks) {react = reactAfterVerAck [Right (manyHeaders net 2000)]}+        net+        "full-batch"+        $ \TestNode {..} -> do+          bn <-+            receiveMatch nodeEvents $ \case+              ChainEvent (ChainBestBlock bn) | bn.height > 0 -> Just bn+              _ -> Nothing+          bn.height `shouldBe` 2000+    it "disconnects a peer that sends more than 2000 headers" $+      withTestNodeOpts+        (defOpts allBlocks) {react = reactAfterVerAck [Right (manyHeaders net 2001)]}+        net+        "too-many-headers"+        $ \TestNode {..} -> do+          p <- waitForPeer nodeEvents+          waitForDisconnect nodeEvents p+          (.height) <$> chainGetBest testChain `shouldReturn` 0+    it "disconnects a peer that sends more than 1000 addresses" $ do+      let na = NetworkAddress 0 (sockToHostAddress (SockAddrInet 8333 0x08080808))+          addrs = MAddr (Addr (replicate 1001 (0, na)))+      withTestNodeOpts+        (defOpts allBlocks) {react = reactAfterVerAck [Right addrs]}+        net+        "too-many-addrs"+        $ \TestNode {..} -> do+          p <- waitForPeer nodeEvents+          waitForDisconnect nodeEvents p+    it "disconnects a peer that sends the wrong network magic" $ do+      let bad = runPut (putMessage btc (MPing (Ping 1)))+      withTestNodeOpts+        (defOpts allBlocks) {react = reactAfterVerAck [Left bad]}+        net+        "wrong-magic"+        $ \TestNode {..} -> do+          p <- waitForPeer nodeEvents+          waitForDisconnect nodeEvents p+    it "disconnects a peer that sends a second verack" $+      withTestNodeOpts+        (defOpts allBlocks) {react = reactAfterVerAck [Right MVerAck]}+        net+        "duplicate-verack"+        $ \TestNode {..} -> do+          p <- waitForPeer nodeEvents+          waitForDisconnect nodeEvents p+    it "ignores headers sent before the handshake" $ do+      let hs = MHeaders (Headers [(b.header, VarInt 0) | b <- allBlocks])+          react = \case+            MVersion _ -> [Right hs, Right MVerAck]+            MPing (Ping n) -> [Right (MPong (Pong n))]+            _ -> []+      withTestNodeOpts (defOpts allBlocks) {react = react} net "early-headers" $+        \TestNode {..} -> do+          _ <- waitForPeer nodeEvents+          threadDelay (500 * 1000)+          (.height) <$> chainGetBest testChain `shouldReturn` 0+    it "disconnects a peer that chatters instead of answering a ping" $ do+      -- Answer the node's ping with a ping of our own, and its pong with+      -- another ping, so that the peer is never silent.+      let react = \case+            MVersion _ -> [Right MVerAck]+            MPing _ -> [Right (MPing (Ping 1))]+            MPong _ -> [Right (MPing (Ping 1))]+            -- end the header sync so its own timeout does not apply+            MGetHeaders _ -> [Right (MHeaders (Headers []))]+            _ -> []+      withTestNodeOpts+        (defOpts allBlocks) {react = react, timeout = 2}+        net+        "ignored-ping"+        $ \TestNode {..} -> do+          p <- waitForPeer nodeEvents+          waitForDisconnect nodeEvents p+    it "does not connect to private addresses that peers relay" $ do+      let v4 a b c d p = SockAddrInet p (tupleToHostAddress (a, b, c, d))+          v6 t p = SockAddrInet6 p 0 (tupleToHostAddress6 t) 0+      isRoutable (v4 8 8 8 8 8333) `shouldBe` True+      isRoutable (v4 8 8 8 8 0) `shouldBe` False+      isRoutable (v4 127 0 0 1 8333) `shouldBe` False+      isRoutable (v4 10 1 2 3 8333) `shouldBe` False+      isRoutable (v4 172 16 0 1 8333) `shouldBe` False+      isRoutable (v4 172 32 0 1 8333) `shouldBe` True+      isRoutable (v4 192 168 1 1 8333) `shouldBe` False+      isRoutable (v4 169 254 1 1 8333) `shouldBe` False+      isRoutable (v4 100 64 0 1 8333) `shouldBe` False+      isRoutable (v4 224 0 0 1 8333) `shouldBe` False+      isRoutable (v6 (0x2a01, 0x4f8, 0, 0, 0, 0, 0, 1) 8333) `shouldBe` True+      isRoutable (v6 (0, 0, 0, 0, 0, 0, 0, 1) 8333) `shouldBe` False+      isRoutable (v6 (0xfd00, 0, 0, 0, 0, 0, 0, 1) 8333) `shouldBe` False+      isRoutable (v6 (0xfe80, 0, 0, 0, 0, 0, 0, 1) 8333) `shouldBe` False+      isRoutable (v6 (0, 0, 0, 0, 0, 0xffff, 0x7f00, 1) 8333) `shouldBe` False+      isRoutable (v6 (0, 0, 0, 0, 0, 0xffff, 0x0808, 0x0808) 8333) `shouldBe` True+      isRoutable (hostToSockAddr (sockToHostAddress (v4 127 0 0 1 8333)))+        `shouldBe` False+  describe "chain database" $ do+    it "leaves the default column family alone when purging" $ do+      -- The same key in both families: the purge must delete it only from+      -- the family that holds the chain.+      let k = B.pack [0x90, 1, 2, 3]+          setup db = do+            R.put db k "keep"+            forM_ (R.columnFamilies db) $ \cf -> R.putCF db cf k "stale"+      withTestNodeOpts (defOpts allBlocks) {setup = setup} net "purge-cf" $+        \TestNode {..} -> do+          R.get testDB k `shouldReturn` Just "keep"   describe "chain on test network" $ do     it "stops asking a peer that has no more headers" $       -- The peer answers getheaders with an empty headers message, as a@@ -195,6 +326,33 @@         forM_ (zip ps hs) $ \(p, h) ->           headerHash p.header `shouldBe` h +-- | Wait for the node to disconnect from this peer, failing after a while.+waitForDisconnect :: Inbox NodeEvent -> Peer -> IO ()+waitForDisconnect inbox p =+  timeout (20 * 1000 * 1000) wait >>= \case+    Just () -> return ()+    Nothing -> expectationFailure "peer was not disconnected"+  where+    wait =+      receiveMatch inbox $ \case+        PeerEvent (PeerDisconnected p') | p == p' -> Just ()+        _ -> Nothing++-- | Mock peer that answers the handshake and pings, and sends these right+-- after its version ack.+reactAfterVerAck :: [Either ByteString Message] -> React+reactAfterVerAck extra = \case+  MVersion _ -> Right MVerAck : extra+  MPing (Ping n) -> [Right (MPong (Pong n))]+  _ -> []++-- | Headers message with a valid chain of this many headers on top of+-- genesis.+manyHeaders :: Network -> Int -> Message+manyHeaders net n =+  MHeaders . Headers $+    map (,VarInt 0) (appendBlocks net 0 net.genesisHeader n)+ waitForPeer :: (MonadIO m) => Inbox NodeEvent -> m Peer waitForPeer inbox =   receiveMatch inbox $ \case@@ -213,12 +371,18 @@ withTestNodeBlocks ::   (MonadUnliftIO m) =>   [Block] -> Network -> String -> (TestNode -> m a) -> m a-withTestNodeBlocks blocks net str f = flip runContT return $ do+withTestNodeBlocks blocks = withTestNodeOpts (defOpts blocks)++withTestNodeOpts ::+  (MonadUnliftIO m) =>+  TestOpts -> Network -> String -> (TestNode -> m a) -> m a+withTestNodeOpts opts net str f = flip runContT return $ do   w <- ContT $ withSystemTempDirectory ("haskoin-node-test-" <> str <> "-")   count <- newTVarIO 0   pub <- ContT withPublisher   sub <- ContT $ withSubscription pub   db <- ContT $ withLifted (R.withDBCF w cfg cols)+  liftIO (opts.setup db)   let ad =         NetworkAddress           nodeNetwork@@ -238,9 +402,9 @@             address = na,             net = net,             pub = pub,-            timeout = 120,+            timeout = opts.timeout,             maxPeerLife = 48 * 3600,-            connect = dummyPeerConnect blocks count net ad,+            connect = dummyPeerConnect opts.react count net ad,             logger = Logger {level = LevelError, source = "", push = \_ -> return ()}           }   Node mgr ch <- ContT $ withNode cfg'@@ -250,6 +414,7 @@         { testMgr = mgr,           testChain = ch,           nodeEvents = sub,+          testDB = db,           getHeadersCount = count         }   where