haskoin-node 2.3.1 → 2.4.0
raw patch · 35 files changed
+5486/−1931 lines, 35 filesdep +QuickCheckdep +directorydep +processdep −hashabledep −mtldep −safedep ~asyncdep ~basedep ~bytestringsetup-changedPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck, directory, process
Dependencies removed: hashable, mtl, safe, unordered-containers
Dependency ranges changed: async, base, bytestring, cereal, conduit, conduit-extra, containers, fast-logger, haskoin-core, hspec, network, nqe, random, rocksdb-haskell-jprupp, rocksdb-query, stm, string-conversions, text, time, transformers
API changes (from Hackage documentation)
- Haskoin.Node: ChainBestBlock :: !BlockNode -> ChainEvent
- Haskoin.Node: ChainConfig :: !DB -> !Maybe ColumnFamily -> !Network -> !Publisher ChainEvent -> !NominalDiffTime -> !Logger -> ChainConfig
- Haskoin.Node: ChainSynced :: !BlockNode -> ChainEvent
- Haskoin.Node: Conduits :: ConduitT () ByteString IO () -> ConduitT ByteString Void IO () -> Conduits
- Haskoin.Node: LevelDebug :: LogLevel
- Haskoin.Node: LevelError :: LogLevel
- Haskoin.Node: LevelInfo :: LogLevel
- Haskoin.Node: LevelWarn :: LogLevel
- Haskoin.Node: Logger :: !LogLevel -> !Text -> !LogStr -> IO () -> Logger
- Haskoin.Node: OnlinePeer :: !SockAddr -> !Bool -> !Bool -> !Maybe Version -> !Async () -> !Peer -> !Word64 -> !Maybe (UTCTime, Word64) -> ![NominalDiffTime] -> !UTCTime -> !UTCTime -> OnlinePeer
- Haskoin.Node: Peer :: !Mailbox PeerMessage -> !Publisher PeerEvent -> !Text -> !TVar Bool -> !TVar Int -> Peer
- Haskoin.Node: PeerConfig :: !Publisher PeerEvent -> !Network -> !Text -> !WithConnection -> !Logger -> PeerConfig
- Haskoin.Node: PeerConnected :: !Peer -> PeerEvent
- Haskoin.Node: PeerDisconnected :: !Peer -> PeerEvent
- Haskoin.Node: PeerMessage :: !Peer -> !Message -> PeerEvent
- Haskoin.Node: PeerMgrConfig :: !Int -> ![String] -> !Bool -> !NetworkAddress -> !Network -> !Publisher PeerEvent -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Logger -> PeerMgrConfig
- Haskoin.Node: [async] :: OnlinePeer -> !Async ()
- Haskoin.Node: [busy] :: Peer -> !TVar Bool
- Haskoin.Node: [connected] :: OnlinePeer -> !UTCTime
- Haskoin.Node: [inboundConduit] :: Conduits -> ConduitT () ByteString IO ()
- Haskoin.Node: [label] :: PeerConfig -> !Text
- Haskoin.Node: [level] :: Logger -> !LogLevel
- Haskoin.Node: [mailbox] :: OnlinePeer -> !Peer
- Haskoin.Node: [nonce] :: OnlinePeer -> !Word64
- Haskoin.Node: [online] :: OnlinePeer -> !Bool
- Haskoin.Node: [outboundConduit] :: Conduits -> ConduitT ByteString Void IO ()
- Haskoin.Node: [pending] :: Peer -> !TVar Int
- Haskoin.Node: [ping] :: OnlinePeer -> !Maybe (UTCTime, Word64)
- Haskoin.Node: [pings] :: OnlinePeer -> ![NominalDiffTime]
- Haskoin.Node: [push] :: Logger -> !LogStr -> IO ()
- Haskoin.Node: [source] :: Logger -> !Text
- Haskoin.Node: [tickled] :: OnlinePeer -> !UTCTime
- Haskoin.Node: [verack] :: OnlinePeer -> !Bool
- Haskoin.Node: [version] :: OnlinePeer -> !Maybe Version
- Haskoin.Node: buildVersion :: Network -> Word64 -> BlockHeight -> NetworkAddress -> NetworkAddress -> Word64 -> Version
- Haskoin.Node: chainBlockMain :: MonadIO m => Chain -> BlockHash -> m Bool
- Haskoin.Node: chainGetAncestor :: MonadIO m => Chain -> BlockHeight -> BlockNode -> m (Maybe BlockNode)
- Haskoin.Node: chainGetBest :: MonadIO m => Chain -> m BlockNode
- Haskoin.Node: chainGetBlock :: MonadIO m => Chain -> BlockHash -> m (Maybe BlockNode)
- Haskoin.Node: chainGetParents :: MonadIO m => Chain -> BlockHeight -> BlockNode -> m [BlockNode]
- Haskoin.Node: chainGetSplitBlock :: MonadIO m => Chain -> BlockNode -> BlockNode -> m BlockNode
- Haskoin.Node: chainHeaders :: MonadIO m => Chain -> Peer -> [BlockHeader] -> m ()
- Haskoin.Node: chainIsSynced :: MonadIO m => Chain -> m Bool
- Haskoin.Node: chainPeerConnected :: MonadIO m => Chain -> Peer -> m ()
- Haskoin.Node: chainPeerDisconnected :: MonadIO m => Chain -> Peer -> m ()
- Haskoin.Node: chainSync :: MonadIO m => Chain -> m ()
- Haskoin.Node: data Chain
- Haskoin.Node: data ChainConfig
- Haskoin.Node: data ChainEvent
- Haskoin.Node: data Conduits
- Haskoin.Node: data LogLevel
- Haskoin.Node: data Logger
- Haskoin.Node: data OnlinePeer
- Haskoin.Node: data Peer
- Haskoin.Node: data PeerConfig
- Haskoin.Node: data PeerEvent
- Haskoin.Node: data PeerMgr
- Haskoin.Node: data PeerMgrConfig
- Haskoin.Node: getBlocks :: MonadUnliftIO m => Network -> Int -> Peer -> [BlockHash] -> m (Maybe [Block])
- Haskoin.Node: getBusy :: MonadIO m => Peer -> m Bool
- Haskoin.Node: getData :: MonadUnliftIO m => Int -> Peer -> GetData -> m (Maybe [Either Tx Block])
- Haskoin.Node: getOnlinePeer :: MonadIO m => PeerMgr -> Peer -> m (Maybe OnlinePeer)
- Haskoin.Node: getPeers :: MonadIO m => PeerMgr -> m [OnlinePeer]
- Haskoin.Node: getTxs :: MonadUnliftIO m => Network -> Int -> Peer -> [TxHash] -> m (Maybe [Tx])
- Haskoin.Node: isRoutable :: SockAddr -> Bool
- Haskoin.Node: killPeer :: MonadIO m => Peer -> m ()
- Haskoin.Node: logAt :: MonadIO m => LogLevel -> Logger -> Text -> m ()
- Haskoin.Node: logDebug :: MonadIO m => Logger -> Text -> m ()
- Haskoin.Node: logError :: MonadIO m => Logger -> Text -> m ()
- Haskoin.Node: logInfo :: MonadIO m => Logger -> Text -> m ()
- Haskoin.Node: logWarn :: MonadIO m => Logger -> Text -> m ()
- Haskoin.Node: maxPendingMessages :: Int
- Haskoin.Node: myVersion :: Word32
- Haskoin.Node: peer :: MonadUnliftIO m => PeerConfig -> Peer -> Inbox PeerMessage -> m ()
- Haskoin.Node: peerMessageDone :: MonadIO m => Peer -> m ()
- Haskoin.Node: peerMgrAddrs :: MonadIO m => PeerMgr -> Peer -> [NetworkAddress] -> m ()
- Haskoin.Node: peerMgrBest :: MonadIO m => PeerMgr -> BlockHeight -> m ()
- Haskoin.Node: peerMgrPing :: MonadIO m => PeerMgr -> Peer -> Word64 -> m ()
- Haskoin.Node: peerMgrPong :: MonadIO m => PeerMgr -> Peer -> Word64 -> m ()
- Haskoin.Node: peerMgrSync :: MonadIO m => PeerMgr -> m ()
- Haskoin.Node: peerMgrVerAck :: MonadIO m => PeerMgr -> Peer -> m ()
- Haskoin.Node: peerMgrVersion :: MonadIO m => PeerMgr -> Peer -> Version -> m ()
- Haskoin.Node: pingPeer :: MonadUnliftIO m => Int -> Peer -> m Bool
- Haskoin.Node: runChainT :: ChainT m a -> Chain -> m a
- Haskoin.Node: sendMessage :: MonadIO m => Message -> Peer -> m ()
- Haskoin.Node: setBusy :: MonadIO m => Peer -> m Bool
- Haskoin.Node: setFree :: MonadIO m => Peer -> m ()
- Haskoin.Node: ticklePeer :: MonadIO m => PeerMgr -> Peer -> m ()
- Haskoin.Node: toHostService :: String -> (Maybe String, Maybe String)
- Haskoin.Node: toSockAddr :: Network -> String -> IO [SockAddr]
- Haskoin.Node: type ChainT (m :: Type -> Type) = ReaderT Chain m
- Haskoin.Node: type WithConnection = Conduits -> IO () -> IO ()
- Haskoin.Node: withChain :: MonadUnliftIO m => ChainConfig -> (Chain -> m a) -> m a
- Haskoin.Node: withLogger :: LogLevel -> (Logger -> IO a) -> IO a
- Haskoin.Node: withPeerMgr :: MonadUnliftIO m => PeerMgrConfig -> (PeerMgr -> m a) -> m a
- Haskoin.Node: withSource :: Text -> Logger -> Logger
- Haskoin.Node: wrapPeer :: PeerConfig -> TVar Bool -> TVar Int -> Mailbox PeerMessage -> Peer
+ Haskoin.Node.Address: isRoutable :: SockAddr -> Bool
+ Haskoin.Node.Address: toHostService :: String -> (Maybe String, Maybe String)
+ Haskoin.Node.Address: toSockAddr :: Network -> String -> IO [SockAddr]
+ Haskoin.Node.Address: withConnection :: SockAddr -> WithConnection
+ Haskoin.Node.Chain: ChainBestBlock :: !BlockNode -> ChainEvent
+ Haskoin.Node.Chain: ChainConfig :: !DB -> !Maybe ColumnFamily -> !Network -> !Publisher ChainEvent -> !NominalDiffTime -> !Logger -> ChainConfig
+ Haskoin.Node.Chain: ChainSynced :: !BlockNode -> ChainEvent
+ Haskoin.Node.Chain: [cf] :: ChainConfig -> !Maybe ColumnFamily
+ Haskoin.Node.Chain: [db] :: ChainConfig -> !DB
+ Haskoin.Node.Chain: [logger] :: ChainConfig -> !Logger
+ Haskoin.Node.Chain: [net] :: ChainConfig -> !Network
+ Haskoin.Node.Chain: [pub] :: ChainConfig -> !Publisher ChainEvent
+ Haskoin.Node.Chain: [timeout] :: ChainConfig -> !NominalDiffTime
+ Haskoin.Node.Chain: chainBlockMain :: Chain -> BlockHash -> IO Bool
+ Haskoin.Node.Chain: chainBlocksAnnounced :: Chain -> Peer -> [BlockHash] -> IO ()
+ Haskoin.Node.Chain: chainGetAncestor :: Chain -> BlockHeight -> BlockNode -> IO (Maybe BlockNode)
+ Haskoin.Node.Chain: chainGetBest :: Chain -> IO BlockNode
+ Haskoin.Node.Chain: chainGetBlock :: Chain -> BlockHash -> IO (Maybe BlockNode)
+ Haskoin.Node.Chain: chainGetParents :: Chain -> BlockHeight -> BlockNode -> IO [BlockNode]
+ Haskoin.Node.Chain: chainGetSplitBlock :: Chain -> BlockNode -> BlockNode -> IO BlockNode
+ Haskoin.Node.Chain: chainHeaders :: Chain -> Peer -> [BlockHeader] -> IO ()
+ Haskoin.Node.Chain: chainIsSynced :: Chain -> IO Bool
+ Haskoin.Node.Chain: chainPeerConnected :: Chain -> Peer -> IO ()
+ Haskoin.Node.Chain: chainPeerDisconnected :: Chain -> Peer -> IO ()
+ Haskoin.Node.Chain: chainRunHeaders :: Chain -> (forall (n :: Type -> Type). BlockHeaders n => n a) -> IO a
+ Haskoin.Node.Chain: chainSync :: Chain -> IO ()
+ Haskoin.Node.Chain: data Chain
+ Haskoin.Node.Chain: data ChainConfig
+ Haskoin.Node.Chain: data ChainEvent
+ Haskoin.Node.Chain: instance GHC.Classes.Eq Haskoin.Node.Chain.Chain
+ Haskoin.Node.Chain: instance GHC.Classes.Eq Haskoin.Node.Chain.ChainEvent
+ Haskoin.Node.Chain: instance GHC.Internal.Show.Show Haskoin.Node.Chain.ChainEvent
+ Haskoin.Node.Chain: withChain :: ChainConfig -> (Chain -> IO a) -> IO a
+ Haskoin.Node.Chain.Database: ChainDB :: !DB -> !Maybe ColumnFamily -> ChainDB
+ Haskoin.Node.Chain.Database: [cf] :: ChainDB -> !Maybe ColumnFamily
+ Haskoin.Node.Chain.Database: [db] :: ChainDB -> !DB
+ Haskoin.Node.Chain.Database: data ChainDB
+ Haskoin.Node.Chain.Database: initChainDB :: Network -> ChainDB -> IO ()
+ Haskoin.Node.Chain.Database: instance Data.Serialize.Serialize Haskoin.Node.Chain.Database.BestBlockKey
+ Haskoin.Node.Chain.Database: instance Data.Serialize.Serialize Haskoin.Node.Chain.Database.BlockHeaderKey
+ Haskoin.Node.Chain.Database: instance Data.Serialize.Serialize Haskoin.Node.Chain.Database.VersionKey
+ Haskoin.Node.Chain.Database: instance Database.RocksDB.Query.Key Haskoin.Node.Chain.Database.VersionKey
+ Haskoin.Node.Chain.Database: instance Database.RocksDB.Query.KeyValue Haskoin.Node.Chain.Database.BestBlockKey Haskoin.Block.Headers.BlockNode
+ Haskoin.Node.Chain.Database: instance Database.RocksDB.Query.KeyValue Haskoin.Node.Chain.Database.BlockHeaderKey Haskoin.Block.Headers.BlockNode
+ Haskoin.Node.Chain.Database: instance Database.RocksDB.Query.KeyValue Haskoin.Node.Chain.Database.VersionKey GHC.Internal.Word.Word32
+ Haskoin.Node.Chain.Database: instance GHC.Classes.Eq Haskoin.Node.Chain.Database.BestBlockKey
+ Haskoin.Node.Chain.Database: instance GHC.Classes.Eq Haskoin.Node.Chain.Database.BlockHeaderKey
+ Haskoin.Node.Chain.Database: instance GHC.Classes.Eq Haskoin.Node.Chain.Database.VersionKey
+ Haskoin.Node.Chain.Database: instance GHC.Internal.Show.Show Haskoin.Node.Chain.Database.BestBlockKey
+ Haskoin.Node.Chain.Database: instance GHC.Internal.Show.Show Haskoin.Node.Chain.Database.BlockHeaderKey
+ Haskoin.Node.Chain.Database: instance GHC.Internal.Show.Show Haskoin.Node.Chain.Database.VersionKey
+ Haskoin.Node.Chain.Database: instance Haskoin.Block.Headers.BlockHeaders (Control.Monad.Trans.Reader.ReaderT Haskoin.Node.Chain.Database.ChainDB GHC.Types.IO)
+ Haskoin.Node.Chain.Database: runHeaders :: ChainDB -> (forall (n :: Type -> Type). BlockHeaders n => n a) -> IO a
+ Haskoin.Node.Chain.Sync: Dropped :: PeerStatus
+ Haskoin.Node.Chain.Sync: Idle :: PeerStatus
+ Haskoin.Node.Chain.Sync: Queued :: PeerStatus
+ Haskoin.Node.Chain.Sync: Syncing :: !UTCTime -> !Bool -> PeerStatus
+ Haskoin.Node.Chain.Sync: connectedPeers :: SyncState p -> [p]
+ Haskoin.Node.Chain.Sync: data PeerStatus
+ Haskoin.Node.Chain.Sync: data SyncState p
+ Haskoin.Node.Chain.Sync: dropPeer :: Eq p => p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: emptySync :: UTCTime -> SyncState p
+ Haskoin.Node.Chain.Sync: finishSync :: Eq p => p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: instance GHC.Classes.Eq Haskoin.Node.Chain.Sync.PeerStatus
+ Haskoin.Node.Chain.Sync: instance GHC.Classes.Eq p => GHC.Classes.Eq (Haskoin.Node.Chain.Sync.SyncState p)
+ Haskoin.Node.Chain.Sync: instance GHC.Internal.Show.Show Haskoin.Node.Chain.Sync.PeerStatus
+ Haskoin.Node.Chain.Sync: instance GHC.Internal.Show.Show p => GHC.Internal.Show.Show (Haskoin.Node.Chain.Sync.SyncState p)
+ Haskoin.Node.Chain.Sync: isSynced :: Network -> UTCTime -> BlockNode -> SyncState p -> Bool
+ Haskoin.Node.Chain.Sync: lastActivity :: SyncState p -> UTCTime
+ Haskoin.Node.Chain.Sync: maxTipAge :: NominalDiffTime
+ Haskoin.Node.Chain.Sync: peerConnected :: Eq p => p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: peerDisconnected :: Eq p => p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: peerStatus :: Eq p => p -> SyncState p -> Maybe PeerStatus
+ Haskoin.Node.Chain.Sync: pollPeers :: UTCTime -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: queuedPeers :: SyncState p -> [p]
+ Haskoin.Node.Chain.Sync: refreshSync :: Eq p => UTCTime -> p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: requestSync :: Eq p => p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: startSync :: Eq p => UTCTime -> p -> SyncState p -> SyncState p
+ Haskoin.Node.Chain.Sync: syncingPeer :: SyncState p -> Maybe (p, UTCTime)
+ Haskoin.Node.Chain.Sync: touchSync :: UTCTime -> SyncState p -> SyncState p
+ Haskoin.Node.Logger: LevelDebug :: LogLevel
+ Haskoin.Node.Logger: LevelError :: LogLevel
+ Haskoin.Node.Logger: LevelInfo :: LogLevel
+ Haskoin.Node.Logger: LevelWarn :: LogLevel
+ Haskoin.Node.Logger: Logger :: !LogLevel -> !Text -> !LogStr -> IO () -> Logger
+ Haskoin.Node.Logger: [level] :: Logger -> !LogLevel
+ Haskoin.Node.Logger: [push] :: Logger -> !LogStr -> IO ()
+ Haskoin.Node.Logger: [source] :: Logger -> !Text
+ Haskoin.Node.Logger: data LogLevel
+ Haskoin.Node.Logger: data Logger
+ Haskoin.Node.Logger: instance GHC.Classes.Eq Haskoin.Node.Logger.LogLevel
+ Haskoin.Node.Logger: instance GHC.Classes.Ord Haskoin.Node.Logger.LogLevel
+ Haskoin.Node.Logger: instance GHC.Internal.Show.Show Haskoin.Node.Logger.LogLevel
+ Haskoin.Node.Logger: logAt :: LogLevel -> Logger -> Text -> IO ()
+ Haskoin.Node.Logger: logDebug :: Logger -> Text -> IO ()
+ Haskoin.Node.Logger: logError :: Logger -> Text -> IO ()
+ Haskoin.Node.Logger: logInfo :: Logger -> Text -> IO ()
+ Haskoin.Node.Logger: logWarn :: Logger -> Text -> IO ()
+ Haskoin.Node.Logger: withLogger :: LogLevel -> (Logger -> IO a) -> IO a
+ Haskoin.Node.Logger: withSource :: Text -> Logger -> Logger
+ Haskoin.Node.Loop: loopForever :: Monad m => m a -> m b
+ Haskoin.Node.Peer: Conduits :: ConduitT () ByteString IO () -> ConduitT ByteString Void IO () -> Conduits
+ Haskoin.Node.Peer: ConnectionClosed :: PeerException
+ Haskoin.Node.Peer: InvalidMessageHeader :: PeerException
+ Haskoin.Node.Peer: InvalidPayload :: !MessageCommand -> !String -> PeerException
+ Haskoin.Node.Peer: KillPeer :: PeerMessage
+ Haskoin.Node.Peer: MessageTooLarge :: !MessageCommand -> !Word32 -> PeerException
+ Haskoin.Node.Peer: Peer :: !Mailbox PeerMessage -> !Publisher PeerEvent -> !Text -> !TVar (Lock Owner) -> !TVar Int -> Peer
+ Haskoin.Node.Peer: PeerConfig :: !Publisher PeerEvent -> !Network -> !Text -> !WithConnection -> !Logger -> PeerConfig
+ Haskoin.Node.Peer: PeerConnected :: !Peer -> PeerEvent
+ Haskoin.Node.Peer: PeerDisconnected :: !Peer -> PeerEvent
+ Haskoin.Node.Peer: PeerMessage :: !Peer -> !Message -> PeerEvent
+ Haskoin.Node.Peer: SendMessage :: !Message -> PeerMessage
+ Haskoin.Node.Peer: WrongNetwork :: !Word32 -> PeerException
+ Haskoin.Node.Peer: [busy] :: Peer -> !TVar (Lock Owner)
+ Haskoin.Node.Peer: [connect] :: PeerConfig -> !WithConnection
+ Haskoin.Node.Peer: [inboundConduit] :: Conduits -> ConduitT () ByteString IO ()
+ Haskoin.Node.Peer: [label] :: Peer -> !Text
+ Haskoin.Node.Peer: [logger] :: PeerConfig -> !Logger
+ Haskoin.Node.Peer: [mailbox] :: Peer -> !Mailbox PeerMessage
+ Haskoin.Node.Peer: [net] :: PeerConfig -> !Network
+ Haskoin.Node.Peer: [outboundConduit] :: Conduits -> ConduitT ByteString Void IO ()
+ Haskoin.Node.Peer: [pending] :: Peer -> !TVar Int
+ Haskoin.Node.Peer: [pub] :: Peer -> !Publisher PeerEvent
+ Haskoin.Node.Peer: data Conduits
+ Haskoin.Node.Peer: data Owner
+ Haskoin.Node.Peer: data Peer
+ Haskoin.Node.Peer: data PeerConfig
+ Haskoin.Node.Peer: data PeerEvent
+ Haskoin.Node.Peer: data PeerException
+ Haskoin.Node.Peer: data PeerMessage
+ Haskoin.Node.Peer: getBlocks :: Network -> Int -> Peer -> [BlockHash] -> IO (Maybe [Block])
+ Haskoin.Node.Peer: getBusy :: Peer -> IO Bool
+ Haskoin.Node.Peer: getData :: Int -> Peer -> GetData -> IO (Maybe [Either Tx Block])
+ Haskoin.Node.Peer: getTxs :: Network -> Int -> Peer -> [TxHash] -> IO (Maybe [Tx])
+ Haskoin.Node.Peer: instance GHC.Classes.Eq Haskoin.Node.Peer.Peer
+ Haskoin.Node.Peer: instance GHC.Classes.Eq Haskoin.Node.Peer.PeerEvent
+ Haskoin.Node.Peer: instance GHC.Internal.Exception.Type.Exception Haskoin.Node.Peer.PeerException
+ Haskoin.Node.Peer: instance GHC.Internal.Show.Show Haskoin.Node.Peer.Peer
+ Haskoin.Node.Peer: instance GHC.Internal.Show.Show Haskoin.Node.Peer.PeerException
+ Haskoin.Node.Peer: killPeer :: Peer -> IO ()
+ Haskoin.Node.Peer: maxPendingMessages :: Int
+ Haskoin.Node.Peer: newOwner :: Text -> IO Owner
+ Haskoin.Node.Peer: ownerName :: Owner -> Text
+ Haskoin.Node.Peer: peer :: PeerConfig -> Peer -> Inbox PeerMessage -> IO ()
+ Haskoin.Node.Peer: peerHolder :: Peer -> IO (Maybe Owner)
+ Haskoin.Node.Peer: peerMessageDone :: Peer -> IO ()
+ Haskoin.Node.Peer: pingPeer :: Int -> Peer -> IO Bool
+ Haskoin.Node.Peer: releasePeer :: Owner -> Peer -> IO Bool
+ Haskoin.Node.Peer: reservePeer :: Owner -> Peer -> IO Bool
+ Haskoin.Node.Peer: sendMessage :: Message -> Peer -> IO ()
+ Haskoin.Node.Peer: setBusy :: Peer -> IO Bool
+ Haskoin.Node.Peer: setFree :: Peer -> IO ()
+ Haskoin.Node.Peer: type WithConnection = Conduits -> IO () -> IO ()
+ Haskoin.Node.Peer: wrapPeer :: PeerConfig -> TVar (Lock Owner) -> TVar Int -> Mailbox PeerMessage -> Peer
+ Haskoin.Node.Peer.Lock: Free :: Lock o
+ Haskoin.Node.Peer.Lock: HeldBy :: !o -> Lock o
+ Haskoin.Node.Peer.Lock: acquire :: o -> Lock o -> Maybe (Lock o)
+ Haskoin.Node.Peer.Lock: data Lock o
+ Haskoin.Node.Peer.Lock: data Owner
+ Haskoin.Node.Peer.Lock: holder :: Lock o -> Maybe o
+ Haskoin.Node.Peer.Lock: instance GHC.Classes.Eq Haskoin.Node.Peer.Lock.Owner
+ Haskoin.Node.Peer.Lock: instance GHC.Classes.Eq o => GHC.Classes.Eq (Haskoin.Node.Peer.Lock.Lock o)
+ Haskoin.Node.Peer.Lock: instance GHC.Internal.Show.Show Haskoin.Node.Peer.Lock.Owner
+ Haskoin.Node.Peer.Lock: instance GHC.Internal.Show.Show o => GHC.Internal.Show.Show (Haskoin.Node.Peer.Lock.Lock o)
+ Haskoin.Node.Peer.Lock: legacyOwner :: Owner
+ Haskoin.Node.Peer.Lock: newOwner :: Text -> IO Owner
+ Haskoin.Node.Peer.Lock: ownerName :: Owner -> Text
+ Haskoin.Node.Peer.Lock: release :: Eq o => o -> Lock o -> Maybe (Lock o)
+ Haskoin.Node.Peer.Request: Complete :: ![Either Tx Block] -> RequestStep
+ Haskoin.Node.Peer.Request: Continue :: !Request -> RequestStep
+ Haskoin.Node.Peer.Request: Failed :: !Text -> RequestStep
+ Haskoin.Node.Peer.Request: data Request
+ Haskoin.Node.Peer.Request: data RequestStep
+ Haskoin.Node.Peer.Request: instance GHC.Classes.Eq Haskoin.Node.Peer.Request.Request
+ Haskoin.Node.Peer.Request: instance GHC.Classes.Eq Haskoin.Node.Peer.Request.RequestStep
+ Haskoin.Node.Peer.Request: instance GHC.Internal.Show.Show Haskoin.Node.Peer.Request.Request
+ Haskoin.Node.Peer.Request: instance GHC.Internal.Show.Show Haskoin.Node.Peer.Request.RequestStep
+ Haskoin.Node.Peer.Request: newRequest :: Word64 -> [InvVector] -> RequestStep
+ Haskoin.Node.Peer.Request: stepRequest :: Request -> Message -> RequestStep
+ Haskoin.Node.PeerMgr: Disconnecting :: !DisconnectReason -> Stage
+ Haskoin.Node.PeerMgr: HandshakeTimeout :: DisconnectReason
+ Haskoin.Node.PeerMgr: Handshaking :: !Handshake -> Stage
+ Haskoin.Node.PeerMgr: NoPong :: DisconnectReason
+ Haskoin.Node.PeerMgr: Online :: !Version -> !Maybe (UTCTime, Word64) -> Stage
+ Haskoin.Node.PeerMgr: OnlinePeer :: !SockAddr -> !PeerState -> !Async () -> !Peer -> !Word64 -> OnlinePeer
+ Haskoin.Node.PeerMgr: PeerConnected :: !Peer -> PeerEvent
+ Haskoin.Node.PeerMgr: PeerDisconnected :: !Peer -> PeerEvent
+ Haskoin.Node.PeerMgr: PeerMessage :: !Peer -> !Message -> PeerEvent
+ Haskoin.Node.PeerMgr: PeerMgrConfig :: !Int -> ![String] -> !Bool -> !NetworkAddress -> !Network -> !Publisher PeerEvent -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Logger -> PeerMgrConfig
+ Haskoin.Node.PeerMgr: PeerState :: !Stage -> !UTCTime -> !UTCTime -> ![NominalDiffTime] -> !Bool -> PeerState
+ Haskoin.Node.PeerMgr: Rejected :: !HandshakeError -> DisconnectReason
+ Haskoin.Node.PeerMgr: Silent :: DisconnectReason
+ Haskoin.Node.PeerMgr: TooOld :: DisconnectReason
+ Haskoin.Node.PeerMgr: [address] :: OnlinePeer -> !SockAddr
+ Haskoin.Node.PeerMgr: [announced] :: PeerState -> !Bool
+ Haskoin.Node.PeerMgr: [async] :: OnlinePeer -> !Async ()
+ Haskoin.Node.PeerMgr: [connect] :: PeerMgrConfig -> !SockAddr -> WithConnection
+ Haskoin.Node.PeerMgr: [connected] :: PeerState -> !UTCTime
+ Haskoin.Node.PeerMgr: [discover] :: PeerMgrConfig -> !Bool
+ Haskoin.Node.PeerMgr: [lastHeard] :: PeerState -> !UTCTime
+ Haskoin.Node.PeerMgr: [logger] :: PeerMgrConfig -> !Logger
+ Haskoin.Node.PeerMgr: [mailbox] :: OnlinePeer -> !Peer
+ Haskoin.Node.PeerMgr: [maxPeerLife] :: PeerMgrConfig -> !NominalDiffTime
+ Haskoin.Node.PeerMgr: [maxPeers] :: PeerMgrConfig -> !Int
+ Haskoin.Node.PeerMgr: [net] :: PeerMgrConfig -> !Network
+ Haskoin.Node.PeerMgr: [nonce] :: OnlinePeer -> !Word64
+ Haskoin.Node.PeerMgr: [peers] :: PeerMgrConfig -> ![String]
+ Haskoin.Node.PeerMgr: [pings] :: PeerState -> ![NominalDiffTime]
+ Haskoin.Node.PeerMgr: [pub] :: PeerMgrConfig -> !Publisher PeerEvent
+ Haskoin.Node.PeerMgr: [stage] :: PeerState -> !Stage
+ Haskoin.Node.PeerMgr: [state] :: OnlinePeer -> !PeerState
+ Haskoin.Node.PeerMgr: [timeout] :: PeerMgrConfig -> !NominalDiffTime
+ Haskoin.Node.PeerMgr: buildVersion :: Network -> Word64 -> BlockHeight -> NetworkAddress -> NetworkAddress -> Word64 -> Version
+ Haskoin.Node.PeerMgr: data DisconnectReason
+ Haskoin.Node.PeerMgr: data OnlinePeer
+ Haskoin.Node.PeerMgr: data PeerEvent
+ Haskoin.Node.PeerMgr: data PeerMgr
+ Haskoin.Node.PeerMgr: data PeerMgrConfig
+ Haskoin.Node.PeerMgr: data PeerState
+ Haskoin.Node.PeerMgr: data Stage
+ Haskoin.Node.PeerMgr: getOnlinePeer :: PeerMgr -> Peer -> IO (Maybe OnlinePeer)
+ Haskoin.Node.PeerMgr: getPeers :: PeerMgr -> IO [OnlinePeer]
+ Haskoin.Node.PeerMgr: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.OnlinePeer
+ Haskoin.Node.PeerMgr: instance GHC.Classes.Ord Haskoin.Node.PeerMgr.OnlinePeer
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "connected" Haskoin.Node.PeerMgr.OnlinePeer Data.Time.Clock.Internal.UTCTime.UTCTime
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "online" Haskoin.Node.PeerMgr.OnlinePeer GHC.Types.Bool
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "ping" Haskoin.Node.PeerMgr.OnlinePeer (GHC.Internal.Maybe.Maybe (Data.Time.Clock.Internal.UTCTime.UTCTime, GHC.Internal.Word.Word64))
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "pings" Haskoin.Node.PeerMgr.OnlinePeer [Data.Time.Clock.Internal.NominalDiffTime.NominalDiffTime]
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "tickled" Haskoin.Node.PeerMgr.OnlinePeer Data.Time.Clock.Internal.UTCTime.UTCTime
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "verack" Haskoin.Node.PeerMgr.OnlinePeer GHC.Types.Bool
+ Haskoin.Node.PeerMgr: instance GHC.Internal.Records.HasField "version" Haskoin.Node.PeerMgr.OnlinePeer (GHC.Internal.Maybe.Maybe Haskoin.Network.Common.Version)
+ Haskoin.Node.PeerMgr: isRoutable :: SockAddr -> Bool
+ Haskoin.Node.PeerMgr: myVersion :: Word32
+ Haskoin.Node.PeerMgr: peerMgrAddrs :: PeerMgr -> Peer -> [NetworkAddress] -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrBest :: PeerMgr -> BlockHeight -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrPing :: PeerMgr -> Peer -> Word64 -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrPong :: PeerMgr -> Peer -> Word64 -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrSync :: PeerMgr -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrVerAck :: PeerMgr -> Peer -> IO ()
+ Haskoin.Node.PeerMgr: peerMgrVersion :: PeerMgr -> Peer -> Version -> IO ()
+ Haskoin.Node.PeerMgr: ticklePeer :: PeerMgr -> Peer -> IO ()
+ Haskoin.Node.PeerMgr: toHostService :: String -> (Maybe String, Maybe String)
+ Haskoin.Node.PeerMgr: toSockAddr :: Network -> String -> IO [SockAddr]
+ Haskoin.Node.PeerMgr: withPeerMgr :: PeerMgrConfig -> (PeerMgr -> IO a) -> IO a
+ Haskoin.Node.PeerMgr.Addresses: Configured :: Source
+ Haskoin.Node.PeerMgr.Addresses: Disconnected :: Outcome
+ Haskoin.Node.PeerMgr.Addresses: Discovered :: Source
+ Haskoin.Node.PeerMgr.Addresses: Failed :: Outcome
+ Haskoin.Node.PeerMgr.Addresses: InUse :: AddressState
+ Haskoin.Node.PeerMgr.Addresses: Misbehaved :: Outcome
+ Haskoin.Node.PeerMgr.Addresses: Ready :: AddressState
+ Haskoin.Node.PeerMgr.Addresses: Waiting :: !UTCTime -> !Int -> AddressState
+ Haskoin.Node.PeerMgr.Addresses: addAddress :: Ord a => Source -> a -> AddressBook a -> AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: addressState :: Ord a => a -> AddressBook a -> Maybe AddressState
+ Haskoin.Node.PeerMgr.Addresses: banTime :: NominalDiffTime
+ Haskoin.Node.PeerMgr.Addresses: data AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: data AddressState
+ Haskoin.Node.PeerMgr.Addresses: data Outcome
+ Haskoin.Node.PeerMgr.Addresses: data Source
+ Haskoin.Node.PeerMgr.Addresses: emptyBook :: AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: finishAddress :: Ord a => UTCTime -> Outcome -> a -> AddressBook a -> AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Addresses.AddressState
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Addresses.Entry
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Addresses.Outcome
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Addresses.Source
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Classes.Eq a => GHC.Classes.Eq (Haskoin.Node.PeerMgr.Addresses.AddressBook a)
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Addresses.AddressState
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Addresses.Entry
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Addresses.Outcome
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Addresses.Source
+ Haskoin.Node.PeerMgr.Addresses: instance GHC.Internal.Show.Show a => GHC.Internal.Show.Show (Haskoin.Node.PeerMgr.Addresses.AddressBook a)
+ Haskoin.Node.PeerMgr.Addresses: knownAddresses :: AddressBook a -> [a]
+ Haskoin.Node.PeerMgr.Addresses: markConnected :: Ord a => a -> AddressBook a -> AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: markLoaded :: UTCTime -> AddressBook a -> AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: maxBackoff :: NominalDiffTime
+ Haskoin.Node.PeerMgr.Addresses: maxDiscovered :: Int
+ Haskoin.Node.PeerMgr.Addresses: needsReload :: Ord a => UTCTime -> AddressBook a -> Bool
+ Haskoin.Node.PeerMgr.Addresses: reloadInterval :: NominalDiffTime
+ Haskoin.Node.PeerMgr.Addresses: retryDelay :: NominalDiffTime
+ Haskoin.Node.PeerMgr.Addresses: returnAddress :: Ord a => a -> AddressBook a -> AddressBook a
+ Haskoin.Node.PeerMgr.Addresses: takeAddress :: Ord a => UTCTime -> AddressBook a -> Maybe (a, AddressBook a)
+ Haskoin.Node.PeerMgr.Handshake: AnnounceConnected :: HandshakeAction
+ Haskoin.Node.PeerMgr.Handshake: AwaitingBoth :: Handshake
+ Haskoin.Node.PeerMgr.Handshake: AwaitingVerAck :: !Version -> Handshake
+ Haskoin.Node.PeerMgr.Handshake: AwaitingVersion :: Handshake
+ Haskoin.Node.PeerMgr.Handshake: ConnectedToSelf :: HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: DuplicateVerAck :: HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: DuplicateVersion :: HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: Established :: !Version -> Handshake
+ Haskoin.Node.PeerMgr.Handshake: GotVerAck :: HandshakeEvent
+ Haskoin.Node.PeerMgr.Handshake: GotVersion :: !Version -> HandshakeEvent
+ Haskoin.Node.PeerMgr.Handshake: NotFullNode :: HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: SendVerAck :: HandshakeAction
+ Haskoin.Node.PeerMgr.Handshake: checkVersion :: [Word64] -> Version -> Either HandshakeError Version
+ Haskoin.Node.PeerMgr.Handshake: data Handshake
+ Haskoin.Node.PeerMgr.Handshake: data HandshakeAction
+ Haskoin.Node.PeerMgr.Handshake: data HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: data HandshakeEvent
+ Haskoin.Node.PeerMgr.Handshake: handshakeVersion :: Handshake -> Maybe Version
+ Haskoin.Node.PeerMgr.Handshake: hasVerAck :: Handshake -> Bool
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Handshake.Handshake
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Handshake.HandshakeAction
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Handshake.HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Handshake.HandshakeEvent
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Handshake.Handshake
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Handshake.HandshakeAction
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Handshake.HandshakeError
+ Haskoin.Node.PeerMgr.Handshake: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Handshake.HandshakeEvent
+ Haskoin.Node.PeerMgr.Handshake: isEstablished :: Handshake -> Bool
+ Haskoin.Node.PeerMgr.Handshake: stepHandshake :: Handshake -> HandshakeEvent -> Either HandshakeError (Handshake, [HandshakeAction])
+ Haskoin.Node.PeerMgr.Lifecycle: Announce :: PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: Disconnect :: !DisconnectReason -> PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: Disconnecting :: !DisconnectReason -> Stage
+ Haskoin.Node.PeerMgr.Lifecycle: GotHandshake :: !HandshakeEvent -> PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: GotPong :: !UTCTime -> !Word64 -> PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: HandshakeTimeout :: DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: Handshaking :: !Handshake -> Stage
+ Haskoin.Node.PeerMgr.Lifecycle: Heard :: !UTCTime -> PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: Limits :: !NominalDiffTime -> !NominalDiffTime -> Limits
+ Haskoin.Node.PeerMgr.Lifecycle: NoPong :: DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: Online :: !Version -> !Maybe (UTCTime, Word64) -> Stage
+ Haskoin.Node.PeerMgr.Lifecycle: PeerState :: !Stage -> !UTCTime -> !UTCTime -> ![NominalDiffTime] -> !Bool -> PeerState
+ Haskoin.Node.PeerMgr.Lifecycle: Refused :: !HandshakeError -> PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: Rejected :: !HandshakeError -> DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: ReplyVerAck :: PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: SendPing :: !Word64 -> PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: Silent :: DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: Tick :: !UTCTime -> !Word64 -> PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: TooOld :: DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: [announced] :: PeerState -> !Bool
+ Haskoin.Node.PeerMgr.Lifecycle: [connected] :: PeerState -> !UTCTime
+ Haskoin.Node.PeerMgr.Lifecycle: [lastHeard] :: PeerState -> !UTCTime
+ Haskoin.Node.PeerMgr.Lifecycle: [maxLife] :: Limits -> !NominalDiffTime
+ Haskoin.Node.PeerMgr.Lifecycle: [pings] :: PeerState -> ![NominalDiffTime]
+ Haskoin.Node.PeerMgr.Lifecycle: [stage] :: PeerState -> !Stage
+ Haskoin.Node.PeerMgr.Lifecycle: [timeout] :: Limits -> !NominalDiffTime
+ Haskoin.Node.PeerMgr.Lifecycle: data DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: data Limits
+ Haskoin.Node.PeerMgr.Lifecycle: data PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: data PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: data PeerState
+ Haskoin.Node.PeerMgr.Lifecycle: data Stage
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.Limits
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.PeerState
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Classes.Eq Haskoin.Node.PeerMgr.Lifecycle.Stage
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.DisconnectReason
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.Limits
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.PeerAction
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.PeerInput
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.PeerState
+ Haskoin.Node.PeerMgr.Lifecycle: instance GHC.Internal.Show.Show Haskoin.Node.PeerMgr.Lifecycle.Stage
+ Haskoin.Node.PeerMgr.Lifecycle: isOnline :: PeerState -> Bool
+ Haskoin.Node.PeerMgr.Lifecycle: newPeerState :: UTCTime -> PeerState
+ Haskoin.Node.PeerMgr.Lifecycle: pendingPing :: PeerState -> Maybe (UTCTime, Word64)
+ Haskoin.Node.PeerMgr.Lifecycle: stageVersion :: PeerState -> Maybe Version
+ Haskoin.Node.PeerMgr.Lifecycle: stepPeer :: Limits -> PeerInput -> PeerState -> (PeerState, [PeerAction])
- Haskoin.Node: [address] :: PeerMgrConfig -> !NetworkAddress
+ Haskoin.Node: [address] :: NodeConfig -> !NetworkAddress
- Haskoin.Node: [cf] :: ChainConfig -> !Maybe ColumnFamily
+ Haskoin.Node: [cf] :: NodeConfig -> !Maybe ColumnFamily
- Haskoin.Node: [connect] :: PeerMgrConfig -> !SockAddr -> WithConnection
+ Haskoin.Node: [connect] :: NodeConfig -> !SockAddr -> WithConnection
- Haskoin.Node: [db] :: ChainConfig -> !DB
+ Haskoin.Node: [db] :: NodeConfig -> !DB
- Haskoin.Node: [discover] :: PeerMgrConfig -> !Bool
+ Haskoin.Node: [discover] :: NodeConfig -> !Bool
- Haskoin.Node: [logger] :: ChainConfig -> !Logger
+ Haskoin.Node: [logger] :: NodeConfig -> !Logger
- Haskoin.Node: [maxPeerLife] :: PeerMgrConfig -> !NominalDiffTime
+ Haskoin.Node: [maxPeerLife] :: NodeConfig -> !NominalDiffTime
- Haskoin.Node: [maxPeers] :: PeerMgrConfig -> !Int
+ Haskoin.Node: [maxPeers] :: NodeConfig -> !Int
- Haskoin.Node: [net] :: ChainConfig -> !Network
+ Haskoin.Node: [net] :: NodeConfig -> !Network
- Haskoin.Node: [peers] :: PeerMgrConfig -> ![String]
+ Haskoin.Node: [peers] :: NodeConfig -> ![String]
- Haskoin.Node: [pub] :: ChainConfig -> !Publisher ChainEvent
+ Haskoin.Node: [pub] :: NodeConfig -> !Publisher NodeEvent
- Haskoin.Node: [timeout] :: ChainConfig -> !NominalDiffTime
+ Haskoin.Node: [timeout] :: NodeConfig -> !NominalDiffTime
- Haskoin.Node: withNode :: MonadUnliftIO m => NodeConfig -> (Node -> m a) -> m a
+ Haskoin.Node: withNode :: NodeConfig -> (Node -> IO a) -> IO a
Files
- CHANGELOG.md +93/−0
- README.md +121/−20
- Setup.hs +0/−2
- haskoin-node.cabal +104/−44
- src/Haskoin/Node.hs +68/−68
- src/Haskoin/Node/Address.hs +157/−0
- src/Haskoin/Node/Chain.hs +309/−525
- src/Haskoin/Node/Chain/Database.hs +169/−0
- src/Haskoin/Node/Chain/Sync.hs +204/−0
- src/Haskoin/Node/Logger.hs +5/−11
- src/Haskoin/Node/Peer.hs +145/−147
- src/Haskoin/Node/Peer/Lock.hs +102/−0
- src/Haskoin/Node/Peer/Request.hs +104/−0
- src/Haskoin/Node/PeerMgr.hs +332/−561
- src/Haskoin/Node/PeerMgr/Addresses.hs +208/−0
- src/Haskoin/Node/PeerMgr/Handshake.hs +142/−0
- src/Haskoin/Node/PeerMgr/Lifecycle.hs +209/−0
- test/Haskoin/NodeSpec.hs +0/−552
- test/Spec.hs +0/−1
- test/common/Haskoin/Node/Test.hs +391/−0
- test/regtest/Containerfile +49/−0
- test/regtest/Main.hs +196/−0
- test/regtest/Regtest/Podman.hs +231/−0
- test/unit/Haskoin/Node/AddressSpec.hs +45/−0
- test/unit/Haskoin/Node/Chain/SyncSpec.hs +154/−0
- test/unit/Haskoin/Node/ChainSpec.hs +407/−0
- test/unit/Haskoin/Node/Peer/LockSpec.hs +60/−0
- test/unit/Haskoin/Node/Peer/RequestSpec.hs +90/−0
- test/unit/Haskoin/Node/PeerMgr/AddressesSpec.hs +139/−0
- test/unit/Haskoin/Node/PeerMgr/HandshakeSpec.hs +95/−0
- test/unit/Haskoin/Node/PeerMgr/LifecycleSpec.hs +157/−0
- test/unit/Haskoin/Node/PeerMgrSpec.hs +228/−0
- test/unit/Haskoin/Node/PeerSpec.hs +246/−0
- test/unit/Haskoin/NodeSpec.hs +525/−0
- test/unit/Spec.hs +1/−0
CHANGELOG.md view
@@ -4,6 +4,99 @@ 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.4.0] - 2026-11-09++### Added++- `chainBlocksAnnounced`: the node passes blocks announced in inventory+ messages to the chain, which asks the peer for headers if it does not know+ them.+- Pure state machines, each with property tests: the life of a connection+ (`Haskoin.Node.PeerMgr.Lifecycle`) and its version handshake+ (`Haskoin.Node.PeerMgr.Handshake`), the address book+ (`Haskoin.Node.PeerMgr.Addresses`), reserving a peer+ (`Haskoin.Node.Peer.Lock`), choosing which peer to ask for headers+ (`Haskoin.Node.Chain.Sync`), and receiving requested blocks and transactions+ (`Haskoin.Node.Peer.Request`).+- `isSynced` says whether the header chain counts as synced, as a pure+ function.+- `newOwner`, `reservePeer`, `releasePeer` and `peerHolder` reserve a peer for+ an owner, so that only that owner can release it.+- `PeerException` says why a peer connection ended, instead of an `error`+ message.+- `Haskoin.Node.Address` and `Haskoin.Node.Chain.Database` hold code split+ out of the peer manager, the node and the chain, which still re-export it.+ Every module is exposed, and `PeerMessage` is exported.+- Unit tests of the chain, peer and peer manager processes against in-memory+ mock peers.+- A regtest suite that runs the node against Bitcoin Core 32.1 and Bitcoin Cash+ Node 29.2.0 in Podman, from official binaries with checked hashes, and a CI+ job that runs it.+- `cabal.project`, so the package builds with Cabal too.+- `chainRunHeaders` runs header functions of haskoin-core, such as+ `getAncestor` or `blockLocator`, against the headers of a chain, and+ `runHeaders` does the same for a `ChainDB`.++### Changed++- If no headers are requested or received for `timeout` seconds, every+ connected peer is asked for headers again. This bounds how long any stall of+ the header chain can last.+- The chain checks for timeouts at intervals of at most a quarter of+ `timeout`, between 0.1 and 20 seconds, instead of every 2 to 20 seconds.+- Breaking: `OnlinePeer` holds the state of the connection in a `state` field+ instead of the `verack`, `online`, `version`, `ping`, `pings`, `connected`+ and `tickled` fields. These are still available as computed fields, so+ `o.version` and `o.online` keep working, but code that builds or pattern+ matches `OnlinePeer` records must change.+- Breaking: the busy flag of a `Peer` is a lock held by an owner, so `wrapPeer`+ takes a `TVar (Lock Owner)`. `setBusy` and `setFree` are deprecated: they act+ for a single owner shared by all their callers, and `setFree` no longer+ releases a peer that another owner reserved. The chain reserves peers as its+ own owner.+- A peer told to disconnect is no longer returned by `getPeers`, and its+ messages are no longer routed, even before its connection closes.+- A peer that does not complete the handshake within `timeout` seconds is+ disconnected, even if it keeps sending other messages.+- An address whose connection fails waits before it is tried again, twice as+ long after each failure up to ten minutes, and a relayed address whose peer+ broke the protocol waits a day. Before, failing addresses were retried+ within seconds forever. The configured peers and seeds are resolved again at+ most every five minutes.+- Breaking: `ChainT` and `runChainT` are removed, along with the+ `BlockHeaders` instances for `ReaderT Chain m` and `ReaderT ChainConfig m`.+ Use `chainRunHeaders` instead. The package no longer uses monad+ transformers outside `Haskoin.Node.Chain.Database`, where a `ReaderT` carries+ the database for haskoin-core's header functions; `withNode` nests its+ resources directly instead of through `ContT`.+- Breaking: every function runs in `IO`. The `MonadIO` and `MonadUnliftIO`+ constraints are gone, so code in another monad must call the node with+ `liftIO`. The library depends on async and stm instead of unliftio.+- The package uses the GHC2024 language edition, strict warnings and upper+ bounds on its dependencies. `Setup.hs`, `hie.yaml` and `.hindent.yaml` were+ removed.+- `stack.yaml` builds against secp256k1-haskell 2.0.1. The README's Stack+ example names the same version.++### Fixed++- Peers were ranked by the newest ping and the ten fastest ever measured,+ instead of the last eleven: slow recent pings were dropped. A peer that+ became slow kept ranking as fast.+- The header chain stopped following the network when a peer announced new+ blocks with an inventory message instead of headers, which peers do when+ many blocks are found at once. The chain stayed at the old height until the+ peer reconnected.+- A peer that announced headers that did not connect to the chain was+ disconnected. It is now asked for headers.+- A syncing peer that timed out kept the chain waiting until its disconnection+ arrived. The chain now moves on at once.+- A full batch of headers from a peer that was not being synced against was+ not followed up.+- `getData`, `getBlocks` and `getTxs` failed when the peer sent an unrelated+ message, or a notfound message for items that were not requested, before all+ the requested items arrived.+ ## [2.3.1] - 2026-11-07 ### Changed
README.md view
@@ -70,6 +70,18 @@ - **Small, isolated components.** The peer manager, the header chain and each connection are separate [NQE](https://github.com/jprupp/nqe) processes with their own state. One misbehaving peer dies alone.+- **Explicit protocol states.** What the node expects from each peer, during the+ handshake, while syncing headers and while receiving requested data, is+ written down as a pure state machine. A message that does not fit the current+ state is refused instead of handled by accident, and every rule is tested+ without a network.+- **Plain `IO`.** Every function runs in `IO`, with no monad transformers and+ no `MonadIO` or `MonadUnliftIO` constraints. Types say exactly what runs, GHC+ has nothing to specialise, and long-running loops cannot leak through an+ unspecialised dictionary. From your own monad, call the node with `liftIO`.+- **Never stuck.** Every hint that a peer has blocks the node lacks makes it ask+ for headers, and a quiet period makes it ask every peer, so a missed+ announcement cannot leave the chain behind. ## Features @@ -77,11 +89,11 @@ | --------------- | ---------------------------------------------------------------------------------------------- | | Networks | Every network of haskoin-core: Bitcoin and Bitcoin Cash mainnet, testnets and regtest | | Peer discovery | DNS seeds and addresses relayed by peers, or a static list of peers |-| Peer management | Version handshake, ping-based latency, peers sorted by median ping, rotation after a maximum age |-| Header sync | Headers downloaded from one peer at a time, connected to the chain with the most work, stored in RocksDB |+| Peer management | Version handshake, ping-based latency, peers sorted by median ping, rotation after a maximum age, retry backoff and bans for bad addresses |+| Header sync | Headers downloaded from one peer at a time, connected to the chain with the most work, stored in RocksDB; new blocks followed whether peers announce them with headers or inventory, and peers polled after a quiet period | | Events | Best header changes, sync completion, peer connections and every message received | | Data requests | Fetch blocks and transactions from a peer, with a timeout |-| Protection | Strict parsing, per-command size limits, back-pressure, bounded address pool |+| Protection | Strict parsing, per-command size limits, back-pressure, bounded address pool, explicit handshake states | ### What is not included @@ -150,9 +162,9 @@ ```yaml extra-deps:- - haskoin-node-2.3.1+ - haskoin-node-2.4.0 - haskoin-core-1.6.1- - secp256k1-haskell-2.0.0+ - secp256k1-haskell-2.0.1 - base16-2.0 - rocksdb-haskell-jprupp-2.6.1 - rocksdb-query-0.6.1@@ -312,11 +324,28 @@ - The **peer manager** (`Haskoin.Node.PeerMgr`) finds peer addresses, connects to them, does the version handshake, pings them, and drops those that are slow,- silent, misbehaving or too old.+ silent, misbehaving or too old. An address whose connection failed is tried+ again later each time, and one whose peer misbehaved waits a day. - The **chain** (`Haskoin.Node.Chain`) asks one peer at a time for headers, connects them to the headers it has, follows the chain with the most work, and- stores the headers in RocksDB.+ stores the headers in RocksDB. It asks a peer for headers whenever the peer+ announces blocks it does not know, with headers or with inventory, and asks+ every peer when it has heard nothing for `timeout` seconds. +The rules for talking to peers are pure state machines, each in its own module:++| Module | Rules |+| -------------------------------- | -------------------------------------------------------------- |+| `Haskoin.Node.PeerMgr.Lifecycle` | Life of a connection: handshake, pings, timers, disconnection |+| `Haskoin.Node.PeerMgr.Handshake` | The version handshake, the first stage of a connection |+| `Haskoin.Node.PeerMgr.Addresses` | Which address to try next, and when to retry one |+| `Haskoin.Node.Chain.Sync` | Which peer to ask for headers, and when headers count as synced |+| `Haskoin.Node.Peer.Request` | Receiving requested blocks and transactions |+| `Haskoin.Node.Peer.Lock` | Who may hold a peer for a long conversation |++The processes store the state and carry out what a transition says. Time is an+input to the transitions, so every timing rule is tested without waiting.+ Each peer connection is its own process (`Haskoin.Node.Peer`), run by a supervisor inside the peer manager. @@ -353,7 +382,8 @@ | `0x92` | data version | You can keep your own data in the same database, as Haskoin Store does, as long-as your keys do not start with these bytes.+as your keys do not start with these bytes. `Haskoin.Node.Chain.Database` has+the code. ## Usage guide @@ -371,9 +401,28 @@ ``` `getBlocks`, `getTxs` and `getData` return `Nothing` if the peer does not send-everything, in order, before the timeout in seconds. `pingPeer` measures a round-trip.+everything, in order, before the timeout in seconds. Unrelated messages that the+peer sends in between are ignored. `pingPeer` measures a round trip. +`getPeers` leaves out peers that are being disconnected, even before their+connection closes.++For a conversation that spans many messages, such as downloading a long run of+blocks, reserve the peer first so that no other task uses it at the same time.+The header chain does the same while it syncs. Only the owner that reserved a+peer can release it:++```haskell+owner <- newOwner "downloader"+reserved <- reservePeer owner p+when reserved $ do+ -- ... long conversation with p ...+ void (releasePeer owner p)+```++`setBusy` and `setFree` still work, but are deprecated: they act for a single+owner shared by all their callers.+ ### Querying headers ```haskell@@ -385,7 +434,7 @@ synced <- chainIsSynced node.chain ``` -`runChainT` runs the header functions of haskoin-core, such as `getAncestor`,+`chainRunHeaders` runs the header functions of haskoin-core, such as `getAncestor`, against the database of the chain. ### Static peers@@ -420,26 +469,43 @@ | `address` | This node's address, sent in version messages | | `net` | Network constants from haskoin-core (`btc`, `bch`, `btcTest`, ...) | | `pub` | Publisher that receives every `NodeEvent` |-| `timeout` | Seconds of silence, or without a pong, before a peer is dropped |+| `timeout` | Seconds of silence, without a pong, or without completing the handshake, before a peer is dropped; also how long the syncing peer may take to send headers, and the quiet period after which every peer is asked for headers | | `maxPeerLife` | Seconds after which a peer is dropped, so that connections rotate | | `connect` | How to open a connection; usually `withConnection` | | `logger` | Where to log; see `withLogger` | ## Module overview -`Haskoin.Node` is the only exposed module. It re-exports:+`Haskoin.Node` re-exports everything a program needs: | Part | Contents | | -------------- | --------------------------------------------------------------------------- | | Node | `NodeConfig`, `NodeEvent`, `Node`, `withNode`, `withConnection` |-| Peers | `Peer`, `PeerEvent`, `sendMessage`, `getBlocks`, `getTxs`, `getData`, `pingPeer`, `killPeer` |-| Peer manager | `PeerMgr`, `OnlinePeer`, `getPeers`, `getOnlinePeer`, `toSockAddr`, `isRoutable` |-| Chain | `Chain`, `ChainEvent`, `chainGetBest`, `chainGetBlock`, `chainGetAncestor`, `chainGetParents`, `chainGetSplitBlock`, `chainBlockMain`, `chainIsSynced`, `runChainT` |+| Peers | `Peer`, `PeerEvent`, `PeerException`, `sendMessage`, `getBlocks`, `getTxs`, `getData`, `pingPeer`, `killPeer`, `newOwner`, `reservePeer`, `releasePeer` |+| Peer manager | `PeerMgr`, `OnlinePeer`, `PeerState`, `Stage`, `DisconnectReason`, `getPeers`, `getOnlinePeer`, `toSockAddr`, `isRoutable` |+| Chain | `Chain`, `ChainEvent`, `chainGetBest`, `chainGetBlock`, `chainGetAncestor`, `chainGetParents`, `chainGetSplitBlock`, `chainBlockMain`, `chainIsSynced`, `chainRunHeaders` | | Logging | `Logger`, `LogLevel`, `withLogger`, `withSource`, `logDebug` ... `logError` | The lower-level functions that feed messages to the peer manager and the chain are exported too, for programs that wire the components themselves. +Every module is exposed and can be imported on its own:++| Module | Purpose |+| -------------------------------- | ----------------------------------------------------------- |+| `Haskoin.Node.Peer` | The process that talks to one peer, and data requests |+| `Haskoin.Node.Peer.Request` | State machine for receiving requested blocks and transactions |+| `Haskoin.Node.Peer.Lock` | State machine for reserving a peer |+| `Haskoin.Node.PeerMgr` | Finds, connects, ranks and drops peers |+| `Haskoin.Node.PeerMgr.Lifecycle` | State machine for the life of a connection |+| `Haskoin.Node.PeerMgr.Handshake` | State machine for the version handshake |+| `Haskoin.Node.PeerMgr.Addresses` | State machine for the address book |+| `Haskoin.Node.Chain` | Keeps the header chain in sync |+| `Haskoin.Node.Chain.Sync` | State machine that decides which peer to ask for headers, and when the chain is synced |+| `Haskoin.Node.Chain.Database` | Storage of headers in RocksDB |+| `Haskoin.Node.Address` | Parsing peer addresses and opening connections |+| `Haskoin.Node.Logger` | Logging |+ ## Security notes - Every message header is checked before its payload is read. The payload limit@@ -447,11 +513,21 @@ for everything but block messages. - List messages are refused as soon as their count is over the limit, such as 2000 headers, 1000 addresses or 50000 inventory entries.-- A peer that sends anything invalid is disconnected.+- A peer that sends anything invalid is disconnected, with a `PeerException`+ saying why.+- A peer that sends a second version or verack, or does not complete the+ handshake within `timeout` seconds even while sending other messages, is+ disconnected.+- A peer that sends invalid headers, or does not answer a request for headers in+ time, is never asked for headers again before it disconnects. - Addresses relayed by peers are only used if they are public unicast addresses, so a peer cannot make the node connect to services on its own host or network. Static peers are not checked. - At most 4096 relayed addresses are kept.+- An address whose connection fails waits twice as long after each failure+ before it is tried again, up to ten minutes. A relayed address whose peer+ broke the protocol is not tried again for a day. Static peers are never+ banned, and wait at most ten minutes. - Headers are not reported as synced until they have the minimum chain work of the network. Above that work, forks from far below the tip on a chain with less work are refused.@@ -475,9 +551,34 @@ `package.yaml` is the source of the package description. Change it rather than `haskoin-node.cabal`, which [hpack](https://github.com/sol/hpack) generates. -The tests are in `test/` and use [hspec](https://hspec.github.io/). They run-against mock peers on the Bitcoin Cash regression test network, so they need no-Internet access.+The package also builds with [Cabal](https://www.haskell.org/cabal)+(`cabal build`, `cabal test`).++There are two test suites, using [hspec](https://hspec.github.io/) and+[QuickCheck](https://hackage.haskell.org/package/QuickCheck):++- `spec` (`test/unit`) tests the state machines with properties, and runs the+ peer, peer manager, chain and node against in-memory mock peers. It needs no+ Internet access.++ ```sh+ stack test haskoin-node:spec+ ```++- `regtest` (`test/regtest`) runs the node against+ [Bitcoin Core](https://bitcoincore.org) and+ [Bitcoin Cash Node](https://bitcoincashnode.org) in regression test mode. The+ nodes run in a [Podman](https://podman.io) container built from+ `test/regtest/Containerfile`, which installs the official release binaries on+ Ubuntu after checking their SHA-256 hashes. The image is built the first time+ the suite runs. Nodes use random ports, so runs do not collide. The suite is+ skipped if Podman is missing or `HASKOIN_NODE_SKIP_REGTEST` is set.++ ```sh+ stack test haskoin-node:regtest+ ```++Set `HASKOIN_TEST_LOG` to see the node's logs while tests run. ### Contributing
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
haskoin-node.cabal view
@@ -1,105 +1,165 @@-cabal-version: 1.12+cabal-version: 1.18 -- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack name: haskoin-node-version: 2.3.1+version: 2.4.0 synopsis: P2P library for Bitcoin and Bitcoin Cash-description: Please see the README on GitHub at <https://github.com/jprupp/haskoin-node#readme>+description: Connect to Bitcoin (BTC) and Bitcoin Cash (BCH) peers, keep them healthy, and sync block headers into a RocksDB database. Blocks and transactions can be downloaded from connected peers. See the README on GitHub at <https://github.com/jprupp/haskoin-node#readme>. category: Bitcoin, Finance, Network-homepage: http://github.com/jprupp/haskoin-node#readme-bug-reports: http://github.com/jprupp/haskoin-node/issues+homepage: https://github.com/jprupp/haskoin-node#readme+bug-reports: https://github.com/jprupp/haskoin-node/issues author: JP Rupp maintainer: jprupp@protonmail.ch+copyright: 2020 Haskoin Developers license: MIT license-file: LICENSE build-type: Simple+tested-with:+ GHC == 9.12.4 extra-source-files:+ test/regtest/Containerfile+extra-doc-files: README.md CHANGELOG.md source-repository head type: git- location: https://github.com/jprupp/haskoin-node.git+ location: https://github.com/jprupp/haskoin-node library exposed-modules: Haskoin.Node- other-modules:+ Haskoin.Node.Address Haskoin.Node.Chain+ Haskoin.Node.Chain.Database+ Haskoin.Node.Chain.Sync Haskoin.Node.Logger Haskoin.Node.Loop Haskoin.Node.Peer+ Haskoin.Node.Peer.Lock+ Haskoin.Node.Peer.Request Haskoin.Node.PeerMgr- Paths_haskoin_node+ Haskoin.Node.PeerMgr.Addresses+ Haskoin.Node.PeerMgr.Handshake+ Haskoin.Node.PeerMgr.Lifecycle hs-source-dirs: src+ default-extensions:+ DuplicateRecordFields+ MultiWayIf+ NoFieldSelectors+ OverloadedRecordDot+ OverloadedStrings+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -Wunused-packages build-depends:- async- , base >=4.9 && <5+ async ==2.2.*+ , base >=4.20 && <5+ , bytestring >=0.11 && <0.13+ , cereal ==0.5.*+ , conduit ==1.3.*+ , conduit-extra ==1.3.*+ , containers >=0.6 && <0.8+ , fast-logger ==3.2.*+ , haskoin-core >=1.6.1 && <1.7+ , network >=3.1 && <3.3+ , nqe >=0.6.9 && <0.7+ , random >=1.2 && <1.4+ , rocksdb-haskell-jprupp >=2.6.1 && <2.7+ , rocksdb-query >=0.6.1 && <0.7+ , stm ==2.5.*+ , string-conversions ==0.4.*+ , text >=2.0 && <2.2+ , time >=1.12 && <1.15+ , transformers ==0.6.*+ default-language: GHC2024++test-suite regtest+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules:+ Regtest.Podman+ Haskoin.Node.Test+ Paths_haskoin_node+ hs-source-dirs:+ test/regtest+ test/common+ default-extensions:+ DuplicateRecordFields+ MultiWayIf+ NoFieldSelectors+ OverloadedRecordDot+ OverloadedStrings+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -Wunused-packages -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ HUnit+ , base >=4.20 && <5 , bytestring , cereal , conduit- , conduit-extra- , containers , data-default- , fast-logger- , hashable- , haskoin-core >=1.6.0- , mtl+ , directory+ , haskoin-core+ , haskoin-node+ , hspec >=2.10 , network- , nqe >=0.6.3+ , nqe+ , process , random- , rocksdb-haskell-jprupp >=2.3.0- , rocksdb-query >=0.6.0- , stm- , string-conversions- , text+ , rocksdb-haskell-jprupp , time- , transformers , unliftio- , unordered-containers- default-language: Haskell2010+ default-language: GHC2024 test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs other-modules:+ Haskoin.Node.AddressSpec+ Haskoin.Node.Chain.SyncSpec+ Haskoin.Node.ChainSpec+ Haskoin.Node.Peer.LockSpec+ Haskoin.Node.Peer.RequestSpec+ Haskoin.Node.PeerMgr.AddressesSpec+ Haskoin.Node.PeerMgr.HandshakeSpec+ Haskoin.Node.PeerMgr.LifecycleSpec+ Haskoin.Node.PeerMgrSpec+ Haskoin.Node.PeerSpec Haskoin.NodeSpec+ Haskoin.Node.Test Paths_haskoin_node hs-source-dirs:- test+ test/unit+ test/common+ default-extensions:+ DuplicateRecordFields+ MultiWayIf+ NoFieldSelectors+ OverloadedRecordDot+ OverloadedStrings+ ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -Wunused-packages -threaded -rtsopts -with-rtsopts=-N build-depends: HUnit+ , QuickCheck >=2.14 , async- , base >=4.9 && <5+ , base >=4.20 && <5 , base64 , bytestring , cereal , conduit- , conduit-extra- , containers , data-default- , fast-logger- , hashable- , haskoin-core >=1.6.0+ , haskoin-core , haskoin-node- , hspec- , mtl+ , hspec >=2.10 , network- , nqe >=0.6.3+ , nqe , random- , rocksdb-haskell-jprupp >=2.3.0- , rocksdb-query >=0.6.0- , safe- , stm- , string-conversions+ , rocksdb-haskell-jprupp , text , time- , transformers , unliftio- , unordered-containers- default-language: Haskell2010- build-tool-depends: hspec-discover:hspec-discover+ default-language: GHC2024+ build-tool-depends:+ hspec-discover:hspec-discover
src/Haskoin/Node.hs view
@@ -1,13 +1,3 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-}- -- | -- Module : Haskoin.Node -- Copyright : (c) Haskoin Developers@@ -32,7 +22,9 @@ -- too long. -- -- * The chain ("Haskoin.Node.Chain") downloads block headers from peers,--- connects them to the best chain, and stores them in the database.+-- connects them to the best chain, and stores them in the database. It+-- follows the announcements of new blocks, whether they come as headers or+-- as inventory, and polls peers when it has heard nothing for a while. -- -- Everything that happens is published as a 'NodeEvent' to the 'Publisher' -- given in 'NodeConfig'. Subscribe to it to learn about new best blocks, peers@@ -66,6 +58,13 @@ -- by messages, which keeps the concurrency easy to reason about and lets a -- failing peer die without disturbing the rest. --+-- * __Plain 'IO'.__ Every function runs in 'IO', without monad transformers+-- or @MonadIO@ and @MonadUnliftIO@ constraints. The types say exactly what+-- runs, GHC has nothing to specialise, and the long-running loops of the node+-- cannot leak memory through an unspecialised dictionary (see+-- "Haskoin.Node.Loop"). Programs that work in their own monad call the node+-- with @liftIO@.+-- -- * __Trust nothing from the network.__ Peers are strangers. Every message is -- checked against size limits before it is read, invalid input disconnects -- the peer, and a peer cannot make the node allocate memory faster than it@@ -114,33 +113,33 @@ Node (..), withNode, withConnection,+ -- * Peers module Haskoin.Node.Peer,+ -- * Peer manager module Haskoin.Node.PeerMgr,+ -- * Chain module Haskoin.Node.Chain,+ -- * Logging module Haskoin.Node.Logger, ) where -import Control.Monad.Cont (ContT (..), MonadCont (callCC), cont, runCont, runContT)-import Control.Monad.Trans (lift)-import Data.Conduit.Network-import Data.String.Conversions (cs)+import Control.Concurrent.Async (link, withAsync) import Data.Time.Clock (NominalDiffTime) import Database.RocksDB (ColumnFamily, DB) import Haskoin+import Haskoin.Node.Address (withConnection) import Haskoin.Node.Chain import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever) import Haskoin.Node.Peer import Haskoin.Node.PeerMgr import NQE-import Network.Socket-import Text.Read (readMaybe)-import UnliftIO+import Network.Socket (SockAddr) -- | General node configuration. Passed to 'withNode'. --@@ -192,39 +191,13 @@ ChainEvent !ChainEvent | -- | a peer connected, disconnected or sent a message PeerEvent !PeerEvent- deriving (Eq)---- | Open a TCP connection to a peer address and pass its conduits to a--- function. The connection is closed when the function returns. Calls 'error'--- if the address cannot be converted into a host and port.-withConnection :: SockAddr -> WithConnection-withConnection na f =- fromSockAddr na >>= \case- Nothing -> error $ "Invalid address " ++ show na- Just cset ->- runTCPClient cset $ \ad ->- f (Conduits (appSource ad) (appSink ad))---- | Turn a socket address into client settings for a TCP connection, or--- 'Nothing' if it cannot be converted.-fromSockAddr :: (MonadUnliftIO m) => SockAddr -> m (Maybe ClientSettings)-fromSockAddr sa = go `catch` e- where- go = do- (maybe_host, maybe_port) <- liftIO $ getNameInfo flags True True sa- return $- clientSettings- <$> (readMaybe =<< maybe_port)- <*> (cs <$> maybe_host)- flags = [NI_NUMERICHOST, NI_NUMERICSERV]- e :: (Monad m) => SomeException -> m (Maybe a)- e _ = return Nothing+ deriving stock (Eq) -- | Forward events from the chain to the node publisher, telling the peer -- manager about every new best block height so that it can tell peers apart -- that are behind. chainEvents ::- (MonadIO m) => PeerMgr -> Inbox ChainEvent -> Publisher NodeEvent -> m ()+ PeerMgr -> Inbox ChainEvent -> Publisher NodeEvent -> IO () chainEvents mgr input output = loopForever $ do event <- receive input case event of@@ -235,11 +208,13 @@ -- | 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.+-- node's queues cannot grow faster than it can work. Address, header and+-- inventory messages are only taken from peers that are online: the handshake+-- is complete and the peer is not being disconnected.+-- Inventory with blocks goes to the chain, which asks for headers if it does+-- not know them. peerEvents ::- (MonadIO m) =>- Chain -> PeerMgr -> Inbox PeerEvent -> Publisher NodeEvent -> m ()+ Chain -> PeerMgr -> Inbox PeerEvent -> Publisher NodeEvent -> IO () peerEvents ch mgr input output = loopForever $ do event <- receive input case event of@@ -247,8 +222,8 @@ chainPeerConnected ch p PeerDisconnected p -> chainPeerDisconnected ch p- PeerMessage p msg -> do- case msg of+ PeerMessage p m -> do+ case m of MVersion v -> peerMgrVersion mgr p v >> peerMgrSync mgr MVerAck ->@@ -261,6 +236,9 @@ whenOnline p $ peerMgrAddrs mgr p (map snd ns) >> peerMgrSync mgr MHeaders (Headers hs) -> whenOnline p $ chainHeaders ch p (map fst hs) >> chainSync ch+ MInv (Inv ivs) -> case [BlockHash iv.invHash | iv <- ivs, isBlock iv] of+ [] -> return ()+ bhs -> whenOnline p $ chainBlocksAnnounced ch p bhs >> chainSync ch _ -> return () ticklePeer mgr p peerMessageDone p@@ -268,8 +246,9 @@ where whenOnline p act = getOnlinePeer mgr p >>= \case- Just OnlinePeer {online = True} -> act+ Just o | o.online -> act _ -> return ()+ isBlock iv = iv.invType `elem` [InvBlock, InvWitnessBlock] -- | Launch node process in the foreground. --@@ -277,18 +256,39 @@ -- handle to the given action, and stop everything when the action returns. If -- any component fails, the exception is re-thrown in the calling thread. withNode ::- (MonadUnliftIO m) => NodeConfig -> (Node -> m a) -> m a-withNode NodeConfig {..} action = flip runContT return $ do- peerPub <- ContT withPublisher- peerSub <- ContT (withSubscription peerPub)- chainPub <- ContT withPublisher- chainSub <- ContT (withSubscription chainPub)- let peerMgrCfg =- PeerMgrConfig {pub = peerPub, logger = withSource "PeerMgr" logger, ..}- let chainCfg =- ChainConfig {pub = chainPub, logger = withSource "Chain" logger, ..}- chain <- ContT (withChain chainCfg)- peerMgr <- ContT $ withPeerMgr peerMgrCfg- link =<< ContT (withAsync $ chainEvents peerMgr chainSub pub)- link =<< ContT (withAsync $ peerEvents chain peerMgr peerSub pub)- lift $ action Node {..}+ NodeConfig -> (Node -> IO a) -> IO a+withNode cfg action =+ withPublisher $ \peerPub ->+ withSubscription peerPub $ \peerSub ->+ withPublisher $ \chainPub ->+ withSubscription chainPub $ \chainSub ->+ withChain (chainConfig chainPub) $ \chain ->+ withPeerMgr (peerMgrConfig peerPub) $ \peerMgr ->+ withAsync (chainEvents peerMgr chainSub cfg.pub) $ \a1 ->+ withAsync (peerEvents chain peerMgr peerSub cfg.pub) $ \a2 -> do+ link a1+ link a2+ action Node {peerMgr = peerMgr, chain = chain}+ where+ chainConfig pub =+ ChainConfig+ { db = cfg.db,+ cf = cfg.cf,+ net = cfg.net,+ pub = pub,+ timeout = cfg.timeout,+ logger = withSource "Chain" cfg.logger+ }+ peerMgrConfig pub =+ PeerMgrConfig+ { maxPeers = cfg.maxPeers,+ peers = cfg.peers,+ discover = cfg.discover,+ address = cfg.address,+ net = cfg.net,+ pub = pub,+ timeout = cfg.timeout,+ maxPeerLife = cfg.maxPeerLife,+ connect = cfg.connect,+ logger = withSource "PeerMgr" cfg.logger+ }
+ src/Haskoin/Node/Address.hs view
@@ -0,0 +1,157 @@+-- |+-- Module : Haskoin.Node.Address+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Peer addresses: parsing them, deciding which relayed addresses are worth+-- connecting to, and opening TCP connections.+--+-- "Haskoin.Node.PeerMgr" re-exports 'toHostService', 'toSockAddr' and+-- 'isRoutable', and "Haskoin.Node" re-exports 'withConnection', where they+-- used to be defined. They live here because none of them depends on the+-- peer manager or the node: they are about addresses and sockets only.+module Haskoin.Node.Address+ ( toHostService,+ toSockAddr,+ isRoutable,+ withConnection,+ )+where++import Control.Applicative ((<|>))+import Control.Arrow (second)+import Control.Exception (IOException, catch)+import Data.Bits ((.&.))+import Data.Conduit.Network+ ( ClientSettings,+ appSink,+ appSource,+ clientSettings,+ runTCPClient,+ )+import Data.List (elemIndex)+import Data.String.Conversions (cs)+import Data.Word (Word8)+import Haskoin (Network (..))+import Haskoin.Node.Peer (Conduits (..), WithConnection)+import Network.Socket+ ( AddrInfo (..),+ NameInfoFlag (..),+ SockAddr (..),+ getAddrInfo,+ getNameInfo,+ hostAddress6ToTuple,+ hostAddressToTuple,+ )+import Text.Read (readMaybe)++-- | Split a peer address into host and port. Accepts @host@, @host:port@,+-- @[ipv6]@ and @[ipv6]:port@. Either part is 'Nothing' if missing.+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 (drop 1) $ splitAt i xs+ | x == ':' -> do+ return (str, "")+ _ -> Nothing+ in (host, srv)++-- | Resolve a peer address given as in 'toHostService' into socket addresses,+-- using the default port of the network if none is given. Returns an empty list+-- if it cannot be resolved.+--+-- Only resolver failures, which are 'IOException's, are caught. Catching+-- every exception would also swallow the asynchronous exception that cancels+-- the thread doing the lookup, and the node could then never shut down.+toSockAddr :: Network -> String -> IO [SockAddr]+toSockAddr net str =+ resolve `catch` \(_ :: IOException) -> return []+ where+ resolve = map (.addrAddress) <$> getAddrInfo Nothing host srv+ (host, srv) =+ second (<|> Just (show net.defaultPort)) (toHostService str)++-- | 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++-- | Whether an IPv4 address is public unicast.+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++-- | Open a TCP connection to a peer address and pass its conduits to a+-- function. The connection is closed when the function returns. Calls 'error'+-- if the address cannot be converted into a host and port.+withConnection :: SockAddr -> WithConnection+withConnection sa f =+ fromSockAddr sa >>= \case+ Nothing -> error $ "Invalid address " <> show sa+ Just settings ->+ runTCPClient settings $ \ad ->+ f Conduits {inboundConduit = appSource ad, outboundConduit = appSink ad}++-- | Turn a socket address into client settings for a TCP connection, or+-- 'Nothing' if it cannot be converted.+fromSockAddr :: SockAddr -> IO (Maybe ClientSettings)+fromSockAddr sa =+ resolve `catch` \(_ :: IOException) -> return Nothing+ where+ flags = [NI_NUMERICHOST, NI_NUMERICSERV]+ resolve = do+ (host, port) <- (getNameInfo flags True True sa)+ return $ clientSettings <$> (readMaybe =<< port) <*> (cs <$> host)
src/Haskoin/Node/Chain.hs view
@@ -1,20 +1,3 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE NoFieldSelectors #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}- -- | -- Module : Haskoin.Node.Chain -- Copyright : (c) Haskoin Developers@@ -34,7 +17,9 @@ -- -- After a peer has delivered all its headers, it is asked to announce new -- blocks with headers messages, which are fed back to the chain process by--- "Haskoin.Node".+-- "Haskoin.Node". Peers fall back to inventory messages when they cannot+-- announce with headers, and "Haskoin.Node" feeds those to+-- 'chainBlocksAnnounced'. -- -- = Why it works this way --@@ -42,8 +27,21 @@ -- peers for the same range at once would only download it several times. -- The chain syncs from one peer and moves to the next when it is done or -- times out, which also means every connected peer eventually gets a chance--- to show a better chain.+-- to show a better chain. "Haskoin.Node.Chain.Sync" decides whose turn it+-- is. --+-- * __Every hint of unknown blocks is followed.__ A peer announces a new block+-- with a headers message only while it believes the node has the parent.+-- When many blocks are found at once, or the node missed an announcement,+-- the peer sends an inventory message instead, or headers that do not+-- connect. Either way the node asks that peer for headers. Ignoring these+-- left the chain stuck at an old height until the peer reconnected.+--+-- * __Quiet means poll.__ If no headers are requested or received for+-- 'timeout' seconds, every connected peer is asked for headers again. This+-- costs one small request per peer per quiet period, and puts a bound on how+-- long any unforeseen stall can last.+-- -- * __Most work wins.__ The best chain is the one with the most accumulated -- proof of work, not the longest or the first seen, as in every Bitcoin -- implementation.@@ -55,24 +53,14 @@ -- use 'ChainSynced' to start work that only makes sense at the tip, so a false -- signal would be costly. ----- * __Headers are stored, not kept in memory.__ Storing them in RocksDB lets a--- restart resume where it stopped instead of downloading the whole chain--- again.------ = Database------ Headers are stored as 'BlockNode' values in the configured column family, or--- the default one, under keys starting with byte @0x90@. The best block is--- stored under key @0x91@ and a data version under key @0x92@. If the data--- version does not match, keys @0x90@ and @0x91@ are deleted when the chain--- starts. Applications that share the database must not use these prefixes.+-- * __Headers are stored, not kept in memory.__ "Haskoin.Node.Chain.Database"+-- describes how, and which key prefixes it uses in the database. module Haskoin.Node.Chain ( ChainConfig (..), ChainEvent (..), Chain,- ChainT,- runChainT, withChain,+ chainRunHeaders, chainGetBlock, chainGetBest, chainGetAncestor,@@ -83,33 +71,32 @@ chainIsSynced, chainBlockMain, chainHeaders,+ chainBlocksAnnounced, chainSync, ) where -import Control.Monad (forM_, guard, when)-import Control.Monad.Trans.Reader-import Data.ByteString qualified as B+import Control.Concurrent (threadDelay)+import Control.Concurrent.Async (link, withAsync)+import Control.Concurrent.STM+import Control.Monad (filterM, unless, void, when) import Data.Function (on)-import Data.List (delete, nub)-import Data.Maybe (isJust, isNothing)-import Data.Serialize+import Data.Functor ((<&>))+import Data.Maybe (isNothing) import Data.String.Conversions (cs)+import Data.Text (Text) import Data.Time.Clock-import Data.Time.Clock.POSIX-import Data.Word (Word32)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Database.RocksDB (ColumnFamily, DB)-import Database.RocksDB qualified as R-import Database.RocksDB.Query-import Haskoin hiding (Key)+import Haskoin+import Haskoin.Node.Chain.Database (ChainDB (..), initChainDB, runHeaders)+import Haskoin.Node.Chain.Sync import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever) import Haskoin.Node.Peer import Haskoin.Node.PeerMgr (myVersion) import NQE import System.Random (randomRIO)-import UnliftIO-import UnliftIO.Concurrent (threadDelay) -- | Configuration for chain syncing process. data ChainConfig = ChainConfig@@ -122,7 +109,8 @@ -- | send header chain events here pub :: !(Publisher ChainEvent), -- | seconds to wait for headers from the syncing peer before it is- -- disconnected+ -- disconnected; also the quiet period after which all peers are asked for+ -- headers again timeout :: !NominalDiffTime, -- | log messages here logger :: !Logger@@ -132,11 +120,13 @@ data ChainMessage = -- | a peer sent headers ChainHeaders !Peer ![BlockHeader]+ | -- | a peer announced blocks in an inventory message+ ChainBlocksAnnounced !Peer ![BlockHash] | -- | a peer completed the handshake ChainPeerConnected !Peer | -- | a peer disconnected ChainPeerDisconnected !Peer- | -- | periodic tick to check the syncing peer for timeouts+ | -- | periodic tick to check for timeouts and quiet periods ChainPing | -- | reply once every previous message has been handled ChainSyncMsg !(Listen ())@@ -148,7 +138,7 @@ | -- | chain is in sync with the network; published only once, with the best -- block at that time ChainSynced !BlockNode- deriving (Eq, Show)+ deriving stock (Eq, Show) -- | Handle to a running chain process. Obtain one with 'withChain', or from -- the 'Haskoin.Node.Node' returned by 'Haskoin.Node.withNode'.@@ -158,113 +148,36 @@ -- | mailbox of the chain process mailbox :: !(Mailbox ChainMessage), -- | syncing state- state :: !(TVar ChainState)+ state :: !(TVar ChainState),+ -- | reserves the peer that headers are requested from+ owner :: !Owner } instance Eq Chain where (==) = (==) `on` (.mailbox) --- | Database key for version.-data ChainDataVersionKey = ChainDataVersionKey- deriving (Eq, Ord, Show)--instance Key ChainDataVersionKey--instance KeyValue ChainDataVersionKey Word32--instance Serialize ChainDataVersionKey where- get = do- guard . (== 0x92) =<< getWord8- return ChainDataVersionKey- put ChainDataVersionKey = putWord8 0x92---- | State of the sync against one peer.-data ChainSync = ChainSync- { -- | peer being synced against- peer :: !Peer,- -- | when it last sent headers, or when syncing started- timestamp :: !UTCTime,- -- | last header it sent, from which to continue syncing- best :: !(Maybe BlockNode)- }- -- | Mutable state for the header chain process. data ChainState = ChainState- { -- | peer to sync against and time of last received message- syncing :: !(Maybe ChainSync),- -- | queue of peers to sync against- peers :: ![Peer],+ { -- | which peers to ask for headers+ sync :: !(SyncState Peer), -- | has the header chain ever been considered synced? beenInSync :: !Bool } --- | Key for block header in database.-newtype BlockHeaderKey = BlockHeaderKey BlockHash deriving (Eq, Show)--instance Serialize BlockHeaderKey where- get = do- guard . (== 0x90) =<< getWord8- BlockHeaderKey <$> get- put (BlockHeaderKey bh) = do- putWord8 0x90- put bh---- | Key for best block in database.-data BestBlockKey = BestBlockKey deriving (Eq, Show)--instance KeyValue BlockHeaderKey BlockNode--instance KeyValue BestBlockKey BlockNode--instance Serialize BestBlockKey where- get = do- guard . (== 0x91) =<< getWord8- return BestBlockKey- put BestBlockKey = putWord8 0x91---- | Monad transformer with access to a 'Chain'. It has a 'BlockHeaders'--- instance, so the header functions of haskoin-core, such as 'getAncestor' and--- 'splitPoint', can run in it against the database of the chain.-type ChainT m = ReaderT Chain m---- | Run a 'ChainT' action against a chain.-runChainT :: ChainT m a -> Chain -> m a-runChainT = runReaderT+-- | Where the headers of a configuration are stored.+configDB :: ChainConfig -> ChainDB+configDB cfg = ChainDB {db = cfg.db, cf = cfg.cf} --- | Read and write headers in the database of a chain configuration. Orphan--- instance.-instance (MonadIO m) => BlockHeaders (ReaderT ChainConfig m) where- addBlockHeader bn = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do- case cf of- Nothing -> insert db (BlockHeaderKey h) bn- Just cf' -> insertCF db cf' (BlockHeaderKey h) bn- where- h = headerHash bn.header- getBlockHeader bh = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do- retrieveCommon db cf (BlockHeaderKey bh)- getBestBlockHeader = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do- retrieveCommon db cf BestBlockKey >>= \case- Nothing -> error "Could not get best block from database"- Just b -> return b- setBestBlockHeader bn = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do- case cf of- Nothing -> insert db BestBlockKey bn- Just cf' -> insertCF db cf' BestBlockKey bn- addBlockHeaders bns = ReaderT $ \ChainConfig {db, cf} -> liftIO $ do- writeBatch db (map (f cf) bns)- where- h bn = headerHash bn.header- f cf bn = case cf of- Nothing -> insertOp (BlockHeaderKey (h bn)) bn- Just cf' -> insertOpCF cf' (BlockHeaderKey (h bn)) bn+-- | Run a header function of haskoin-core against the database of a chain.+withDB :: Chain -> (forall n. (BlockHeaders n) => n a) -> IO a+withDB ch = runHeaders (configDB ch.config) --- | Read and write headers in the database of a chain. Orphan instance.-instance (MonadIO m) => BlockHeaders (ChainT m) where- addBlockHeader bn = withReaderT (.config) (addBlockHeader bn)- getBlockHeader bh = withReaderT (.config) (getBlockHeader bh)- getBestBlockHeader = withReaderT (.config) getBestBlockHeader- setBestBlockHeader bn = withReaderT (.config) (setBestBlockHeader bn)- addBlockHeaders bns = withReaderT (.config) (addBlockHeaders bns)+-- | Run header functions of haskoin-core, such as 'getAncestor' or+-- 'blockLocator', against the headers of a chain. The functions that start+-- with @chain@ cover the common queries; this is for the rest.+chainRunHeaders ::+ Chain -> (forall n. (BlockHeaders n) => n a) -> IO a+chainRunHeaders = withDB -- | Start the chain process and pass its handle to the given action. The process -- is stopped when the action returns, and its exceptions are re-thrown in the@@ -274,462 +187,333 @@ -- seeded with the genesis block of the network if it has no best block. The -- current best block is published as a 'ChainBestBlock' event on start. withChain ::- (MonadUnliftIO m) => ChainConfig -> (Chain -> m a) -> m a+ ChainConfig -> (Chain -> IO a) -> IO a withChain cfg action = do (inbox, mailbox) <- newMailbox- st <-- newTVarIO- ChainState- { syncing = Nothing,- beenInSync = False,- peers = []- }- let ch = Chain {config = cfg, mailbox = mailbox, state = st}- initChainDB cfg- withAsync (main_loop ch inbox) $ \a ->+ now <- getCurrentTime+ st <- newTVarIO ChainState {sync = emptySync now, beenInSync = False}+ owner <- newOwner "Chain"+ let ch = Chain {config = cfg, mailbox = mailbox, state = st, owner = owner}+ initChainDB cfg.net (configDB cfg)+ withAsync (withClock cfg.timeout mailbox (run ch inbox)) $ \a -> link a >> action ch where- main_loop ch inbox =- withSyncLoop ch.mailbox (run ch inbox) run ch inbox = do- runReaderT getBestBlockHeader cfg- >>= chainEvent cfg . ChainBestBlock+ withDB ch getBestBlockHeader >>= chainEvent ch . ChainBestBlock loopForever $ do- logDebug ch.config.logger "Awaiting event..."- msg <- receive inbox- chainConfigMessage ch msg+ logDebug cfg.logger "Awaiting event..."+ receive inbox >>= chainMessage ch -- | Log and publish a chain event.-chainEvent :: (MonadIO m) => ChainConfig -> ChainEvent -> m ()-chainEvent cfg e = do+chainEvent :: Chain -> ChainEvent -> IO ()+chainEvent ch e = do case e of ChainBestBlock b -> logInfo- cfg.logger+ ch.config.logger ("Best block header at height " <> cs (show b.height)) ChainSynced b -> logInfo- cfg.logger+ ch.config.logger ("Headers in sync at height " <> cs (show b.height))- publish e cfg.pub+ publish e ch.config.pub --- | Import headers sent by a peer. Disconnect the peer if they do not connect.--- Publish a new best block if it changed. If the peer has no more headers, move--- on to the next peer and check whether the chain is now in sync; otherwise ask--- it for more.-processHeaders :: (MonadIO m) => Chain -> Peer -> [BlockHeader] -> m ()-processHeaders ch p hs = do- let len = length hs+-- | Handle a message sent to the chain process.+chainMessage :: Chain -> ChainMessage -> IO ()+chainMessage ch = \case+ ChainPeerConnected p -> do+ logDebug ch.config.logger ("Connected peer " <> p.label)+ update ch (peerConnected p)+ syncNext ch+ ChainPeerDisconnected p -> do+ logDebug ch.config.logger ("Disconnected peer " <> p.label)+ releaseIfSyncing ch p+ update ch (peerDisconnected p)+ syncNext ch+ ChainHeaders p hs -> whenConnected ch p (processHeaders ch p hs)+ ChainBlocksAnnounced p bhs -> whenConnected ch p (processAnnouncement ch p bhs)+ ChainPing -> checkSync ch+ ChainSyncMsg r -> atomically (r ())++-- | Ignore messages from peers that are not connected, or that are being+-- disconnected for misbehaving.+whenConnected :: Chain -> Peer -> IO () -> IO ()+whenConnected ch p go =+ getStatus ch p >>= \case+ Just Dropped -> ignore "being dropped"+ Just _ -> go+ Nothing -> ignore "not connected"+ where+ ignore why =+ logDebug ch.config.logger ("Ignoring peer " <> why <> ": " <> p.label)++-- | Import headers sent by a peer.+--+-- An empty message from the syncing peer means it has nothing more. Headers+-- that do not connect mean the node missed an announcement, so the peer is+-- asked for headers. Invalid headers disconnect the peer. After a full+-- message of 'maxHeadersSize' headers the peer is asked for more, and after a+-- shorter one the syncing peer is done.+processHeaders :: Chain -> Peer -> [BlockHeader] -> IO ()+processHeaders ch p [] = do+ logDebug ch.config.logger ("No headers from peer " <> p.label)+ syncing <- isSyncing ch p+ when syncing $ finish ch p+processHeaders ch p hs@(h : _) = do logDebug ch.config.logger- ("Processing " <> cs (show len) <> " headers from peer " <> p.label)- let net = ch.config.net- now <- liftIO getCurrentTime- pbest <- runReaderT getBestBlockHeader ch.config- importHeaders ch now hs >>= \case+ ("Processing " <> cs (show (length hs)) <> " headers from peer " <> p.label)+ withDB ch (getBlockHeader h.prev) >>= \case Nothing -> do- logWarn+ logDebug ch.config.logger- ("Could not connect headers from peer " <> p.label)- killPeer p- Just done -> do- setLastReceived ch.state p- best <- runReaderT getBestBlockHeader ch.config- when (pbest.header /= best.header) $- chainEvent ch.config (ChainBestBlock best)- if done- then do- MSendHeaders `sendMessage` p- finishPeer ch p- syncNewPeer ch- syncNotif ch- else syncPeer ch p---- | Start syncing against the next peer in the queue, unless a peer is already being synced.-syncNewPeer :: (MonadIO m) => Chain -> m ()-syncNewPeer ch =- getSyncingPeer ch.state >>= \case- Just _ -> return ()- Nothing ->- nextPeer ch >>= \case- Nothing -> return ()- Just p -> do- logDebug ch.config.logger ("Syncing against peer " <> p.label)- syncPeer ch p---- | Publish a 'ChainSynced' event if the chain just became synced.-syncNotif :: (MonadIO m) => Chain -> m ()-syncNotif ch =- notifySynced ch >>= \case- False -> return ()- True ->- runReaderT getBestBlockHeader ch.config- >>= chainEvent ch.config . ChainSynced+ ("Headers from peer " <> p.label <> " do not connect")+ update ch (requestSync p)+ syncNext ch+ Just _ ->+ importHeaders ch hs >>= \case+ Left e -> misbehaving ch p ("could not connect headers: " <> cs e)+ Right lst -> do+ syncing <- isSyncing ch p+ let full = length hs >= fromIntegral maxHeadersSize+ if+ | full && syncing -> requestHeaders ch p (Just lst)+ | full -> update ch (requestSync p) >> syncNext ch+ | syncing -> finish ch p+ | otherwise -> syncNotif ch --- | Ask a peer for headers, continuing from the last header it sent, or from the--- best block. Does nothing if another peer is being synced.-syncPeer :: (MonadIO m) => Chain -> Peer -> m ()-syncPeer ch p = do- t <- liftIO getCurrentTime- m <-- chainSyncingPeer ch.state >>= \case- Just ChainSync {peer = s, best = m}- | p == s -> syncing_me t m- | otherwise -> return Nothing- Nothing -> syncing_new t- forM_ m $ \g -> do+-- | A peer announced blocks in an inventory message. If any is unknown, ask+-- the peer for headers.+processAnnouncement :: Chain -> Peer -> [BlockHash] -> IO ()+processAnnouncement ch p bhs = do+ unknown <- filterM (fmap isNothing . chainGetBlock ch) bhs+ unless (null unknown) $ do logDebug ch.config.logger- ("Requesting headers from peer " <> p.label)- MGetHeaders g `sendMessage` p- where- syncing_new t =- setSyncingPeer ch p >>= \case- False -> return Nothing- True -> do- logDebug ch.config.logger ("Locked peer " <> p.label)- h <- runReaderT getBestBlockHeader ch.config- Just <$> syncHeaders ch t h p- syncing_me t m = do- h <- case m of- Nothing -> runReaderT getBestBlockHeader ch.config- Just h -> return h- Just <$> syncHeaders ch t h p+ ( "Peer "+ <> p.label+ <> " announced "+ <> cs (show (length unknown))+ <> " unknown blocks"+ )+ update ch (requestSync p)+ syncNext ch --- | Handle a message sent to the chain process.-chainConfigMessage :: (MonadIO m) => Chain -> ChainMessage -> m ()-chainConfigMessage ch (ChainHeaders p hs) =- processHeaders ch p hs-chainConfigMessage ch (ChainPeerConnected p) = do- logDebug ch.config.logger ("Connected peer " <> p.label)- addPeer ch.state p- syncNewPeer ch-chainConfigMessage ch (ChainPeerDisconnected p) = do- 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- now <- liftIO getCurrentTime- chainSyncingPeer ch.state >>= \case- Just ChainSync {peer = p, timestamp = t}- | now `diffUTCTime` t > to -> do- logWarn- ch.config.logger- ("Syncing peer " <> p.label <> " timed out")- killPeer p+-- | Disconnect the syncing peer if it has not answered in time. If no peer is+-- syncing, start with a queued one, or queue every peer if nothing has+-- happened for a while.+checkSync :: Chain -> IO ()+checkSync ch = do+ now <- getCurrentTime+ s <- (.sync) <$> readTVarIO ch.state+ let quiet t = now `diffUTCTime` t > ch.config.timeout+ case syncingPeer s of+ Just (p, t)+ | quiet t -> misbehaving ch p "timed out" | otherwise -> return ()- Nothing -> syncNewPeer ch+ Nothing+ | not (null (queuedPeers s)) -> syncNext ch+ | quiet (lastActivity s) && not (null (connectedPeers s)) -> do+ logDebug ch.config.logger "No recent header activity, polling peers"+ update ch (pollPeers now)+ syncNext ch+ | otherwise -> return () --- | Run a loop in the background that sends a 'ChainPing' to the chain process--- every 2 to 20 seconds.-withSyncLoop :: (MonadUnliftIO m) => Mailbox ChainMessage -> m a -> m a-withSyncLoop mbox mf =- withAsync go $ \a ->- link a >> mf+-- | Unless a peer is being synced against already, start with the first queued+-- peer that 'reservePeer' can reserve. Check whether the chain is synced if there+-- is nobody to sync against.+syncNext :: Chain -> IO ()+syncNext ch = do+ s <- (.sync) <$> readTVarIO ch.state+ when (isNothing (syncingPeer s)) $ go (queuedPeers s) where- go = loopForever $ do- delay <- randomRIO (2 * 10 ^ 6, 2 * 10 ^ 7)- threadDelay delay- ChainPing `send` mbox---- | Version of the database.-dataVersion :: Word32-dataVersion = 1---- | Initialize header database. If version is different from current, the--- database is purged of conflicting elements first.-initChainDB :: (MonadIO m) => ChainConfig -> m ()-initChainDB cfg@ChainConfig {db, cf, net} = liftIO $ do- ver <- retrieveCommon db cf ChainDataVersionKey- when (ver /= Just dataVersion) $ purgeChainDB cfg- case cf of- Nothing -> insert db ChainDataVersionKey dataVersion- Just cf' -> insertCF db cf' ChainDataVersionKey dataVersion- retrieveCommon db cf BestBlockKey >>= \b ->- when (isNothing (b :: Maybe BlockNode)) . flip runReaderT cfg $ do- addBlockHeader (genesisNode net)- setBestBlockHeader (genesisNode net)+ go [] = syncNotif ch+ go (p : ps) =+ reservePeer ch.owner p >>= \case+ True -> do+ logDebug ch.config.logger ("Syncing against peer " <> p.label)+ now <- getCurrentTime+ update ch (startSync now p)+ requestHeaders ch p Nothing+ False -> do+ logDebug ch.config.logger ("Could not lock peer: " <> p.label)+ go ps --- | Purge database of elements having keys that may conflict with those used in--- 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)- go it+-- | Ask the syncing peer for the headers after the given block, or after the+-- best block.+requestHeaders :: Chain -> Peer -> Maybe BlockNode -> IO ()+requestHeaders ch p after = do+ now <- getCurrentTime+ update ch (refreshSync now p)+ bn <- maybe (withDB ch getBestBlockHeader) return after+ loc <- withDB ch (blockLocator bn)+ logDebug+ ch.config.logger+ ( "Requesting headers after height "+ <> cs (show bn.height)+ <> " from peer "+ <> p.label+ )+ MGetHeaders GetHeaders {version = myVersion, locator = loc, stop = zero}+ `sendMessage` p where- with_iter = case cf of- Nothing -> R.withIter db- Just cf' -> R.withIterCF db cf'- 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.take 1 k `elem` [B.singleton 0x90, B.singleton 0x91] -> do- del k- R.iterNext it- go it- _ -> return ()+ zero = "0000000000000000000000000000000000000000000000000000000000000000" --- | Import a bunch of continuous headers. Returns 'True' when the peer that--- delivered them has no more headers to sync. That is the case when it sent--- fewer than 'maxHeadersSize' (2000) headers, the most that one message may--- hold. An empty headers message, which a peer sends when it has nothing--- after the locator, counts as done too.-importHeaders ::- (MonadIO m) => Chain -> UTCTime -> [BlockHeader] -> m (Maybe Bool)-importHeaders ch now hs =- connect >>= \case- Left e -> do- logError ch.config.logger ("connectBlocks: " <> cs e)- return Nothing- Right bns- | null bns -> return (Just True)- | otherwise -> do- bb <- get_last- atomically . modifyTVar ch.state $ \s ->- s {syncing = set_best bb <$> s.syncing}- return (Just (length bns < fromIntegral maxHeadersSize))- where- set_best bb ChainSync {..} = ChainSync {best = bb, ..}- timestamp = floor (utcTimeToPOSIXSeconds now)- connect = runReaderT (connectBlocks ch.config.net timestamp hs) ch.config- get_last = runReaderT (getBlockHeader (headerHash (last hs))) ch.config+-- | The syncing peer has no more headers. Ask it to announce new blocks with+-- headers, release it, and move on.+finish :: Chain -> Peer -> IO ()+finish ch p = do+ logDebug ch.config.logger ("Releasing syncing peer " <> p.label)+ MSendHeaders `sendMessage` p+ update ch (finishSync p)+ void (releasePeer ch.owner p)+ syncNext ch --- | Check if best block header is in sync with the rest of the block chain by--- comparing the best block with the current time, verifying that there are no--- peers in the queue to be synced, and no peer is being synced at the moment.--- This function will only return 'True' once. It should be used to decide--- whether to notify other processes that the header chain has been synced. The--- state of the chain will be flipped to synced when this function returns--- 'True'.------ The best block must also have the 'minimumChainWork' of the network.--- Bitcoin Core requires the same before it leaves initial block download. A--- chain with less work is not the real one, however recent its last block--- is. Below that work 'connectBlocks' does not refuse cheap forks either.-notifySynced :: (MonadIO m) => Chain -> m Bool-notifySynced ch = do- bb <- runReaderT getBestBlockHeader ch.config- df <- (`diffUTCTime` block_time bb) <$> liftIO getCurrentTime- atomically $ do- s <- readTVar ch.state- if- | df > 7200 -> return False- | bb.work < ch.config.net.minimumChainWork -> return False- | isJust s.syncing -> return False- | not (null s.peers) -> return False- | s.beenInSync -> return False- | otherwise -> do- writeTVar ch.state s {beenInSync = True}- return True- where- block_time =- posixSecondsToUTCTime . fromIntegral . (.header.timestamp)+-- | Disconnect a peer that sent invalid headers or timed out, and move on+-- without waiting for its disconnection.+misbehaving :: Chain -> Peer -> Text -> IO ()+misbehaving ch p why = do+ logWarn ch.config.logger ("Disconnecting peer " <> p.label <> ": " <> why)+ releaseIfSyncing ch p+ update ch (dropPeer p)+ killPeer p+ syncNext ch --- | Get next peer to sync against from the queue.-nextPeer :: (MonadIO m) => Chain -> m (Maybe Peer)-nextPeer ch = fmap (.peers) (readTVarIO ch.state) >>= go- where- go [] = return Nothing- go (p : ps) =- setSyncingPeer ch p >>= \case- True -> return (Just p)- False -> go ps+-- | Release a peer reserved for syncing.+releaseIfSyncing :: Chain -> Peer -> IO ()+releaseIfSyncing ch p = do+ syncing <- isSyncing ch p+ when syncing . void $ releasePeer ch.owner p --- | Set a syncing peer and generate a 'GetHeaders' data structure with a block--- locator to send to that peer for syncing.-syncHeaders ::- (MonadIO m) => Chain -> UTCTime -> BlockNode -> Peer -> m GetHeaders-syncHeaders ch now bb p = do- atomically $- modifyTVar ch.state $ \s ->- s- { syncing =- Just- ChainSync- { peer = p,- timestamp = now,- best = Nothing- },- peers = delete p s.peers- }- loc <- runReaderT (blockLocator bb) ch.config- return- GetHeaders- { version = myVersion,- locator = loc,- stop = z- }- where- z = "0000000000000000000000000000000000000000000000000000000000000000"+-- | Apply a transition to the sync state.+update :: Chain -> (SyncState Peer -> SyncState Peer) -> IO ()+update ch f = atomically . modifyTVar ch.state $ \s -> s {sync = f s.sync} --- | 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 c@ChainSync {..}- | peer == p = ChainSync {timestamp = now, ..}- | otherwise = c- atomically (modifyTVar st (\s -> s {syncing = f <$> s.syncing}))+getStatus :: Chain -> Peer -> IO (Maybe PeerStatus)+getStatus ch p = peerStatus p . (.sync) <$> readTVarIO ch.state --- | Add a new peer to the queue of peers to sync against.-addPeer :: (MonadIO m) => TVar ChainState -> Peer -> m ()-addPeer st p = do- atomically (modifyTVar st (\s -> s {peers = nub (p : s.peers)}))+isSyncing :: Chain -> Peer -> IO Bool+isSyncing ch p =+ getStatus ch p <&> \case+ Just (Syncing _ _) -> True+ _ -> False --- | Get syncing peer if there is one.-getSyncingPeer :: (MonadIO m) => TVar ChainState -> m (Maybe Peer)-getSyncingPeer st =- readTVarIO st >>= \case- ChainState {syncing = Just ChainSync {peer}} -> return (Just peer)- _ -> return Nothing+-- | Validate and store a run of headers, and publish the best block if it+-- changed. Returns the node of the last header.+importHeaders ::+ Chain -> [BlockHeader] -> IO (Either String BlockNode)+importHeaders ch hs = do+ now <- getCurrentTime+ let timestamp = floor (utcTimeToPOSIXSeconds now)+ before <- withDB ch getBestBlockHeader+ withDB ch (connectBlocks ch.config.net timestamp hs) >>= \case+ Left e -> do+ logError ch.config.logger ("connectBlocks: " <> cs e)+ return (Left e)+ Right [] -> return (Left "no headers")+ Right (lst : _) -> do+ update ch (touchSync now)+ after <- withDB ch getBestBlockHeader+ when (before.header /= after.header) $+ chainEvent ch (ChainBestBlock after)+ return (Right lst) --- | Reserve a peer with 'setBusy' and make it the syncing peer. Returns 'False'--- if the peer is busy with something else.-setSyncingPeer :: (MonadIO m) => Chain -> Peer -> m Bool-setSyncingPeer ch p =- setBusy p >>= \case- False -> do- logDebug- ch.config.logger- ("Could not lock peer: " <> p.label)- return False- True -> do- logDebug ch.config.logger $- ("Locked peer: " <> p.label)- set_it- return True- where- set_it = do- now <- liftIO getCurrentTime- atomically $ modifyTVar ch.state $ \s ->- s- { syncing =- Just- ChainSync- { peer = p,- timestamp = now,- best = Nothing- }- }+-- | Publish a 'ChainSynced' event the first time the chain is synced (see+-- 'isSynced').+syncNotif :: Chain -> IO ()+syncNotif ch = do+ best <- withDB ch getBestBlockHeader+ now <- getCurrentTime+ notify <- atomically $ do+ s <- readTVar ch.state+ if isSynced ch.config.net now best s.sync && not s.beenInSync+ then writeTVar ch.state s {beenInSync = True} >> return True+ else return False+ when notify $ chainEvent ch (ChainSynced best) --- | Remove a peer from the queue of peers to sync and unset the syncing peer if--- it is set to the provided peer.-finishPeer :: (MonadIO m) => Chain -> Peer -> m ()-finishPeer ch p =- remove_peer >>= \case- False ->- logDebug- ch.config.logger- ("Removed peer " <> p.label <> " from queue")- True -> do- logDebug- ch.config.logger- ("Releasing syncing peer " <> p.label)- setFree p+-- | Run a loop in the background that sends a 'ChainPing' to the chain+-- process at random intervals. They are at most a quarter of the timeout, so+-- timeouts are noticed promptly, and between 0.1 and 20 seconds, so a short+-- timeout does not make the chain spin and a long one does not make it doze.+withClock ::+ NominalDiffTime -> Mailbox ChainMessage -> IO a -> IO a+withClock to mbox go =+ withAsync clock $ \a -> link a >> go where- remove_peer =- atomically $- readTVar ch.state >>= \s -> case s.syncing of- Just ChainSync {peer = p'}- | p == p' -> do- unset_syncing- return True- _ -> do- remove_from_queue- return False- unset_syncing =- modifyTVar ch.state $ \x ->- x {syncing = Nothing}- remove_from_queue =- modifyTVar ch.state $ \x ->- x {peers = delete p x.peers}---- | Return syncing peer data.-chainSyncingPeer :: (MonadIO m) => TVar ChainState -> m (Maybe ChainSync)-chainSyncingPeer st = (.syncing) <$> readTVarIO st+ upper :: Int+ upper = max 100000 (min 20000000 (floor (to * 1000000) `div` 4))+ clock = loopForever $ do+ threadDelay =<< randomRIO (upper `div` 10, upper)+ ChainPing `send` mbox -- | Get a block header from the block chain. Returns headers on side chains -- too; use 'chainBlockMain' to check whether a block is in the main chain.-chainGetBlock :: (MonadIO m) => Chain -> BlockHash -> m (Maybe BlockNode)-chainGetBlock ch bh = runReaderT (getBlockHeader bh) ch.config+chainGetBlock :: Chain -> BlockHash -> IO (Maybe BlockNode)+chainGetBlock ch bh = withDB ch (getBlockHeader bh) -- | Get best block header from chain process.-chainGetBest :: (MonadIO m) => Chain -> m BlockNode-chainGetBest ch = runReaderT getBestBlockHeader ch.config+chainGetBest :: Chain -> IO BlockNode+chainGetBest ch = withDB ch getBestBlockHeader -- | Get ancestor of 'BlockNode' at 'BlockHeight' from chain process. Returns -- 'Nothing' if the height is above that of the node. chainGetAncestor ::- (MonadIO m) => Chain -> BlockHeight -> BlockNode -> m (Maybe BlockNode)-chainGetAncestor ch h bn = runReaderT (getAncestor h bn) ch.config+ Chain -> BlockHeight -> BlockNode -> IO (Maybe BlockNode)+chainGetAncestor ch h bn = withDB ch (getAncestor h bn) -- | Get parents of 'BlockNode' starting at 'BlockHeight' from chain process. -- The result is in ascending order, from the block at the given height up to -- the parent of the given node, which is not included. chainGetParents ::- (MonadIO m) => Chain -> BlockHeight -> BlockNode -> m [BlockNode]-chainGetParents ch height top =- go [] top+ Chain -> BlockHeight -> BlockNode -> IO [BlockNode]+chainGetParents ch height = go [] where go acc b | height >= b.height = return acc- | otherwise = do- m <- chainGetBlock ch b.header.prev- case m of+ | otherwise =+ chainGetBlock ch b.header.prev >>= \case Nothing -> return acc Just p -> go (p : acc) p -- | Get last common block from chain process: the most recent block that is an -- ancestor of both nodes. chainGetSplitBlock ::- (MonadIO m) => Chain -> BlockNode -> BlockNode -> m BlockNode-chainGetSplitBlock ch l r = runReaderT (splitPoint l r) ch.config+ Chain -> BlockNode -> BlockNode -> IO BlockNode+chainGetSplitBlock ch l r = withDB ch (splitPoint l r) --- | Notify chain that a new peer is connected. It is added to the queue of--- peers to sync against.-chainPeerConnected :: (MonadIO m) => Chain -> Peer -> m ()-chainPeerConnected ch p =- ChainPeerConnected p `send` ch.mailbox+-- | Notify chain that a new peer is connected. It is queued to be asked for+-- headers.+chainPeerConnected :: Chain -> Peer -> IO ()+chainPeerConnected ch p = ChainPeerConnected p `send` ch.mailbox -- | Notify chain that a peer has disconnected.-chainPeerDisconnected :: (MonadIO m) => Chain -> Peer -> m ()-chainPeerDisconnected ch p =- ChainPeerDisconnected p `send` ch.mailbox+chainPeerDisconnected :: Chain -> Peer -> IO ()+chainPeerDisconnected ch p = ChainPeerDisconnected p `send` ch.mailbox -- | Is given 'BlockHash' in the main chain?-chainBlockMain :: (MonadIO m) => Chain -> BlockHash -> m Bool-chainBlockMain ch bh =- chainGetBest ch >>= \bb ->- chainGetBlock ch bh >>= \case- Nothing ->- return False- bm@(Just bn) ->- (== bm) <$> chainGetAncestor ch bn.height bb+chainBlockMain :: Chain -> BlockHash -> IO Bool+chainBlockMain ch bh = do+ best <- chainGetBest ch+ chainGetBlock ch bh >>= \case+ Nothing -> return False+ Just bn -> (== Just bn) <$> chainGetAncestor ch bn.height best -- | Is chain in sync with network? Once 'True', it stays 'True' even if the -- chain falls behind later.-chainIsSynced :: (MonadIO m) => Chain -> m Bool-chainIsSynced ch =- (.beenInSync) <$> readTVarIO (ch.state)+chainIsSynced :: Chain -> IO Bool+chainIsSynced ch = (.beenInSync) <$> readTVarIO ch.state -- | Peer sends a bunch of headers to the chain process. "Haskoin.Node" calls -- this for every headers message received from an online peer.-chainHeaders :: (MonadIO m) => Chain -> Peer -> [BlockHeader] -> m ()-chainHeaders ch p hs =- ChainHeaders p hs `send` ch.mailbox+chainHeaders :: Chain -> Peer -> [BlockHeader] -> IO ()+chainHeaders ch p hs = ChainHeaders p hs `send` ch.mailbox +-- | Peer announced blocks in an inventory message. If any of them is unknown,+-- the peer is asked for headers. "Haskoin.Node" calls this for every+-- inventory message with blocks received from an online peer.+chainBlocksAnnounced :: Chain -> Peer -> [BlockHash] -> IO ()+chainBlocksAnnounced ch p bhs = ChainBlocksAnnounced p bhs `send` ch.mailbox+ -- | Wait until the chain process has handled every message sent to it before.-chainSync :: (MonadIO m) => Chain -> m ()+chainSync :: Chain -> IO () chainSync ch = ChainSyncMsg `query` ch.mailbox
+ src/Haskoin/Node/Chain/Database.hs view
@@ -0,0 +1,169 @@+-- |+-- Module : Haskoin.Node.Chain.Database+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Storage of block headers in RocksDB, for "Haskoin.Node.Chain".+--+-- Headers are stored as 'BlockNode' values in the configured column family, or+-- the default one, under keys starting with byte @0x90@. The best block is+-- stored under key @0x91@ and a data version under key @0x92@. If the data+-- version does not match, keys @0x90@ and @0x91@ are deleted when the chain+-- starts. Applications that share the database must not use these prefixes.+--+-- = Why it works this way+--+-- * __Headers are stored, not kept in memory.__ Storing them lets a restart+-- resume where it stopped instead of downloading the whole chain again.+--+-- * __Through haskoin-core's interface.__ The header functions of+-- haskoin-core, such as 'connectBlocks' and 'getAncestor', run in any monad+-- with a 'BlockHeaders' instance. 'runHeaders' runs them against the+-- database, so validation and storage are not reimplemented here. The monad+-- that carries the database is a 'ReaderT', and it stays inside this+-- module: everything else calls 'runHeaders' and works in plain 'IO'.+--+-- * __Separate from syncing.__ This module only knows how headers are laid+-- out on disk. The chain process decides what to store and when.+module Haskoin.Node.Chain.Database+ ( ChainDB (..),+ initChainDB,+ runHeaders,+ )+where++import Control.Monad (guard, unless, when)+import Control.Monad.Trans.Reader (ReaderT (..))+import Data.ByteString qualified as B+import Data.Maybe (isNothing)+import Data.Serialize (Serialize (..), getWord8, putWord8)+import Data.Word (Word32)+import Database.RocksDB (ColumnFamily, DB)+import Database.RocksDB qualified as R+import Database.RocksDB.Query+ ( Key,+ KeyValue,+ insert,+ insertCF,+ insertOp,+ insertOpCF,+ retrieveCommon,+ writeBatch,+ )+import Haskoin hiding (Key)++-- | Where headers are stored: a database and an optional column family.+data ChainDB = ChainDB+ { -- | database handle+ db :: !DB,+ -- | column family, or the default one if 'Nothing'+ cf :: !(Maybe ColumnFamily)+ }++-- | Key of the data version.+data VersionKey = VersionKey+ deriving stock (Eq, Show)++instance Serialize VersionKey where+ get = getWord8 >>= guard . (== 0x92) >> return VersionKey+ put VersionKey = putWord8 0x92++instance Key VersionKey++instance KeyValue VersionKey Word32++-- | Key of a block header.+newtype BlockHeaderKey = BlockHeaderKey BlockHash+ deriving stock (Eq, Show)++instance Serialize BlockHeaderKey where+ get = getWord8 >>= guard . (== 0x90) >> BlockHeaderKey <$> get+ put (BlockHeaderKey bh) = putWord8 0x90 >> put bh++instance KeyValue BlockHeaderKey BlockNode++-- | Key of the best block.+data BestBlockKey = BestBlockKey+ deriving stock (Eq, Show)++instance Serialize BestBlockKey where+ get = getWord8 >>= guard . (== 0x91) >> return BestBlockKey+ put BestBlockKey = putWord8 0x91++instance KeyValue BestBlockKey BlockNode++-- | Run header functions of haskoin-core against the database.+runHeaders ::+ ChainDB -> (forall n. (BlockHeaders n) => n a) -> IO a+runHeaders c action = (runReaderT action c)++-- | Read and write headers in the database.+instance BlockHeaders (ReaderT ChainDB IO) where+ addBlockHeader bn = ReaderT $ \c -> insertCommon c (nodeKey bn) bn+ getBlockHeader bh =+ ReaderT $ \c -> retrieveCommon c.db c.cf (BlockHeaderKey bh)+ getBestBlockHeader = ReaderT $ \c ->+ (retrieveCommon c.db c.cf BestBlockKey) >>= \case+ Nothing -> error "Could not get best block from database"+ Just b -> return b+ setBestBlockHeader bn = ReaderT $ \c -> insertCommon c BestBlockKey bn+ addBlockHeaders bns =+ ReaderT $ \c -> writeBatch c.db $ map (op c) bns+ where+ op c bn = case c.cf of+ Nothing -> insertOp (nodeKey bn) bn+ Just cf -> insertOpCF cf (nodeKey bn) bn++nodeKey :: BlockNode -> BlockHeaderKey+nodeKey bn = BlockHeaderKey (headerHash bn.header)++insertCommon ::+ (KeyValue k v, Serialize k, Serialize v) =>+ ChainDB ->+ k ->+ v ->+ IO ()+insertCommon c k v = case c.cf of+ Nothing -> insert c.db k v+ Just cf -> insertCF c.db cf k v++-- | Version of the data layout.+dataVersion :: Word32+dataVersion = 1++-- | Prepare the database for a network. If its data version is not the+-- current one, headers and best block are deleted first. An empty database+-- gets the genesis block of the network as its best block.+initChainDB :: Network -> ChainDB -> IO ()+initChainDB net c = do+ ver <- retrieveCommon c.db c.cf VersionKey+ unless (ver == Just dataVersion) $ purgeChainDB c+ insertCommon c VersionKey dataVersion+ best <- retrieveCommon c.db c.cf BestBlockKey+ when (isNothing (best :: Maybe BlockNode)) $ runHeaders c $ do+ addBlockHeader (genesisNode net)+ setBestBlockHeader (genesisNode net)++-- | Delete headers and best block from the column family that holds the+-- chain. Keys are deleted one at a time as the iterator walks them, so that+-- memory use does not grow with the size of the database.+purgeChainDB :: ChainDB -> IO ()+purgeChainDB c = withIter $ \it -> do+ R.iterSeek it (B.singleton 0x90)+ let go =+ R.iterKey it >>= \case+ Just k | B.take 1 k `elem` [B.singleton 0x90, B.singleton 0x91] -> do+ case c.cf of+ Nothing -> R.delete c.db k+ Just cf -> R.deleteCF c.db cf k+ R.iterNext it+ go+ _ -> return ()+ go+ where+ withIter = case c.cf of+ Nothing -> R.withIter c.db+ Just cf -> R.withIterCF c.db cf
+ src/Haskoin/Node/Chain/Sync.hs view
@@ -0,0 +1,204 @@+-- |+-- Module : Haskoin.Node.Chain.Sync+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Which peer to ask for headers, as an explicit state machine.+--+-- Every connected peer has a 'PeerStatus'. "Haskoin.Node.Chain" asks one+-- peer at a time for headers. A peer is queued when it connects, and again+-- whenever it tells the node about blocks that the node does not have. If+-- nothing happens for a while, every idle peer is queued again.+--+-- = Why it works this way+--+-- * __Announcements can be missed.__ Peers announce a new block with a+-- headers message only while they believe the node knows its parent, and+-- fall back to an inventory message when many blocks arrive at once. If+-- the node only ever asked for headers when a peer connected, a missed or+-- unexpected announcement would leave it stuck at an old height for good.+-- So every hint of unknown blocks queues the peer, and a quiet period queues+-- every idle peer, which bounds how long any stall can last.+--+-- * __A peer that announced while syncing is asked again.__ Its last reply may+-- have been computed before the new block existed. The 'Syncing' status+-- remembers the announcement so that the peer is queued again when it is+-- done, rather than being forgotten or asked twice at once.+--+-- * __Misbehaving peers stay out.__ A peer that sent invalid headers or timed+-- out is marked 'Dropped' until its disconnection arrives, so nothing can+-- queue it in between.+--+-- * __Pure and polymorphic.__ The rules are plain functions on a value, over+-- any peer type with equality, so they can be tested exhaustively with+-- integers instead of network connections. Transitions that do not apply to+-- the current status leave the state unchanged, which makes them safe to+-- call whenever the matching event happens.+module Haskoin.Node.Chain.Sync+ ( SyncState,+ PeerStatus (..),+ emptySync,+ peerConnected,+ peerDisconnected,+ requestSync,+ startSync,+ refreshSync,+ finishSync,+ dropPeer,+ pollPeers,+ touchSync,+ peerStatus,+ syncingPeer,+ queuedPeers,+ connectedPeers,+ lastActivity,+ isSynced,+ maxTipAge,+ )+where++import Data.Maybe (isNothing, listToMaybe)+import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Haskoin (BlockHeader (..), BlockNode (..), Network (..))++-- | Sync status of a connected peer.+data PeerStatus+ = -- | Nothing to ask it for.+ Idle+ | -- | Waiting for its turn to be asked for headers.+ Queued+ | -- | Being asked for headers. Holds the time of the last request, and+ -- whether the peer announced more blocks meanwhile, in which case it is+ -- queued again when done.+ Syncing !UTCTime !Bool+ | -- | Misbehaved and is being disconnected. Never asked for headers again.+ Dropped+ deriving stock (Eq, Show)++-- | Connected peers in queue order, with the last time that headers were+-- requested or received.+data SyncState p = SyncState+ { peers :: ![(p, PeerStatus)],+ activity :: !UTCTime+ }+ deriving stock (Eq, Show)++-- | No peers, with the given time as the last activity.+emptySync :: UTCTime -> SyncState p+emptySync = SyncState []++-- | A peer completed the handshake. It is queued to be asked for headers.+peerConnected :: (Eq p) => p -> SyncState p -> SyncState p+peerConnected p s+ | Just _ <- peerStatus p s = s+ | otherwise = s {peers = s.peers <> [(p, Queued)]}++-- | A peer disconnected.+peerDisconnected :: (Eq p) => p -> SyncState p -> SyncState p+peerDisconnected p s = s {peers = filter ((/= p) . fst) s.peers}++-- | The peer told the node about blocks that it does not have.+requestSync :: (Eq p) => p -> SyncState p -> SyncState p+requestSync p s =+ case peerStatus p s of+ Just Idle -> enqueue p s+ Just (Syncing t _) -> setStatus p (Syncing t True) s+ _ -> s++-- | Start asking a queued peer for headers, unless another peer is being+-- asked already.+startSync :: (Eq p) => UTCTime -> p -> SyncState p -> SyncState p+startSync now p s =+ case (syncingPeer s, peerStatus p s) of+ (Nothing, Just Queued) -> setStatus p (Syncing now False) s {activity = now}+ _ -> s++-- | Another request was sent to the peer being synced against.+refreshSync :: (Eq p) => UTCTime -> p -> SyncState p -> SyncState p+refreshSync now p s =+ case peerStatus p s of+ Just (Syncing _ again) -> setStatus p (Syncing now again) s {activity = now}+ _ -> s++-- | The peer being synced against has no more headers to send.+finishSync :: (Eq p) => p -> SyncState p -> SyncState p+finishSync p s =+ case peerStatus p s of+ Just (Syncing _ True) -> enqueue p s+ Just (Syncing _ False) -> setStatus p Idle s+ _ -> s++-- | The peer misbehaved and is being disconnected.+dropPeer :: (Eq p) => p -> SyncState p -> SyncState p+dropPeer p s =+ case peerStatus p s of+ Just _ -> setStatus p Dropped s+ Nothing -> s++-- | Queue every idle peer, and record the time as activity so that polling+-- happens at most once per quiet period.+pollPeers :: UTCTime -> SyncState p -> SyncState p+pollPeers now s = SyncState {peers = map poll s.peers, activity = now}+ where+ poll (p, Idle) = (p, Queued)+ poll x = x++-- | Headers were received at the given time.+touchSync :: UTCTime -> SyncState p -> SyncState p+touchSync now s = s {activity = now}++-- | Status of a peer, or 'Nothing' if it is not connected.+peerStatus :: (Eq p) => p -> SyncState p -> Maybe PeerStatus+peerStatus p s = lookup p s.peers++-- | The peer being synced against, with the time of the last request to it.+syncingPeer :: SyncState p -> Maybe (p, UTCTime)+syncingPeer s = listToMaybe [(p, t) | (p, Syncing t _) <- s.peers]++-- | Queued peers, first in line first.+queuedPeers :: SyncState p -> [p]+queuedPeers s = [p | (p, Queued) <- s.peers]++-- | Connected peers that have not been dropped.+connectedPeers :: SyncState p -> [p]+connectedPeers s = [p | (p, st) <- s.peers, st /= Dropped]++-- | Last time headers were requested or received, or peers were polled.+lastActivity :: SyncState p -> UTCTime+lastActivity s = s.activity++-- | Oldest that the best block may be for the chain to count as synced: two+-- hours, the most that a block timestamp may be ahead of the network's time.+maxTipAge :: NominalDiffTime+maxTipAge = 7200++-- | Whether the header chain counts as synced at the given time: the best+-- block is at most 'maxTipAge' old, it has the 'minimumChainWork' of the+-- network, and no peer is being synced against or waiting to be.+--+-- Recent headers alone do not prove much: a peer can feed a cheap chain with+-- fresh timestamps. Bitcoin Core requires the same work before it leaves+-- initial block download. And while peers are still queued, a better chain may+-- be about to arrive.+isSynced :: Network -> UTCTime -> BlockNode -> SyncState p -> Bool+isSynced net now best s = recent && enough && idle+ where+ tipTime = posixSecondsToUTCTime (fromIntegral best.header.timestamp)+ recent = now `diffUTCTime` tipTime <= maxTipAge+ enough = best.work >= net.minimumChainWork+ idle = isNothing (syncingPeer s) && null (queuedPeers s)++setStatus :: (Eq p) => p -> PeerStatus -> SyncState p -> SyncState p+setStatus p st s = s {peers = map set s.peers}+ where+ set (q, old)+ | q == p = (q, st)+ | otherwise = (q, old)++-- | Put a peer at the end of the queue.+enqueue :: (Eq p) => p -> SyncState p -> SyncState p+enqueue p s = s {peers = filter ((/= p) . fst) s.peers <> [(p, Queued)]}
src/Haskoin/Node/Logger.hs view
@@ -1,11 +1,5 @@-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE NoFieldSelectors #-}- -- | Logging to standard error. A 'Logger' is a plain value carried in--- configuration records, so code that logs only needs 'MonadIO'.+-- configuration records, so code that logs only needs 'IO'. -- -- The node used to log through a logging monad transformer. That forced every -- function into that monad, and made polymorphic loops leak memory when GHC did@@ -24,6 +18,7 @@ ) where +import Control.Exception (bracket) import Control.Monad (when) import Data.Text (Text) import Data.Text qualified as T@@ -34,7 +29,6 @@ newFastLogger1, toLogStr, )-import UnliftIO (MonadIO, bracket, liftIO) -- | Severity of a log message, from least to most severe. data LogLevel@@ -74,9 +68,9 @@ -- | Log a message at a level, if the level is at least that of the 'Logger'. -- Lines look like monad-logger's: @[Info#Chain] message@.-logAt :: (MonadIO m) => LogLevel -> Logger -> Text -> m ()+logAt :: LogLevel -> Logger -> Text -> IO () logAt l logger msg =- when (l >= logger.level) . liftIO . logger.push $+ when (l >= logger.level) . logger.push $ "[" <> toLogStr (drop 5 (show l)) <> tag <> "] " <> toLogStr msg <> "\n" where tag@@ -84,7 +78,7 @@ | otherwise = "#" <> toLogStr logger.source -- | Log at 'LevelDebug', 'LevelInfo', 'LevelWarn' or 'LevelError'.-logDebug, logInfo, logWarn, logError :: (MonadIO m) => Logger -> Text -> m ()+logDebug, logInfo, logWarn, logError :: Logger -> Text -> IO () logDebug = logAt LevelDebug logInfo = logAt LevelInfo logWarn = logAt LevelWarn
src/Haskoin/Node/Peer.hs view
@@ -1,16 +1,3 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE NoFieldSelectors #-}- -- | -- Module : Haskoin.Node.Peer -- Copyright : (c) Haskoin Developers@@ -37,17 +24,20 @@ -- the payload read, so a peer cannot make the node buffer a huge message it -- would refuse anyway. ----- * Anything that does not parse closes the connection. There is no attempt to--- recover, because a peer that sends garbage once is not worth the risk.+-- * Anything that does not parse closes the connection with a+-- 'PeerException' saying why. There is no attempt to recover, because a+-- peer that sends garbage once is not worth the risk. -- -- * Reading pauses when the node falls behind ('maxPendingMessages'), so the -- speed of the node, not the speed of the peer, sets the pace. module Haskoin.Node.Peer ( PeerConfig (..), PeerEvent (..),+ PeerException (..), Conduits (..), WithConnection, Peer (..),+ PeerMessage (..), peer, wrapPeer, sendMessage,@@ -56,6 +46,12 @@ getTxs, getData, pingPeer,+ Owner,+ newOwner,+ ownerName,+ reservePeer,+ releasePeer,+ peerHolder, getBusy, setBusy, setFree,@@ -65,14 +61,14 @@ where import Conduit-import Control.Exception+import Control.Concurrent.Async (link, withAsync)+import Control.Concurrent.STM+import Control.Exception (Exception, throwIO) import Control.Monad-import Control.Monad.Trans.Maybe import Data.Bool (bool) import Data.ByteString (ByteString) import Data.ByteString qualified as B import Data.Function (on)-import Data.List (union) import Data.Maybe (isJust) import Data.Serialize (decode, runGet, runPut) import Data.String.Conversions (cs)@@ -81,9 +77,11 @@ import Haskoin import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever)+import Haskoin.Node.Peer.Lock+import Haskoin.Node.Peer.Request import NQE import System.Random-import UnliftIO+import System.Timeout (timeout) -- | The two directions of a network connection, as conduits. --@@ -124,6 +122,26 @@ PeerMessage !Peer !Message deriving (Eq) +-- | Why a peer process stopped with an error. The peer manager logs it and+-- forgets the peer.+--+-- These used to be calls to 'error' with a message. A type lets callers and+-- tests tell the reasons apart without parsing text.+data PeerException+ = -- | the peer closed the connection in the middle of a message+ ConnectionClosed+ | -- | the 24-byte message header could not be parsed+ InvalidMessageHeader+ | -- | the message is for another network; holds its magic bytes+ WrongNetwork !Word32+ | -- | the payload is larger than allowed for the command; holds its size+ MessageTooLarge !MessageCommand !Word32+ | -- | the payload could not be parsed; holds the reason+ InvalidPayload !MessageCommand !String+ deriving stock (Show)++instance Exception PeerException+ -- | Mailbox for a peer. Peers are compared by their mailbox, and shown by their -- label. data Peer = Peer@@ -133,8 +151,9 @@ pub :: !(Publisher PeerEvent), -- | name of the peer in logs, usually its address label :: !Text,- -- | whether the peer is reserved for a task; see 'setBusy'- busy :: !(TVar Bool),+ -- | who has reserved the peer for a long conversation; see+ -- 'reservePeer'+ busy :: !(TVar (Lock Owner)), -- | messages from this peer that the node has not finished handling pending :: !(TVar Int) }@@ -152,11 +171,11 @@ | -- | send a network message to the peer SendMessage !Message --- | Build a 'Peer' handle from its configuration, its busy flag, its counter of--- pending messages, and the mailbox of its process.+-- | Build a 'Peer' handle from its configuration, its lock (which starts+-- 'Free'), its counter of pending messages, and the mailbox of its process. wrapPeer :: PeerConfig ->- TVar Bool ->+ TVar (Lock Owner) -> TVar Int -> Mailbox PeerMessage -> Peer@@ -178,7 +197,7 @@ -- | 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 :: Peer -> IO () peerMessageDone p = atomically $ modifyTVar' p.pending (max 0 . subtract 1) -- | Run peer process in current thread. The 'Peer' must be the one that@@ -189,21 +208,22 @@ -- called or the connection is closed, and throws if the peer sends invalid -- data. peer ::- (MonadUnliftIO m) => PeerConfig -> Peer -> Inbox PeerMessage ->- m ()-peer cfg@PeerConfig {label, net, connect, pub, logger} p inbox =- withRunInIO $ \run -> connect (run . peer_session)+ IO ()+peer cfg@PeerConfig {label, net, pub, logger} p inbox =+ cfg.connect peer_session where+ -- Commands from the mailbox, as a stream of messages to send. go = do- logDebug logger $ label <> " awaiting event..."- msg <- receive inbox- dispatchMessage cfg msg >>= bool (return ()) go+ m <- liftIO $ do+ logDebug logger $ label <> " awaiting event..."+ receive inbox+ dispatchMessage cfg m >>= bool (return ()) go peer_session ad = do- let ins = transPipe liftIO ad.inboundConduit- ons = transPipe liftIO ad.outboundConduit+ let ins = ad.inboundConduit+ ons = ad.outboundConduit src = runConduit $ ins@@ -214,25 +234,24 @@ link as runConduit (go .| snk) -- Wait until the node has room for another message from this peer.- send_msg msg = do+ send_msg m = do atomically $ do n <- readTVar p.pending- checkSTM (n < maxPendingMessages)+ check (n < maxPendingMessages) writeTVar p.pending (n + 1)- publish (PeerMessage p msg) pub+ publish (PeerMessage p m) pub -- | Internal function to dispatch peer messages. dispatchMessage ::- (MonadIO m) => PeerConfig -> PeerMessage ->- ConduitT i Message m Bool-dispatchMessage PeerConfig {label, logger} (SendMessage msg) = do- logDebug logger (label <> " sending cmd " <> cs (show (msgType msg)))- yield msg+ ConduitT i Message IO Bool+dispatchMessage PeerConfig {label, logger} (SendMessage m) = do+ liftIO $ logDebug logger (label <> " sending cmd " <> cs (show (msgType m)))+ yield m return True dispatchMessage PeerConfig {label, logger} KillPeer = do- logInfo logger (label <> " disconnecting")+ liftIO $ logInfo logger (label <> " disconnecting") return False -- | Internal conduit to parse messages coming from peer. The header is checked@@ -245,47 +264,32 @@ -- soon as it reads a count above the limit of the message. The limits are -- 2000 headers and 1000 addresses, for example. inPeerConduit ::- (MonadIO m) => PeerConfig -> ConduitT ByteString Message m ()+ PeerConfig -> ConduitT ByteString Message IO () 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"+ debug (label <> " awaiting message...")+ x <- takeExactly 24 MessageHeader mgc cmd len _ <- case decode x of- Left _ -> bad "invalid message header"+ Left _ -> bad InvalidMessageHeader Right h -> return h- when (mgc /= net.magic) $- bad ("wrong network magic " <> cs (show mgc))- when (len > maxPayloadSize net cmd) $- 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))+ when (mgc /= net.magic) $ bad (WrongNetwork mgc)+ when (len > maxPayloadSize net cmd) $ bad (MessageTooLarge cmd len)+ debug (label <> " sent cmd " <> cs (show cmd))+ y <- takeExactly (fromIntegral len) case runGet (getMessage net) $ x `B.append` y of- Left e ->- bad- ( "invalid payload for cmd "- <> cs (show cmd)- <> ": "- <> cs (firstLine e)- )- Right msg -> do- logDebug- logger- (label <> " sent payload for cmd " <> cs (show cmd))- yield msg+ Left e -> bad (InvalidPayload cmd (firstLine e))+ Right m -> do+ debug (label <> " sent payload for cmd " <> cs (show cmd))+ yield m where- bad :: (MonadIO m) => Text -> m a- bad e = do- logError logger (label <> " sent " <> e)- error ("Peer sent " <> cs e)+ debug = liftIO . logDebug logger+ takeExactly n = do+ bs <- takeCE n .| foldC+ when (B.length bs /= n) $ bad ConnectionClosed+ return bs+ bad :: PeerException -> ConduitT i o IO a+ bad e = liftIO $ do+ logError logger (label <> ": " <> cs (show e))+ throwIO e -- The reason is on the first line of the parser's error text. The lines -- after it are call stack noise. firstLine = takeWhile (/= '\n')@@ -296,34 +300,56 @@ -- | Ask a peer to disconnect. The peer process stops after sending any messages -- queued before this one.-killPeer :: (MonadIO m) => Peer -> m ()+killPeer :: Peer -> IO () killPeer p = KillPeer `send` p.mailbox -- | Send a network message to peer. Returns as soon as the message is queued.-sendMessage :: (MonadIO m) => Message -> Peer -> m ()-sendMessage msg p = SendMessage msg `send` p.mailbox---- | Check whether a peer is reserved for a task.-getBusy :: (MonadIO m) => Peer -> m Bool-getBusy p = readTVarIO p.busy+sendMessage :: Message -> Peer -> IO ()+sendMessage m p = SendMessage m `send` p.mailbox --- | Try to reserve a peer for a task, such as syncing headers. Returns 'True' if--- the peer was free and is now reserved, or 'False' if it was already busy.+-- | Try to reserve a peer for a long conversation, such as syncing headers+-- or downloading blocks. Returns 'True' if the peer was free and the owner now+-- holds it, or 'False' if anyone holds it, including the same owner. ----- Reservation exists because some conversations with a peer span many--- messages, like downloading a long run of headers. Two tasks using the same peer--- for such a conversation at once would confuse each other's replies.-setBusy :: (MonadIO m) => Peer -> m Bool-setBusy p =- atomically $ do- b <- readTVar p.busy- unless b $ writeTVar p.busy True- return $ not b+-- Two tasks holding such a conversation with the same peer at once would+-- confuse each other's replies. "Haskoin.Node.Peer.Lock" has the rules.+reservePeer :: Owner -> Peer -> IO Bool+reservePeer o p = atomically $ do+ lock <- readTVar p.busy+ case acquire o lock of+ Just lock' -> writeTVar p.busy lock' >> return True+ Nothing -> return False --- | Release a peer reserved with 'setBusy'.-setFree :: (MonadIO m) => Peer -> m ()-setFree p = atomically $ writeTVar p.busy False+-- | Release a peer. Returns 'False', and changes nothing, if the owner does not+-- hold it, so that a task cannot free a peer in the middle of another task's+-- conversation.+releasePeer :: Owner -> Peer -> IO Bool+releasePeer o p = atomically $ do+ lock <- readTVar p.busy+ case release o lock of+ Just lock' -> writeTVar p.busy lock' >> return True+ Nothing -> return False +-- | Who holds a peer, if anyone.+peerHolder :: Peer -> IO (Maybe Owner)+peerHolder p = holder <$> readTVarIO p.busy++-- | Check whether a peer is reserved by anyone.+getBusy :: Peer -> IO Bool+getBusy p = isJust <$> peerHolder p++-- | Reserve a peer on behalf of a single owner shared by every caller of this+-- function. Use 'reservePeer' with an owner from 'newOwner' instead.+setBusy :: Peer -> IO Bool+setBusy = reservePeer legacyOwner+{-# DEPRECATED setBusy "Use reservePeer with an owner from newOwner" #-}++-- | Release a peer reserved with 'setBusy'. Peers reserved through+-- 'reservePeer' by another owner are left alone.+setFree :: Peer -> IO ()+setFree = void . releasePeer legacyOwner+{-# DEPRECATED setFree "Use releasePeer with the owner that reserved the peer" #-}+ -- | Request full blocks from peer. Will return 'Nothing' if the list of blocks -- returned by the peer is incomplete, comes out of order, or a timeout is -- reached.@@ -331,17 +357,15 @@ -- The 'Int' argument is the timeout in seconds. On networks with SegWit, -- blocks are requested with their witness data. getBlocks ::- (MonadUnliftIO m) => Network -> Int -> Peer -> [BlockHash] ->- m (Maybe [Block])+ IO (Maybe [Block]) getBlocks net time p bhs =- runMaybeT $ mapM f =<< MaybeT (getData time p (GetData ivs))+ (>>= traverse block) <$> getData time p (GetData ivs) where- f (Right b) = return b- f (Left _) = MaybeT $ return Nothing+ block = either (const Nothing) Just c | net.segWit = InvWitnessBlock | otherwise = InvBlock@@ -354,17 +378,15 @@ -- The 'Int' argument is the timeout in seconds. Peers normally only serve -- transactions that are in their mempool. getTxs ::- (MonadUnliftIO m) => Network -> Int -> Peer -> [TxHash] ->- m (Maybe [Tx])+ IO (Maybe [Tx]) getTxs net time p ths =- runMaybeT $ mapM f =<< MaybeT (getData time p (GetData ivs))+ (>>= traverse tx) <$> getData time p (GetData ivs) where- f (Right _) = MaybeT $ return Nothing- f (Left t) = return t+ tx = either Just (const Nothing) c | net.segWit = InvWitnessTx | otherwise = InvTx@@ -376,51 +398,27 @@ -- -- The first argument is the timeout in seconds. A ping is sent right after the -- request; since peers answer in order, receiving the matching pong before all--- items means that the peer will not send the rest. Transactions are returned--- as 'Left' and blocks as 'Right', in the order they were requested.+-- items means that the peer will not send the rest. Unrelated messages that+-- arrive meanwhile are ignored. Transactions are returned as 'Left' and blocks+-- as 'Right', in the order they were requested. "Haskoin.Node.Peer.Request"+-- has the rules. getData ::- (MonadUnliftIO m) => Int -> Peer -> GetData -> m (Maybe [Either Tx Block])+ Int -> Peer -> GetData -> IO (Maybe [Either Tx Block]) getData seconds p gd@(GetData ivs) = withSubscription p.pub $ \inb -> do r <- randomIO MGetData gd `sendMessage` p MPing (Ping r) `sendMessage` p- fmap join- . timeout (seconds * 1000 * 1000)- . runMaybeT- $ get_thing inb r [] ivs+ join <$> timeout (seconds * 1000 * 1000) (step inb (newRequest r ivs)) where- get_thing _inb _r acc [] =- return $ reverse acc- get_thing inb r acc hss@(InvVector t h : hs) =- lift (filterReceive p inb) >>= \case- MTx tx- | is_tx t && (txHash tx).get == h ->- get_thing inb r (Left tx : acc) hs- MBlock b@(Block bh _)- | is_block t && (headerHash bh).get == h ->- get_thing inb r (Right b : acc) hs- MNotFound (NotFound nvs)- | not (null (nvs `union` hs)) ->- MaybeT $ return Nothing- MPong (Pong r')- | r == r' ->- MaybeT $ return Nothing- _- | null acc ->- get_thing inb r acc hss- | otherwise ->- MaybeT $ return Nothing- is_tx InvWitnessTx = True- is_tx InvTx = True- is_tx _ = False- is_block InvWitnessBlock = True- is_block InvBlock = True- is_block _ = False+ step inb = \case+ Complete items -> return (Just items)+ Failed _ -> return Nothing+ Continue req -> filterReceive p inb >>= step inb . stepRequest req -- | Ping a peer and await response. Return 'False' if response not received -- before timeout. The first argument is the timeout in seconds.-pingPeer :: (MonadUnliftIO m) => Int -> Peer -> m Bool+pingPeer :: Int -> Peer -> IO Bool pingPeer time p = fmap isJust . withSubscription p.pub $ \sub -> do r <- randomIO@@ -432,8 +430,8 @@ -- | Receive the next message from the given peer, discarding events about other -- peers.-filterReceive :: (MonadIO m) => Peer -> Inbox PeerEvent -> m Message+filterReceive :: Peer -> Inbox PeerEvent -> IO Message filterReceive p inb = receive inb >>= \case- PeerMessage p' msg | p == p' -> return msg+ PeerMessage p' m | p == p' -> return m _ -> filterReceive p inb
+ src/Haskoin/Node/Peer/Lock.hs view
@@ -0,0 +1,102 @@+-- |+-- Module : Haskoin.Node.Peer.Lock+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Exclusive use of a peer, as an explicit state machine.+--+-- Some conversations with a peer span many messages, such as downloading a+-- long run of headers or blocks. Two tasks holding the same conversation with+-- one peer would confuse each other's replies, so a task reserves the peer+-- first. The header chain does this, and so do applications that download+-- blocks, such as Haskoin Store.+--+-- = Why it works this way+--+-- * __Only the holder can release.__ The lock used to be a flag that anyone+-- could clear. A task that released a peer it did not hold, by mistake or+-- after its own reservation had already ended, would free the peer in the+-- middle of another task's conversation. Here a peer is 'Free' or held by an+-- 'Owner', and 'release' only succeeds for the holder.+--+-- * __Pure and polymorphic.__ The transitions are plain functions over any+-- owner type with equality, so the rules can be tested with integers.+-- "Haskoin.Node.Peer" keeps a 'Lock' 'Owner' in each 'Haskoin.Node.Peer.Peer'+-- and applies them atomically.+module Haskoin.Node.Peer.Lock+ ( Lock (..),+ acquire,+ release,+ holder,+ Owner,+ newOwner,+ legacyOwner,+ ownerName,+ )+where++import Data.Function (on)+import Data.Text (Text)+import Data.Unique (Unique, newUnique)++-- | Whether a peer is reserved, and by whom.+data Lock o+ = -- | nobody is using the peer for a long conversation+ Free+ | -- | the owner is using the peer+ HeldBy !o+ deriving stock (Eq, Show)++-- | Reserve a free peer. 'Nothing' if it is held, even by the same owner:+-- reserving twice is a mistake that would hide a missing release.+acquire :: o -> Lock o -> Maybe (Lock o)+acquire o = \case+ Free -> Just (HeldBy o)+ HeldBy _ -> Nothing++-- | Release a peer. 'Nothing' unless the owner holds it.+release :: (Eq o) => o -> Lock o -> Maybe (Lock o)+release o = \case+ HeldBy o' | o == o' -> Just Free+ _ -> Nothing++-- | Who holds the peer, if anyone.+holder :: Lock o -> Maybe o+holder = \case+ Free -> Nothing+ HeldBy o -> Just o++-- | A task that reserves peers. Owners are compared by identity, and named+-- for logs.+data Owner+ = Owner !Unique !Text+ | -- | the owner behind the deprecated 'Haskoin.Node.Peer.setBusy' and+ -- 'Haskoin.Node.Peer.setFree'+ Legacy++instance Eq Owner where+ (==) = (==) `on` key+ where+ key (Owner u _) = Just u+ key Legacy = Nothing++instance Show Owner where+ show = show . ownerName++-- | A new owner with a name for logs. Each call gives a different owner.+newOwner :: Text -> IO Owner+newOwner name = (`Owner` name) <$> newUnique++-- | The single owner shared by every caller of the deprecated+-- 'Haskoin.Node.Peer.setBusy' and 'Haskoin.Node.Peer.setFree'.+legacyOwner :: Owner+legacyOwner = Legacy++-- | Name of an owner.+ownerName :: Owner -> Text+ownerName = \case+ Owner _ name -> name+ Legacy -> "legacy"
+ src/Haskoin/Node/Peer/Request.hs view
@@ -0,0 +1,104 @@+-- |+-- 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
src/Haskoin/Node/PeerMgr.hs view
@@ -1,16 +1,3 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}- -- | -- Module : Haskoin.Node.PeerMgr -- Copyright : (c) Haskoin Developers@@ -21,8 +8,8 @@ -- -- The peer manager keeps the node connected to the network. ----- It maintains a pool of known peer addresses, filled from the static peers in--- the configuration, from the DNS seeds of the network when discovery is on, and+-- It keeps a book of peer addresses, filled from the static peers in the+-- configuration, from the DNS seeds of the network when discovery is on, and -- from addresses relayed by connected peers. A background loop connects to new -- peers while there are fewer than 'maxPeers' of them. --@@ -31,14 +18,26 @@ -- with a 'PeerConnected' event. Peers are pinged periodically to measure their -- latency, and the list returned by 'getPeers' is sorted by median ping time, so -- that the fastest peers come first. A peer is disconnected if it stops--- responding, does not answer a ping in time, or has been connected for longer--- than 'maxPeerLife'. A 'PeerDisconnected' event is published when an online--- peer goes away.+-- responding, does not answer a ping in time, does not complete the handshake+-- in time, or has been connected for longer than 'maxPeerLife'. A+-- 'PeerDisconnected' event is published when an announced peer goes away. -- -- Most of the @peerMgr*@ functions are used by "Haskoin.Node" to feed messages -- received from peers to the peer manager; applications rarely need to call -- them directly. --+-- = State machines+--+-- The rules live in pure modules, and this one stores their state and carries+-- out what they decide:+--+-- * "Haskoin.Node.PeerMgr.Lifecycle": the life of each connection, from the+-- handshake ("Haskoin.Node.PeerMgr.Handshake") through pings to its+-- disconnection, with a reason.+--+-- * "Haskoin.Node.PeerMgr.Addresses": which address to try next, and how long+-- to wait before trying an address again after its connection ended.+-- -- = Why it works this way -- -- * __Peers are ranked by measured latency.__ The fastest peers deliver@@ -54,6 +53,14 @@ -- longer than 'timeout' is dropped. Waiting for a peer that has gone away -- would stall whatever was waiting on it. --+-- * __A dropped peer is gone at once.__ A peer told to disconnect is no longer+-- offered by 'getPeers' and its messages are no longer routed, even before+-- its connection closes.+--+-- * __Bad addresses wait.__ An address that fails is tried again later each+-- time, and one whose peer broke the protocol is not tried again for a long+-- time, so the node does not hammer dead peers or return to bad ones.+-- -- * __Relayed addresses are untrusted.__ Peers gossip addresses, and a -- malicious peer could gossip private or loopback addresses to make the node -- probe its own network. Only public addresses are accepted, and only a@@ -62,6 +69,9 @@ ( PeerMgrConfig (..), PeerEvent (..), OnlinePeer (..),+ PeerState (..),+ Stage (..),+ DisconnectReason (..), PeerMgr, withPeerMgr, peerMgrBest,@@ -83,27 +93,31 @@ where import Control.Applicative ((<|>))-import Control.Arrow-import Control.Monad-import Data.Bits ((.&.))+import Control.Concurrent (threadDelay)+import Control.Concurrent.Async (Async, ExceptionInLinkedThread (..), link, withAsync)+import Control.Concurrent.STM+import Control.Exception (SomeException, fromException)+import Control.Monad (forM_, when, (<=<)) import Data.Function (on)-import Data.List (dropWhileEnd, elemIndex, find, nub, sort)-import Data.Maybe-import Data.Set (Set)-import Data.Set qualified as Set+import Data.List (find, sort)+import Data.Maybe (fromMaybe) import Data.String.Conversions (cs) import Data.Time.Clock import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)-import Data.Word (Word32, Word64, Word8)+import Data.Word (Word32, Word64)+import GHC.Records (HasField (..)) import Haskoin+import Haskoin.Node.Address (isRoutable, toHostService, toSockAddr) import Haskoin.Node.Logger import Haskoin.Node.Loop (loopForever) import Haskoin.Node.Peer+import Haskoin.Node.Peer.Lock (Lock (..))+import Haskoin.Node.PeerMgr.Addresses+import Haskoin.Node.PeerMgr.Handshake+import Haskoin.Node.PeerMgr.Lifecycle import NQE-import Network.Socket+import Network.Socket (SockAddr) import System.Random (randomIO, randomRIO)-import UnliftIO-import UnliftIO.Concurrent -- | Configuration of the peer manager. "Haskoin.Node" builds it from the -- 'Haskoin.Node.NodeConfig'.@@ -143,9 +157,9 @@ mailbox :: !(Mailbox PeerMgrMessage), -- | height of the best block, sent to peers in version messages best :: !(TVar BlockHeight),- -- | known addresses not yet connected to, at most 'maxKnownAddresses'- addresses :: !(TVar (Set SockAddr)),- -- | peers being connected to or online, fastest first+ -- | addresses to connect to+ addresses :: !(TVar (AddressBook SockAddr)),+ -- | open connections, fastest peers first peers :: !(TVar [OnlinePeer]) } @@ -153,10 +167,10 @@ data PeerMgrMessage = -- | connect to a peer address Connect !SockAddr- | -- | check whether a peer timed out or must be pinged+ | -- | check the timers of a peer CheckPeer !Peer- | -- | a peer process stopped- PeerDied !Child+ | -- | a peer process stopped, with the exception that stopped it+ PeerDied !Child !(Maybe SomeException) | -- | the height of the best block changed ManagerBest !BlockHeight | -- | a peer acknowledged our version@@ -172,181 +186,177 @@ | -- | reply once every previous message has been handled Sync !(Listen ()) --- | Data structure representing an online peer.+-- | A connection to a peer. -- -- Peers are compared for equality by their 'Peer' handle, and ordered by the -- median of their recent ping times, so that sorting a list puts the fastest -- peers first. Peers without any ping measurement sort as if their median ping -- were 60 seconds.+--+-- The fields @version@, @online@, @verack@, @ping@, @pings@, @connected@ and+-- @tickled@ of earlier releases are now computed from 'state', so record dot+-- syntax such as @o.version@ keeps working. They cannot be set or pattern+-- matched. data OnlinePeer = OnlinePeer { -- | network address of the peer address :: !SockAddr,- -- | whether the peer acknowledged our version- verack :: !Bool,- -- | whether the handshake is complete: both version and verack received- online :: !Bool,- -- | version message sent by the peer, if received- version :: !(Maybe Version),+ -- | stage of the connection and its timers; see+ -- "Haskoin.Node.PeerMgr.Lifecycle"+ state :: !PeerState, -- | thread running the peer process async :: !(Async ()), -- | handle to talk to the peer mailbox :: !Peer, -- | nonce of the version message we sent, used to detect connections to -- ourselves- nonce :: !Word64,- -- | time and nonce of the ping awaiting an answer, if any- ping :: !(Maybe (UTCTime, Word64)),- -- | round-trip times of the last eleven pings, sorted- pings :: ![NominalDiffTime],- -- | when the connection was made- connected :: !UTCTime,- -- | when the peer last sent a message- tickled :: !UTCTime+ nonce :: !Word64 } instance Eq OnlinePeer where- (==) = (==) `on` f- where- f OnlinePeer {mailbox = p} = p+ (==) = (==) `on` (.mailbox) instance Ord OnlinePeer where- compare = compare `on` f- where- f OnlinePeer {pings = pings} = fromMaybe 60 (median pings)+ compare = compare `on` \o -> fromMaybe 60 (median o.state.pings) +-- | Version message sent by the peer, if received.+instance HasField "version" OnlinePeer (Maybe Version) where+ getField o = stageVersion o.state++-- | Whether the peer is online: the handshake is complete and the peer is not+-- being disconnected.+instance HasField "online" OnlinePeer Bool where+ getField o = isOnline o.state++-- | Whether the peer acknowledged our version.+instance HasField "verack" OnlinePeer Bool where+ getField o = case o.state.stage of+ Handshaking h -> hasVerAck h+ Online _ _ -> True+ Disconnecting _ -> False++-- | Time and nonce of the ping awaiting an answer, if any.+instance HasField "ping" OnlinePeer (Maybe (UTCTime, Word64)) where+ getField o = pendingPing o.state++-- | Round-trip times of the last eleven pings, sorted.+instance HasField "pings" OnlinePeer [NominalDiffTime] where+ getField o = sort o.state.pings++-- | When the connection was opened.+instance HasField "connected" OnlinePeer UTCTime where+ getField o = o.state.connected++-- | When the peer last sent a message.+instance HasField "tickled" OnlinePeer UTCTime where+ getField o = o.state.lastHeard+ -- | Start a peer manager and pass it to the given action. The peer manager and -- all its peers are stopped when the action returns. -- -- The peer manager does not connect to any peer until it learns the height of -- the best block through 'peerMgrBest'. "Haskoin.Node" does this as soon as the -- chain is loaded.-withPeerMgr ::- (MonadUnliftIO m) =>- PeerMgrConfig ->- (PeerMgr -> m a) ->- m a+withPeerMgr :: PeerMgrConfig -> (PeerMgr -> IO a) -> IO a withPeerMgr cfg action = do- ibx <- newInbox- withSupervisor (Notify (death ibx)) $ \sup -> do- bb <- newTVarIO 0- kp <- newTVarIO Set.empty- ob <- newTVarIO []+ inbox <- newInbox+ let died (a, e) = PeerDied a e `sendSTM` inbox+ withSupervisor (Notify died) $ \sup -> do+ best <- newTVarIO 0+ addresses <- newTVarIO emptyBook+ peers <- newTVarIO [] let mgr = PeerMgr { config = cfg, supervisor = sup,- mailbox = inboxToMailbox ibx,- best = bb,- addresses = kp,- peers = ob+ mailbox = inboxToMailbox inbox,+ best = best,+ addresses = addresses,+ peers = peers }- go mgr ibx- where- death ibx (a, _e) = PeerDied a `sendSTM` ibx- go mgr ibx = withAsync (peerManager mgr ibx) $ \a ->+ withAsync (peerManager mgr inbox) $ \a -> withConnectLoop mgr (link a >> action mgr) -- | Main loop of the peer manager process. Waits for the best block height, then -- handles messages forever.-peerManager ::- (MonadUnliftIO m) => PeerMgr -> Inbox PeerMgrMessage -> m ()-peerManager mgr ibx = do+peerManager :: PeerMgr -> Inbox PeerMgrMessage -> IO ()+peerManager mgr inbox = do logDebug mgr.config.logger "Getting best block..."- putBestBlock mgr <=< receiveMatch ibx $ \case+ (atomically . writeTVar mgr.best) <=< receiveMatch inbox $ \case ManagerBest b -> Just b _ -> Nothing loopForever $ do logDebug mgr.config.logger "Awaiting event..."- dispatch mgr =<< receive ibx---- | Store the height of the best block.-putBestBlock :: (MonadIO m) => PeerMgr -> BlockHeight -> m ()-putBestBlock mgr bb = atomically $ writeTVar mgr.best bb---- | Read the height of the best block.-getBestBlock :: (MonadIO m) => PeerMgr -> m BlockHeight-getBestBlock mgr = readTVarIO mgr.best---- | Network constants of the peer manager.-getNetwork :: PeerMgr -> Network-getNetwork mgr = mgr.config.net---- | Add the static peers and, if discovery is on, the network seeds to the known addresses.-loadPeers :: (MonadIO m) => PeerMgr -> m ()-loadPeers mgr = do- loadStaticPeers mgr- loadNetSeeds mgr---- | Resolve the static peers in the configuration and add them to the known addresses.-loadStaticPeers :: (MonadIO m) => PeerMgr -> m ()-loadStaticPeers mgr =- mapM_ (newPeer mgr) . concat- =<< mapM (liftIO . toSockAddr mgr.config.net) mgr.config.peers---- | If discovery is on, resolve the DNS seeds of the network and add them to the known addresses.-loadNetSeeds :: (MonadIO m) => PeerMgr -> m ()-loadNetSeeds mgr =- when mgr.config.discover $ do- ss <-- concat- <$> mapM (liftIO . toSockAddr mgr.config.net) mgr.config.net.seeds- mapM_ (newPeer mgr) ss+ dispatch mgr =<< receive inbox --- | Log how many peers are online out of the maximum.-logConnectedPeers :: (MonadIO m) => PeerMgr -> m ()-logConnectedPeers mgr = do- let m = mgr.config.maxPeers- l <- length <$> getConnectedPeers mgr- logInfo- mgr.config.logger- ("Peers connected: " <> cs (show l) <> "/" <> cs (show m))+-- | Time limits of connections.+limits :: PeerMgr -> Limits+limits mgr = Limits {timeout = mgr.config.timeout, maxLife = mgr.config.maxPeerLife} --- | All peers being connected to or online, fastest first.-getOnlinePeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer]-getOnlinePeers mgr = readTVarIO mgr.peers+-- | Handle a message sent to the peer manager.+dispatch :: PeerMgr -> PeerMgrMessage -> IO ()+dispatch mgr = \case+ PeerVersion p v -> do+ logDebug mgr.config.logger ("Received version from peer " <> p.label)+ nonces <- map (.nonce) <$> readTVarIO mgr.peers+ peerInput mgr p $ either Refused (GotHandshake . GotVersion) (checkVersion nonces v)+ PeerVerAck p -> do+ logDebug mgr.config.logger ("Received version ack from peer " <> p.label)+ peerInput mgr p (GotHandshake GotVerAck)+ PeerPong p n -> do+ now <- getCurrentTime+ peerInput mgr p (GotPong now n)+ CheckPeer p -> do+ now <- getCurrentTime+ nonce <- randomIO+ peerInput mgr p (Tick now nonce)+ PeerPing p n -> MPong (Pong n) `sendMessage` p+ PeerAddrs p nas -> relayedAddresses mgr p nas+ ManagerBest h -> atomically $ writeTVar mgr.best h+ Connect sa -> connectPeer mgr sa+ PeerDied a e -> peerDied mgr a e+ Sync r -> atomically (r ()) --- | Peers that completed the handshake, fastest first.-getConnectedPeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer]-getConnectedPeers mgr = filter (.online) <$> getOnlinePeers mgr+-- | Apply an input to the state of a connection and carry out the resulting+-- actions. Inputs for connections that are gone are ignored.+peerInput :: PeerMgr -> Peer -> PeerInput -> IO ()+peerInput mgr p input = do+ result <- atomically $ do+ ops <- readTVar mgr.peers+ case find ((== p) . (.mailbox)) ops of+ Nothing -> return Nothing+ Just o -> do+ let (st, actions) = stepPeer (limits mgr) input o.state+ o' = o {state = st}+ writeTVar mgr.peers (sort (o' : filter (/= o) ops))+ return (Just (o', actions))+ forM_ result $ \(o, actions) -> forM_ actions (perform o)+ where+ perform o = \case+ ReplyVerAck -> MVerAck `sendMessage` p+ Announce -> do+ atomically $ modifyTVar' mgr.addresses (markConnected o.address)+ logInfo mgr.config.logger ("Connected to peer " <> p.label)+ publish (PeerConnected p) mgr.config.pub+ logConnectedPeers mgr+ SendPing n -> MPing (Ping n) `sendMessage` p+ Disconnect r -> do+ logWarn+ mgr.config.logger+ ("Disconnecting peer " <> p.label <> ": " <> cs (show r))+ killPeer p --- | Publish a peer event.-managerEvent :: (MonadIO m) => PeerMgr -> PeerEvent -> m ()-managerEvent mgr e = publish e mgr.config.pub+-- | Record that a peer just sent a message, so that it is not disconnected for+-- being idle. "Haskoin.Node" calls this for every message received.+ticklePeer :: PeerMgr -> Peer -> IO ()+ticklePeer mgr p = getCurrentTime >>= peerInput mgr p . Heard --- | Handle a message sent to the peer manager.-dispatch ::- (MonadUnliftIO m) => PeerMgr -> PeerMgrMessage -> m ()-dispatch mgr (PeerVersion p v) = do- logDebug- mgr.config.logger- ("Received peer " <> p.label <> " version " <> cs (show v))- let b = mgr.peers- atomically (setPeerVersion b p v) >>= \case- Just o -> do- when o.online (announcePeer mgr p)- logDebug- mgr.config.logger- ("Sending version ack to peer " <> p.label)- MVerAck `sendMessage` p- Nothing -> do- logWarn- mgr.config.logger- ("Version rejected for peer " <> p.label)- killPeer p-dispatch mgr (PeerVerAck p) = do- atomically (setPeerVerAck mgr.peers p) >>= \case- Just o -> do- logDebug mgr.config.logger ("Received version ack from peer " <> p.label)- when o.online (announcePeer mgr p)- Nothing -> do- logWarn- mgr.config.logger- ("Verack rejected for peer " <> p.label)- killPeer p-dispatch mgr (PeerAddrs p nas)+-- | Learn the routable addresses that an online peer relayed, if discovery is+-- on.+relayedAddresses :: PeerMgr -> Peer -> [NetworkAddress] -> IO ()+relayedAddresses mgr p nas | mgr.config.discover = getOnlinePeer mgr p >>= \case- Just OnlinePeer {online = True} -> do+ Just o | isOnline o.state -> do let sas = filter isRoutable $ map (hostToSockAddr . (.address)) nas logDebug mgr.config.logger@@ -358,451 +368,212 @@ <> cs (show (length sas)) <> " routable" )- forM_ sas (newPeer mgr)+ atomically . modifyTVar' mgr.addresses $ \book ->+ foldr (addAddress Discovered) book sas _ -> logDebug mgr.config.logger- ("Ignoring addresses from peer before handshake " <> p.label)+ ("Ignoring addresses from peer that is not online " <> 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- ("Received pong " <> cs (show n) <> " from peer " <> p.label)- now <- liftIO getCurrentTime- atomically (gotPong mgr.peers n now p)-dispatch mgr (PeerPing p n) = do- logDebug- mgr.config.logger- ("Responding to ping " <> cs (show n) <> " from peer " <> p.label)- MPong (Pong n) `sendMessage` p-dispatch mgr (ManagerBest h) = do- logDebug mgr.config.logger ("Setting best block to " <> cs (show h))- putBestBlock mgr h-dispatch mgr (Connect sa) = do- connectPeer mgr sa-dispatch mgr (PeerDied a) = do- processPeerOffline mgr a-dispatch mgr (CheckPeer p) = do- logDebug mgr.config.logger ("Housekeeping for peer " <> p.label)- checkPeer mgr p --- | Record that a peer just sent a message, so that it is not disconnected for--- being idle. "Haskoin.Node" calls this for every message received.-ticklePeer :: (MonadIO m) => PeerMgr -> Peer -> m ()-ticklePeer mgr p = do- logDebug mgr.config.logger ("Tickling peer " <> p.label)- t <- liftIO getCurrentTime- atomically (modifyPeer mgr.peers p (\o -> o {tickled = t}))---- | Disconnect a peer that has been connected for too long, has been idle for--- too long, or did not answer a ping in time. Otherwise ping it if no ping is--- pending.-checkPeer :: (MonadIO m) => PeerMgr -> Peer -> m ()-checkPeer mgr p =- atomically (mgr.peers `findPeer` p) >>= \case- Just o -> do- now <- liftIO getCurrentTime- let expired = mgr.config.maxPeerLife `addUTCTime` o.connected- 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")- killPeer p- | 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- logError mgr.config.logger ("Peer " <> p.label <> " is offline")+-- | Forget a connection whose process stopped, decide when to try its address+-- again, and publish its disconnection if it had been announced.+peerDied :: PeerMgr -> Child -> Maybe SomeException -> IO ()+peerDied mgr a e = do+ now <- getCurrentTime+ gone <- atomically $ do+ ops <- readTVar mgr.peers+ case find ((== a) . (.async)) ops of+ Nothing -> return Nothing+ Just o -> do+ let outcome = connectionOutcome o.state e+ writeTVar mgr.peers (filter (/= o) ops)+ modifyTVar' mgr.addresses (finishAddress now outcome o.address)+ return (Just (o, outcome))+ case gone of+ Nothing -> logError mgr.config.logger "Unknown peer process stopped"+ Just (o, outcome) -> do+ logInfo mgr.config.logger $+ "Connection to peer "+ <> o.mailbox.label+ <> " ended: "+ <> cs (show outcome)+ <> maybe "" (\x -> " (" <> cs (show x) <> ")") e+ when o.state.announced $+ publish (PeerDisconnected o.mailbox) mgr.config.pub+ logConnectedPeers mgr --- | Send a ping with a random nonce to an online peer and remember when it was sent.-sendPing :: (MonadIO m) => PeerMgr -> Peer -> m ()-sendPing mgr p = do- atomically (mgr.peers `findPeer` p) >>= \case- Nothing ->- logWarn mgr.config.logger ("Will not ping unknown peer " <> p.label)- Just o- | o.online -> do- n <- randomIO- now <- liftIO getCurrentTime- atomically (setPeerPing mgr.peers n now p)- logDebug- mgr.config.logger- ("Sending ping " <> cs (show n) <> " to peer " <> p.label)- MPing (Ping n) `sendMessage` p- | otherwise ->- logDebug- mgr.config.logger- ("Will not ping offline peer " <> p.label)+-- | How a connection ended, from its last state and the exception that stopped+-- its process, if any.+--+-- A peer process only stops without an exception when told to disconnect. If+-- the peer manager did not do that, another process did, such as the chain+-- after invalid headers, so the address waits as after a failure.+connectionOutcome :: PeerState -> Maybe SomeException -> Outcome+connectionOutcome st e =+ case (st.stage, peerException =<< e) of+ (Disconnecting TooOld, _) -> Disconnected+ (Disconnecting (Rejected _), _) -> Misbehaved+ (Disconnecting _, _) -> Failed+ (_, Just ConnectionClosed)+ | st.announced -> Disconnected+ | otherwise -> Failed+ (_, Just _) -> Misbehaved+ (_, Nothing) -> Failed+ where+ peerException x =+ fromException x+ <|> (fromException x >>= \(ExceptionInLinkedThread _ inner) -> fromException inner) --- | Clean up after a peer process stops, and publish a disconnection event if the peer was online.-processPeerOffline :: (MonadIO m) => PeerMgr -> Child -> m ()-processPeerOffline mgr a = do- atomically (findPeerAsync mgr.peers a) >>= \case- Nothing -> logError mgr.config.logger "Disconnected unknown peer"- Just o -> do- if o.online- then do- logError- mgr.config.logger- ("Disconnected peer " <> o.mailbox.label)- managerEvent mgr (PeerDisconnected o.mailbox)- else- logError- mgr.config.logger- ("Could not connect to peer " <> o.mailbox.label)- atomically (removePeer mgr.peers o.mailbox)- logConnectedPeers mgr+-- | Log how many peers are online out of the maximum.+logConnectedPeers :: PeerMgr -> IO ()+logConnectedPeers mgr = do+ l <- length <$> getPeers mgr+ logInfo+ mgr.config.logger+ ("Peers connected: " <> cs (show l) <> "/" <> cs (show mgr.config.maxPeers)) --- | Publish a connection event for a peer that just completed the handshake.-announcePeer :: (MonadIO m) => PeerMgr -> Peer -> m ()-announcePeer mgr p = do- atomically (findPeer mgr.peers p) >>= \case- Just OnlinePeer {online = True} -> do- logInfo mgr.config.logger ("Connected to peer " <> p.label)- managerEvent mgr (PeerConnected p)- logConnectedPeers mgr- Just OnlinePeer {online = False} ->- return ()- Nothing ->- logWarn mgr.config.logger ("Not announcing disconnected peer " <> p.label)+-- | Next address to connect to. The configured peers and, if discovery is on,+-- the seeds are loaded when there is nothing to try and they were not loaded+-- recently.+getNewPeer :: PeerMgr -> IO (Maybe SockAddr)+getNewPeer mgr = do+ now <- getCurrentTime+ reload <- needsReload now <$> readTVarIO mgr.addresses+ when reload $ loadPeers mgr now+ atomically . stateTVar mgr.addresses $ \book ->+ case takeAddress now book of+ Just (sa, book') -> (Just sa, book')+ Nothing -> (Nothing, book) --- | Take an address from the known addresses. If there are none, reload the static peers and seeds first.-getNewPeer :: (MonadIO m) => PeerMgr -> m (Maybe SockAddr)-getNewPeer mgr =- g >>= \case- Nothing -> loadPeers mgr >> g- Just a -> return (Just a)- where- g =- atomically $- stateTVar- mgr.addresses- (first (listToMaybe . Set.elems) . Set.splitAt 1)+-- | Resolve the configured peers and, if discovery is on, the seeds of the+-- network, and add them to the address book.+loadPeers :: PeerMgr -> UTCTime -> IO ()+loadPeers mgr now = do+ let net = mgr.config.net+ resolve = fmap concat . mapM (toSockAddr net)+ configured <- resolve mgr.config.peers+ seeds <- if mgr.config.discover then resolve net.seeds else return []+ atomically . modifyTVar' mgr.addresses $ \book ->+ markLoaded now $+ foldr (addAddress Discovered) (foldr (addAddress Configured) book configured) seeds --- | Start a peer process for an address and send it our version, unless there--- are no free slots or the address is already connected.-connectPeer :: (MonadUnliftIO m) => PeerMgr -> SockAddr -> m ()+-- | Start a peer process for an address taken from the address book and send+-- it our version, unless there are no free slots or the address is connected+-- already, in which case the address is given back.+connectPeer :: PeerMgr -> SockAddr -> IO () 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- logInfo mgr.config.logger ("Connecting to peer " <> cs (show sa))- nonce <- randomIO- bb <- getBestBlock mgr- now <- liftIO getCurrentTime- let rmt = NetworkAddress (srv mgr.config.net) (sockToHostAddress sa)- unix = floor (utcTimeToPOSIXSeconds now)- ver = buildVersion mgr.config.net nonce bb mgr.config.address rmt unix- text = cs (show sa)- (inbox, mailbox) <- newMailbox- let pc =- PeerConfig- { pub = mgr.config.pub,- net = mgr.config.net,- label = text,- connect = mgr.config.connect sa,- logger = withSource "Peer" mgr.config.logger+ ops <- readTVarIO mgr.peers+ if+ | length ops >= mgr.config.maxPeers -> giveBack "no free slots"+ | any ((== sa) . (.address)) ops -> giveBack "already connected"+ | otherwise -> do+ logInfo mgr.config.logger ("Connecting to peer " <> cs (show sa))+ nonce <- randomIO+ height <- readTVarIO mgr.best+ now <- getCurrentTime+ let net = mgr.config.net+ services = if net.segWit then 8 else 0+ rmt = NetworkAddress services (sockToHostAddress sa)+ unix = floor (utcTimeToPOSIXSeconds now)+ ver = buildVersion net nonce height mgr.config.address rmt unix+ pc =+ PeerConfig+ { pub = mgr.config.pub,+ net = net,+ label = cs (show sa),+ connect = mgr.config.connect sa,+ logger = withSource "Peer" mgr.config.logger+ }+ (inbox, mailbox) <- newMailbox+ busy <- newTVarIO Free+ pending <- newTVarIO 0+ let p = wrapPeer pc busy pending mailbox+ -- The peer manager handles messages one at a time, so the peer cannot+ -- die before it is in the list.+ a <- mgr.supervisor `addChild` withHousekeeping mgr p (peer pc p inbox)+ MVersion ver `sendMessage` p+ atomically . modifyTVar' mgr.peers $ \x ->+ sort $+ OnlinePeer+ { address = sa,+ state = newPeerState now,+ async = a,+ mailbox = p,+ nonce = nonce }- busy <- newTVarIO False- pending <- newTVarIO 0- let p = wrapPeer pc busy pending mailbox- a <- withRunInIO $ \io ->- mgr.supervisor `addChild` io (launch pc inbox p)- MVersion ver `sendMessage` p- atomically $- insertPeer- mgr.peers- OnlinePeer- { address = sa,- verack = False,- online = False,- version = Nothing,- async = a,- mailbox = p,- nonce = nonce,- pings = [],- ping = Nothing,- connected = now,- tickled = now- }+ : x where- srv net- | net.segWit = 8- | otherwise = 0- launch pc inbox p =- withPeerLoop mgr p (\a -> link a >> peer pc p inbox)+ giveBack why = do+ logDebug+ mgr.config.logger+ ("Not connecting to peer " <> cs (show sa) <> ": " <> why)+ atomically $ modifyTVar' mgr.addresses (returnAddress sa) --- | Run a loop in the background that asks the peer manager to check a peer at--- random intervals between a quarter and a half of the timeout.-withPeerLoop :: (MonadUnliftIO m) => PeerMgr -> Peer -> (Async a -> m a) -> m a-withPeerLoop mgr p =- withAsync . loopForever $ do- let timeout = mgr.config.timeout- ms = floor (timeout * 1000 * 1000)- r <- randomRIO (ms `div` 4, ms `div` 2)- threadDelay r- managerCheck mgr p+-- | Run a peer process along with a loop that asks the peer manager to check+-- the peer's timers at random intervals between a quarter and a half of the+-- timeout.+withHousekeeping :: PeerMgr -> Peer -> IO () -> IO ()+withHousekeeping mgr p go =+ withAsync checks $ \a -> link a >> go+ where+ micros = floor (mgr.config.timeout * 1000000) :: Int+ checks = loopForever $ do+ threadDelay =<< randomRIO (micros `div` 4, micros `div` 2)+ CheckPeer p `send` mgr.mailbox -- | Run a loop in the background that connects to a new peer every 0.1 to 5 -- seconds while there are free slots.-withConnectLoop :: (MonadUnliftIO m) => PeerMgr -> m a -> m a+withConnectLoop :: PeerMgr -> IO a -> IO a withConnectLoop mgr act = withAsync go $ \a -> link a >> act where go = loopForever $ do- l <- length <$> getOnlinePeers mgr- when- (l < mgr.config.maxPeers)- (getNewPeer mgr >>= mapM_ (managerConnect mgr))- threadDelay =<< randomRIO (10 ^ 5, 5 * 10 ^ 6)---- | Upper bound on addresses remembered for future connections. Peers keep--- relaying addresses for as long as the node runs, and with every connection--- slot in use nothing ever removes them, so the pool must be bounded.-maxKnownAddresses :: Int-maxKnownAddresses = 4096---- | Add an address to the known addresses, unless it is connected already or the pool is full.-newPeer :: (MonadIO m) => PeerMgr -> SockAddr -> m ()-newPeer mgr sa =- atomically $- findPeerAddress mgr.peers sa >>= \case- Just _ -> return ()- Nothing -> modifyTVar' mgr.addresses $ \known ->- if Set.size known >= maxKnownAddresses- then known- else Set.insert sa known---- | Record the round-trip time of a ping if the nonce matches the pending one.-gotPong :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM ()-gotPong b nonce now p =- findPeer b p >>= \case- Just o@OnlinePeer {ping = Just (time, nonce')} | nonce' == nonce -> do- let d = now `diffUTCTime` time- o' = o {ping = Nothing, pings = sort (take 11 (d : o.pings))}- insertPeer b o'- _ -> return ()---- | Remember the time and nonce of a ping sent to a peer.-setPeerPing :: TVar [OnlinePeer] -> Word64 -> UTCTime -> Peer -> STM ()-setPeerPing b nonce now p =- modifyPeer b p $ \o -> o {ping = Just (now, nonce)}---- | Store the version of a peer. Returns 'Nothing' to reject it if the peer does--- not serve the full chain, is ourselves, is unknown, or already sent a version.------ A peer must offer the full chain ('nodeNetwork') to be useful for syncing, and--- a version whose nonce matches one of ours means the node has connected to--- itself, which happens when it learns its own address from gossip.-setPeerVersion :: TVar [OnlinePeer] -> Peer -> Version -> STM (Maybe OnlinePeer)-setPeerVersion b p v- | v.services .&. nodeNetwork == 0 = return Nothing- | otherwise =- readTVar b >>= \ops ->- if any (\o -> v.nonce == o.nonce) ops- then return Nothing -- peer is myself- 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- return (Just n)---- | Record that a peer acknowledged our version. Returns 'Nothing' if the peer is--- unknown or already sent one.-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'- return (Just o')- Nothing -> return Nothing---- | Find a peer in the list.-findPeer :: TVar [OnlinePeer] -> Peer -> STM (Maybe OnlinePeer)-findPeer b p =- find ((== p) . (.mailbox))- <$> readTVar b---- | Add or replace a peer in the list, keeping it sorted.-insertPeer :: TVar [OnlinePeer] -> OnlinePeer -> STM ()-insertPeer b o =- modifyTVar b $ \x -> sort . nub $ o : x---- | Change a peer in the list, if it is there.-modifyPeer :: TVar [OnlinePeer] -> Peer -> (OnlinePeer -> OnlinePeer) -> STM ()-modifyPeer b p f =- findPeer b p >>= \case- Nothing -> return ()- Just o -> insertPeer b $ f o---- | Remove a peer from the list.-removePeer :: TVar [OnlinePeer] -> Peer -> STM ()-removePeer b p = modifyTVar b (filter ((/= p) . (.mailbox)))---- | Find a peer by the thread that runs it.-findPeerAsync :: TVar [OnlinePeer] -> Async () -> STM (Maybe OnlinePeer)-findPeerAsync b a = find ((== a) . (.async)) <$> readTVar b---- | Find a peer by its address.-findPeerAddress :: TVar [OnlinePeer] -> SockAddr -> STM (Maybe OnlinePeer)-findPeerAddress b a = find ((== a) . (.address)) <$> readTVar b---- | Peers that completed the handshake, sorted so that those with the lowest--- median ping come first.-getPeers :: (MonadIO m) => PeerMgr -> m [OnlinePeer]-getPeers = getConnectedPeers---- | Information about a peer, or 'Nothing' if the peer manager does not know it.--- The peer may still be in the middle of the handshake; check 'online'.-getOnlinePeer :: (MonadIO m) => PeerMgr -> Peer -> m (Maybe OnlinePeer)-getOnlinePeer mgr p = atomically (mgr.peers `findPeer` p)+ n <- length <$> readTVarIO mgr.peers+ when (n < mgr.config.maxPeers) $+ getNewPeer mgr >>= mapM_ (\sa -> Connect sa `send` mgr.mailbox)+ threadDelay =<< randomRIO (100000 :: Int, 5000000) --- | Ask the peer manager to check a peer.-managerCheck :: (MonadIO m) => PeerMgr -> Peer -> m ()-managerCheck mgr p = CheckPeer p `send` mgr.mailbox+-- | Peers that are online, sorted so that those with the lowest median ping+-- come first. Peers in the handshake or being disconnected are left out.+getPeers :: PeerMgr -> IO [OnlinePeer]+getPeers mgr = filter (isOnline . (.state)) <$> readTVarIO mgr.peers --- | Ask the peer manager to connect to an address.-managerConnect :: (MonadIO m) => PeerMgr -> SockAddr -> m ()-managerConnect mgr sa = Connect sa `send` mgr.mailbox+-- | Information about a connection, or 'Nothing' if the peer manager does not+-- know it. The peer may still be in the handshake, or being disconnected;+-- check its 'state'.+getOnlinePeer :: PeerMgr -> Peer -> IO (Maybe OnlinePeer)+getOnlinePeer mgr p = find ((== p) . (.mailbox)) <$> readTVarIO mgr.peers -- | Tell the peer manager the height of the best block.-peerMgrBest :: (MonadIO m) => PeerMgr -> BlockHeight -> m ()+peerMgrBest :: PeerMgr -> BlockHeight -> IO () peerMgrBest mgr bh = ManagerBest bh `send` mgr.mailbox -- | Tell the peer manager that a peer acknowledged our version.-peerMgrVerAck :: (MonadIO m) => PeerMgr -> Peer -> m ()+peerMgrVerAck :: PeerMgr -> Peer -> IO () peerMgrVerAck mgr p = PeerVerAck p `send` mgr.mailbox -- | Tell the peer manager that a peer sent its version.-peerMgrVersion :: (MonadIO m) => PeerMgr -> Peer -> Version -> m ()+peerMgrVersion :: PeerMgr -> Peer -> Version -> IO () peerMgrVersion mgr p ver = PeerVersion p ver `send` mgr.mailbox -- | Tell the peer manager that a peer sent a ping, so that it answers with a pong.-peerMgrPing :: (MonadIO m) => PeerMgr -> Peer -> Word64 -> m ()+peerMgrPing :: PeerMgr -> Peer -> Word64 -> IO () peerMgrPing mgr p nonce = PeerPing p nonce `send` mgr.mailbox -- | Tell the peer manager that a peer answered a ping.-peerMgrPong :: (MonadIO m) => PeerMgr -> Peer -> Word64 -> m ()+peerMgrPong :: PeerMgr -> Peer -> Word64 -> IO () peerMgrPong mgr p nonce = PeerPong p nonce `send` mgr.mailbox -- | Tell the peer manager about addresses relayed by a peer. They are added to -- the known addresses if discovery is on and they are routable.-peerMgrAddrs :: (MonadIO m) => PeerMgr -> Peer -> [NetworkAddress] -> m ()+peerMgrAddrs :: PeerMgr -> Peer -> [NetworkAddress] -> IO () 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 :: PeerMgr -> IO () 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---- | Whether an IPv4 address is public unicast.-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---- | Split a peer address into host and port. Accepts @host@, @host:port@,--- @[ipv6]@ and @[ipv6]:port@. Either part is 'Nothing' if missing.-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 (drop 1) $ splitAt i xs- | x == ':' -> do- return (str, "")- _ -> Nothing- in (host, srv)---- | Resolve a peer address given as in 'toHostService' into socket addresses,--- using the default port of the network if none is given. Returns an empty list--- if it cannot be resolved.-toSockAddr :: Network -> String -> IO [SockAddr]-toSockAddr net str =- go `catch` e- where- go = fmap (map addrAddress) $ getAddrInfo Nothing host srv- (host, srv) =- second (<|> Just (show net.defaultPort)) $- toHostService str- e :: (Monad m) => SomeException -> m [SockAddr]- e _ = return [] -- | Median of a list, or 'Nothing' if it is empty. median :: (Ord a, Fractional a) => [a] -> Maybe a
+ src/Haskoin/Node/PeerMgr/Addresses.hs view
@@ -0,0 +1,208 @@+-- |+-- Module : Haskoin.Node.PeerMgr.Addresses+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- The addresses that the peer manager connects to, as an explicit state+-- machine.+--+-- Every known address is 'Ready' to be tried, 'InUse' by a connection, or+-- 'Waiting' until a given time. How long an address waits after a connection+-- ends depends on how it ended ('Outcome').+--+-- = Why it works this way+--+-- * __Failing addresses back off.__ The node used to try a static peer that+-- refused connections again within seconds, forever, and an address it had+-- just disconnected for misbehaving could be connected again at once. Here a+-- failed address waits twice as long after each failure, up to+-- 'maxBackoff', and a misbehaving one waits 'banTime'.+--+-- * __Configured peers are trusted more.__ Addresses from the configuration+-- are what the operator asked for, so they are never banned and never+-- forgotten: they back off at most 'maxBackoff', even after misbehaving.+-- Discovered addresses, from DNS seeds and gossip, are banned for+-- misbehaving, and at most 'maxDiscovered' of them are kept, so gossip+-- cannot exhaust memory.+--+-- * __Pure, with time as an input.__ Every transition takes the current time,+-- so the timing rules can be tested without waiting. "Haskoin.Node.PeerMgr"+-- keeps the book in a 'Control.Concurrent.STM.TVar'.+module Haskoin.Node.PeerMgr.Addresses+ ( AddressBook,+ Source (..),+ AddressState (..),+ Outcome (..),+ emptyBook,+ addAddress,+ takeAddress,+ returnAddress,+ markConnected,+ finishAddress,+ addressState,+ needsReload,+ markLoaded,+ knownAddresses,+ retryDelay,+ maxBackoff,+ banTime,+ reloadInterval,+ maxDiscovered,+ )+where++import Data.List (find)+import Data.Map.Strict (Map)+import Data.Map.Strict qualified as Map+import Data.Maybe (isNothing)+import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime)++-- | Where an address came from.+data Source+ = -- | the static peers of the configuration+ Configured+ | -- | DNS seeds, or addresses relayed by peers+ Discovered+ deriving stock (Eq, Show)++-- | Whether an address can be tried.+data AddressState+ = -- | can be tried now+ Ready+ | -- | a connection to it is open or being opened+ InUse+ | -- | can be tried again at the given time; holds the number of failures in+ -- a row+ Waiting !UTCTime !Int+ deriving stock (Eq, Show)++-- | How a connection ended.+data Outcome+ = -- | the peer was online and the connection ended normally, such as by+ -- rotation or because the peer closed it+ Disconnected+ | -- | the connection could not be made, or the peer stopped responding+ Failed+ | -- | the peer broke the protocol+ Misbehaved+ deriving stock (Eq, Show)++data Entry = Entry+ { source :: !Source,+ state :: !AddressState,+ -- | failures in a row+ failures :: !Int+ }+ deriving stock (Eq, Show)++-- | Known addresses, and when the configured peers and seeds were last+-- loaded.+data AddressBook a = AddressBook+ { entries :: !(Map a Entry),+ loaded :: !(Maybe UTCTime)+ }+ deriving stock (Eq, Show)++-- | Wait after a normal disconnection before trying the address again.+retryDelay :: NominalDiffTime+retryDelay = 1++-- | Longest wait after failures.+maxBackoff :: NominalDiffTime+maxBackoff = 600++-- | Wait after a discovered address misbehaved.+banTime :: NominalDiffTime+banTime = 24 * 3600++-- | Shortest time between two loads of the configured peers and seeds.+reloadInterval :: NominalDiffTime+reloadInterval = 300++-- | Most discovered addresses kept.+maxDiscovered :: Int+maxDiscovered = 4096++-- | No addresses, never loaded.+emptyBook :: AddressBook a+emptyBook = AddressBook {entries = Map.empty, loaded = Nothing}++-- | Learn an address. A known address keeps its state; one from the+-- configuration is marked 'Configured'. A discovered address is dropped if+-- 'maxDiscovered' are known already.+addAddress :: (Ord a) => Source -> a -> AddressBook a -> AddressBook a+addAddress src a book =+ case Map.lookup a book.entries of+ Just e+ | src == Configured -> put e {source = Configured}+ | otherwise -> book+ Nothing+ | src == Discovered && discovered >= maxDiscovered -> book+ | otherwise -> put Entry {source = src, state = Ready, failures = 0}+ where+ put e = book {entries = Map.insert a e book.entries}+ discovered = Map.size (Map.filter ((== Discovered) . (.source)) book.entries)++-- | Take an address to connect to: a 'Ready' one, or a 'Waiting' one whose+-- time has come. It becomes 'InUse'.+takeAddress :: (Ord a) => UTCTime -> AddressBook a -> Maybe (a, AddressBook a)+takeAddress now book = do+ (a, e) <- find (ready . snd) (Map.toList book.entries)+ return (a, setEntry a e {state = InUse} book)+ where+ ready e = case e.state of+ Ready -> True+ InUse -> False+ Waiting t _ -> t <= now++-- | Give back an address taken with 'takeAddress' without connecting to it.+returnAddress :: (Ord a) => a -> AddressBook a -> AddressBook a+returnAddress = adjust $ \e -> case e.state of+ InUse -> e {state = Ready}+ _ -> e++-- | A connection to the address completed the handshake. Its failures are+-- forgotten.+markConnected :: (Ord a) => a -> AddressBook a -> AddressBook a+markConnected = adjust $ \e -> e {failures = 0}++-- | A connection to the address ended. Decide when to try it again.+finishAddress ::+ (Ord a) => UTCTime -> Outcome -> a -> AddressBook a -> AddressBook a+finishAddress now outcome = adjust $ \e ->+ let wait d f = e {state = Waiting (d `addUTCTime` now) f, failures = f}+ backoff f = min maxBackoff (retryDelay * 2 ^ min f 30)+ in case outcome of+ Disconnected -> wait retryDelay 0+ Failed -> let f = e.failures + 1 in wait (backoff f) f+ Misbehaved+ | e.source == Configured -> wait maxBackoff (e.failures + 1)+ | otherwise -> wait banTime (e.failures + 1)++-- | State of an address, or 'Nothing' if it is not known.+addressState :: (Ord a) => a -> AddressBook a -> Maybe AddressState+addressState a book = (.state) <$> Map.lookup a book.entries++-- | Whether to load the configured peers and seeds: when no address can be+-- tried now, and they were never loaded or not loaded for 'reloadInterval'.+needsReload :: (Ord a) => UTCTime -> AddressBook a -> Bool+needsReload now book =+ isNothing (takeAddress now book)+ && maybe True (\t -> now `diffUTCTime` t >= reloadInterval) book.loaded++-- | The configured peers and seeds were loaded at the given time.+markLoaded :: UTCTime -> AddressBook a -> AddressBook a+markLoaded now book = book {loaded = Just now}++-- | Every known address.+knownAddresses :: AddressBook a -> [a]+knownAddresses book = Map.keys book.entries++setEntry :: (Ord a) => a -> Entry -> AddressBook a -> AddressBook a+setEntry a e book = book {entries = Map.insert a e book.entries}++adjust :: (Ord a) => (Entry -> Entry) -> a -> AddressBook a -> AddressBook a+adjust f a book = book {entries = Map.adjust f a book.entries}
+ src/Haskoin/Node/PeerMgr/Handshake.hs view
@@ -0,0 +1,142 @@+-- |+-- Module : Haskoin.Node.PeerMgr.Handshake+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- The version handshake with a peer, as an explicit state machine.+--+-- The node sends its version message as soon as a connection opens. The peer+-- must answer with its own version and acknowledge ours with a verack, in+-- either order. Only once both have arrived is the peer usable, and only then+-- does "Haskoin.Node.PeerMgr" announce it with a+-- 'Haskoin.Node.Peer.PeerConnected' event.+--+-- = Why a state machine+--+-- The handshake used to be tracked with independent flags, one for the+-- version and one for the verack, with each message handler checking the+-- flags it cared about. Every handler had to remember every rule, and a+-- combination nobody thought of, such as a second verack, would silently do+-- something. Here the states are listed once, 'stepHandshake' says what each+-- message does in each state, and a message that makes no sense in the+-- current state is an error. Unforeseen sequences are rejected instead of+-- handled by accident, and the rules can be tested exhaustively without a+-- network.+--+-- The module is pure. "Haskoin.Node.PeerMgr" stores the state of each peer+-- and carries out the actions that a transition returns.+module Haskoin.Node.PeerMgr.Handshake+ ( Handshake (..),+ HandshakeEvent (..),+ HandshakeAction (..),+ HandshakeError (..),+ stepHandshake,+ handshakeVersion,+ isEstablished,+ hasVerAck,+ checkVersion,+ )+where++import Data.Bits ((.&.))+import Data.Word (Word64)+import Haskoin (Version (..), nodeNetwork)++-- | How far the handshake with a peer has progressed.+data Handshake+ = -- | Our version was sent; nothing has been received yet.+ AwaitingBoth+ | -- | The version of the peer arrived; its verack has not.+ AwaitingVerAck !Version+ | -- | The verack of the peer arrived; its version has not.+ AwaitingVersion+ | -- | Both arrived. The peer is online.+ Established !Version+ deriving stock (Eq, Show)++-- | Handshake messages received from a peer.+data HandshakeEvent+ = -- | the peer sent its version+ GotVersion !Version+ | -- | the peer acknowledged our version+ GotVerAck+ deriving stock (Eq, Show)++-- | What the peer manager must do after a transition.+data HandshakeAction+ = -- | acknowledge the version of the peer with a verack+ SendVerAck+ | -- | publish that the peer is online+ AnnounceConnected+ deriving stock (Eq, Show)++-- | Reasons to disconnect a peer during the handshake.+data HandshakeError+ = -- | the peer sent a second version message+ DuplicateVersion+ | -- | the peer sent a second verack+ DuplicateVerAck+ | -- | the peer does not serve the full block chain+ NotFullNode+ | -- | the version carries one of our own nonces: we connected to ourselves+ ConnectedToSelf+ deriving stock (Eq, Show)++-- | Apply a message to the handshake. Returns the new state and the actions to+-- carry out, or why the peer must be disconnected.+--+-- Each message may arrive once. A second version or verack is a protocol+-- violation: well-behaved peers never send one, and accepting them would let+-- a peer announce itself as connected twice.+stepHandshake ::+ Handshake ->+ HandshakeEvent ->+ Either HandshakeError (Handshake, [HandshakeAction])+stepHandshake state event =+ case (state, event) of+ (AwaitingBoth, GotVersion v) -> Right (AwaitingVerAck v, [SendVerAck])+ (AwaitingBoth, GotVerAck) -> Right (AwaitingVersion, [])+ (AwaitingVerAck v, GotVerAck) -> Right (Established v, [AnnounceConnected])+ (AwaitingVersion, GotVersion v) ->+ Right (Established v, [SendVerAck, AnnounceConnected])+ (AwaitingVerAck _, GotVersion _) -> Left DuplicateVersion+ (AwaitingVersion, GotVerAck) -> Left DuplicateVerAck+ (Established _, GotVersion _) -> Left DuplicateVersion+ (Established _, GotVerAck) -> Left DuplicateVerAck++-- | The version of the peer, once it has arrived.+handshakeVersion :: Handshake -> Maybe Version+handshakeVersion = \case+ AwaitingVerAck v -> Just v+ Established v -> Just v+ AwaitingBoth -> Nothing+ AwaitingVersion -> Nothing++-- | Whether the handshake is complete.+isEstablished :: Handshake -> Bool+isEstablished = \case+ Established _ -> True+ _ -> False++-- | Whether the peer acknowledged our version.+hasVerAck :: Handshake -> Bool+hasVerAck = \case+ AwaitingVersion -> True+ Established _ -> True+ _ -> False++-- | Check the version of a peer before accepting it. Takes the nonces of the+-- version messages that we sent to all our peers.+--+-- A peer must serve the full chain ('nodeNetwork'), because the node syncs+-- headers and applications download blocks from it. A version carrying one of+-- our nonces means the node connected to itself, which happens when it learns+-- its own address from gossip.+checkVersion :: [Word64] -> Version -> Either HandshakeError Version+checkVersion nonces v+ | v.services .&. nodeNetwork == 0 = Left NotFullNode+ | v.nonce `elem` nonces = Left ConnectedToSelf+ | otherwise = Right v
+ src/Haskoin/Node/PeerMgr/Lifecycle.hs view
@@ -0,0 +1,209 @@+-- |+-- 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
− test/Haskoin/NodeSpec.hs
@@ -1,552 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE NoFieldSelectors #-}--module Haskoin.NodeSpec (spec) where--import Conduit-import Control.Monad (forM_, forever, replicateM)-import Control.Monad.Cont-import Control.Monad.Trans (lift)-import Data.ByteString (ByteString)-import Data.ByteString qualified as B-import Data.ByteString.Base64 (decodeBase64Lenient)-import Data.Default (def)-import Data.Either (fromRight)-import Data.List (find)-import Data.Maybe (isJust, listToMaybe, mapMaybe)-import Data.Serialize (decode, get, put, 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 (..), tupleToHostAddress, tupleToHostAddress6)-import System.Random (randomIO)-import Test.Hspec-import Test.Hspec.QuickCheck-import UnliftIO-import UnliftIO.Concurrent (threadDelay)--data TestNode = TestNode- { testMgr :: PeerMgr,- testChain :: Chain,- nodeEvents :: Inbox NodeEvent,- testDB :: R.DB,- -- | how many getheaders messages the mock peer has received- getHeadersCount :: TVar Int- }---- | 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 ::- React ->- TVar Int ->- Network ->- NetworkAddress ->- SockAddr ->- WithConnection-dummyPeerConnect react count net ad sa f = do- r <- newInbox- s <- newInbox- let s' = inboxToMailbox s- withAsync (go r s') $ \_ -> do- let o = awaitForever (`send` r)- i = forever (receive s >>= yield)- f (Conduits i o) :: IO ()- where- go :: Inbox ByteString -> Mailbox ByteString -> IO ()- go r s = do- nonce <- randomIO- now <- round <$> getPOSIXTime- let rmt = NetworkAddress 0 (sockToHostAddress sa)- ver = buildVersion net nonce 0 ad rmt now- runPut (putMessage net (MVersion ver)) `send` s- runConduit $- forever (receive r >>= yield)- .| inc- .| iterMC count_getheaders- .| concatMapC react- .| outc- .| awaitForever (`send` s)- outc = mapMC $ return . either id (runPut . putMessage net)- count_getheaders = \case- MGetHeaders _ -> atomically (modifyTVar' count (+ 1))- _ -> return ()- inc =- forever $ do- x <- takeCE 24 .| foldC- y <- case decode x of- Left _ -> error "Dummy peer not decode message header"- Right (MessageHeader _ _ len _) ->- takeCE (fromIntegral len) .| foldC- case runGet (getMessage net) $ x `B.append` y of- Left e ->- error $- "Dummy peer could not decode payload: " <> show e- Right msg -> yield msg--mockPeerReact :: [Block] -> Message -> [Message]-mockPeerReact _ (MPing (Ping n)) = [MPong (Pong n)]-mockPeerReact _ (MVersion _) = [MVerAck]-mockPeerReact blocks (MGetHeaders (GetHeaders _ _hs _)) = [MHeaders (Headers hs')]- where- f b = (b.header, (VarInt . fromIntegral . length) b.txs)- hs' = map f blocks-mockPeerReact blocks (MGetData (GetData ivs)) = mapMaybe f ivs- where- f (InvVector InvBlock h) = MBlock <$> find (l h) blocks- f _ = Nothing- l h b = headerHash b.header == BlockHash h-mockPeerReact _ _ = []--spec :: Spec-spec = do- let net = bchRegTest- describe "reads address/port combinations" $ do- -- IPv4 only: resolving an IPv6 address needs IPv6 on the host, which- -- containers and CI runners often lack. The IPv6 forms are still parsed- -- by the toHostService tests below, which need no resolver.- prop "reads arbitrary IPv4 addresses" $ \(e, w) -> do- let p = toEnum (e `mod` 65536)- a = SockAddrInet p w- s <- toSockAddr net (show a)- s `shouldStartWith` [a]- it "reads some specific addresses" $ do- toHostService "localhost" `shouldBe` (Just "localhost", Nothing)- toHostService "::1" `shouldBe` (Just "::1", Nothing)- toHostService "localhost:8080" `shouldBe` (Just "localhost", Just "8080")- toHostService "example.com" `shouldBe` (Just "example.com", Nothing)- toHostService "api.example.com:443" `shouldBe` (Just "api.example.com", Just "443")- toHostService "api.example.com:http" `shouldBe` (Just "api.example.com", Just "http")- toHostService "[::1]" `shouldBe` (Just "::1", Nothing)- toHostService "[::1]:8080" `shouldBe` (Just "::1", Just "8080")- toHostService "[2002::dead:beef]:ssh" `shouldBe` (Just "2002::dead:beef", Just "ssh")- describe "peer manager on test network" $ do- it "connects to a peer" $- withTestNode net "connect-one-peer" $ \TestNode {..} -> do- p <- waitForPeer nodeEvents- Just OnlinePeer {version = Just Version {version = ver}} <-- getOnlinePeer testMgr p- ver `shouldSatisfy` (>= 70002)- it "downloads some blocks" $- withTestNode net "get-blocks" $ \TestNode {..} -> do- let h1 =- "3094ed3592a06f3d8e099eed2d9c1192329944f5df4a48acb29e08f12cfbb660"- h2 =- "0c89955fc5c9f98ecc71954f167b938138c90c6a094c4737f2e901669d26763f"- p <- waitForPeer nodeEvents- pbs <- getBlocks net 10 p [h1, h2]- pbs `shouldSatisfy` isJust- let Just [b1, b2] = pbs- headerHash b1.header `shouldBe` h1- headerHash b2.header `shouldBe` h2- let ths b = map txHash b.txs- testMerkle b = buildMerkleRoot (ths b) `shouldBe` Right b.header.merkle- testMerkle b1- testMerkle b2- it "downloads a block larger than 2 MiB" $ do- -- On Bitcoin Cash a block message may be up to twice the block size,- -- while every other message is limited to 2 MiB (2097152 bytes).- let script = B.replicate 3000000 0x6a- tx = Tx 1 [TxIn nullOutPoint B.empty maxBound] [TxOut 0 script] [] 0- firstBlock : _ = allBlocks- big = Block firstBlock.header [tx]- withTestNodeBlocks [big] net "big-block" $ \TestNode {..} -> do- p <- waitForPeer nodeEvents- pbs <- getBlocks net 10 p [headerHash big.header]- let scriptSizes =- [[B.length o.script | o <- t.outputs] | b <- concat pbs, t <- b.txs]- scriptSizes `shouldBe` [[3000000]]- it "builds a version message with the given height and time" $ do- let local = NetworkAddress nodeNetwork (sockToHostAddress (SockAddrInet 0 0))- remote = NetworkAddress 0 (sockToHostAddress (SockAddrInet 8333 0x08080808))- v = buildVersion net 7 650000 local remote 1700000000- v.version `shouldBe` 70012- v.startHeight `shouldBe` 650000- v.timestamp `shouldBe` 1700000000- 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 announces a tx message larger than 2 MiB" $ do- -- Only the header is sent, with a length of 2 MiB plus one byte:- -- 2 × 1024 × 1024 + 1 = 2097153. The node must refuse it there,- -- without waiting for the payload.- let oversized =- runPut . put $- MessageHeader net.magic MCTx 2097153 (checkSum32 B.empty)- withTestNodeOpts- (defOpts allBlocks) {react = reactAfterVerAck [Left oversized]}- net- "oversized-tx"- $ \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- -- peer does when the node already has its whole chain. The node must- -- take that as the end of the sync instead of asking again.- withTestNodeBlocks [] net "no-headers" $ \TestNode {..} -> do- _ <- waitForPeer nodeEvents- atomically $ readTVar getHeadersCount >>= checkSTM . (>= 1)- threadDelay (1000 * 1000)- readTVarIO getHeadersCount `shouldReturn` 1- it "reports the headers as synced when the best header has the minimum chain work" $ do- -- Every regtest header has a work of 2. Genesis and one header on top- -- of it have 2 + 2 = 4, which is the minimum here.- fresh <- freshHeader net- let net4 = net {minimumChainWork = 4}- withTestNodeBlocks [Block fresh []] net4 "min-work-reached" $- \TestNode {..} -> do- receiveMatch nodeEvents $ \case- ChainEvent (ChainBestBlock bn) | bn.height == 1 -> Just ()- _ -> Nothing- chainSync testChain- chainIsSynced testChain `shouldReturn` True- it "does not report the headers as synced below the minimum chain work" $ do- -- Same chain with a work of 2 + 2 = 4, one short of a minimum of 5.- fresh <- freshHeader net- let net5 = net {minimumChainWork = 5}- withTestNodeBlocks [Block fresh []] net5 "min-work-missing" $- \TestNode {..} -> do- receiveMatch nodeEvents $ \case- ChainEvent (ChainBestBlock bn) | bn.height == 1 -> Just ()- _ -> Nothing- chainSync testChain- chainIsSynced testChain `shouldReturn` False- it "syncs some headers" $- withTestNode net "connect-sync" $ \TestNode {..} -> do- let bh =- "3bfa0c6da615fc45aa44ddea6854ac19d16f3ca167e0e21ac2cc262a49c9b002"- ah =- "7dc835a78a55fa76f9184dc4f6663a73e418c7afec789c5ae25e432fd7fc8467"- bn <-- receiveMatch nodeEvents $ \case- ChainEvent (ChainBestBlock bn)- | bn.height > 0 -> Just bn- _ -> Nothing- bb <- chainGetBest testChain- bb.height `shouldSatisfy` (== 15)- an <-- maybe (error "No ancestor found") return- =<< chainGetAncestor testChain 10 bn- headerHash bn.header `shouldBe` bh- headerHash an.header `shouldBe` ah- it "downloads some block parents" $- withTestNode net "parents" $ \TestNode {..} -> do- let hs =- [ "52e886df7b166d961ac2d3d2d561d806325d51a609dc0a5d9d5fcb65d47906d7",- "2537a081b9e2b24d217fac2886f387758cb3aa4e4956b3be7ed229bafbb71b0f",- "7c72f306215a296f9714320a497b1f2cb5f9b99f162d7e04333c243fac9a54d8"- ]- [_, bn] <-- replicateM 2 $- receiveMatch nodeEvents $ \case- ChainEvent (ChainBestBlock bn) -> Just bn- _ -> Nothing- bn.height `shouldBe` 15- ps <- chainGetParents testChain 12 bn- length ps `shouldBe` 3- 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)---- | Valid header on top of genesis that is dated now. The node only counts--- its headers as synced when the best one is less than two hours old, and--- the headers of the other fixtures are years old.-freshHeader :: Network -> IO BlockHeader-freshHeader net = do- now <- round <$> getPOSIXTime- let genesis = net.genesisHeader- return $- mineBlock- net- 0- genesis {version = 4, prev = headerHash genesis, timestamp = now}--waitForPeer :: (MonadIO m) => Inbox NodeEvent -> m Peer-waitForPeer inbox =- receiveMatch inbox $ \case- PeerEvent (PeerConnected p) -> Just p- _ -> Nothing--withLifted :: (MonadUnliftIO m) => ((a -> IO b) -> IO b) -> (a -> m b) -> m b-withLifted w m = withRunInIO $ \io -> w (io . m)--withTestNode ::- (MonadUnliftIO m) =>- Network -> String -> (TestNode -> m a) -> m a-withTestNode = withTestNodeBlocks allBlocks---- | Like 'withTestNode', with the mock peer serving these blocks.-withTestNodeBlocks ::- (MonadUnliftIO m) =>- [Block] -> Network -> String -> (TestNode -> m a) -> m a-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- (sockToHostAddress (SockAddrInet 0 0))- na =- NetworkAddress- 0- (sockToHostAddress (SockAddrInet 0 0))- cfg' =- NodeConfig- { maxPeers = 20,- db = db,- cf = listToMaybe (R.columnFamilies db),- -- IPv4, so that the mock peer is reachable without IPv6- peers = ["127.0.0.1:17486"],- discover = False,- address = na,- net = net,- pub = pub,- timeout = opts.timeout,- maxPeerLife = 48 * 3600,- connect = dummyPeerConnect opts.react count net ad,- logger = Logger {level = LevelError, source = "", push = \_ -> return ()}- }- Node mgr ch <- ContT $ withNode cfg'- lift $- f- TestNode- { testMgr = mgr,- testChain = ch,- nodeEvents = sub,- testDB = db,- getHeadersCount = count- }- where- cfg = def {R.createIfMissing = True, R.errorIfExists = True}- cols = [("node", def)]--allBlocks :: [Block]-allBlocks =- fromRight (error "Could not decode blocks") $- runGet f (decodeBase64Lenient allBlocksBase64)- where- f = mapM (const get) [(1 :: Int) .. 15]--allBlocksBase64 :: ByteString-allBlocksBase64 =- "AAAAIAYibkYRGgtZyq8SYEPrW78ow086XjMqH8eytzzxiJEPakRJalmWTFwdvzNuH8fHLZEjn+4N\- \FNMANdB7ez2M4a3TFbNe//9/IAMAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\- \AAAAAP////8MUQEBCC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf\- \1OHjgkqxnwEYkK9zrAAAAAAAAAAge0RDjOrqVayGUoQsbNTJcTXUM+psaHpmuiFy6hwo2T8yn0CL\- \7WDJw9hxl1kf5c4JySq3WJF8OPsoguzF7mXH3tQVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAA\- \AAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wxSAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOym\- \QKMx7MqzjsE+lp+i7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACCKlhzDaFkrsmO2FhmeQS9ONS8D\- \QsU4H97yNxVhyIXYJuG3a9cyQpdeETjCQ6JybgkwI0OOfa4eYazf7WWI5UAk1BWzXv//fyAEAAAA\- \AQIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFMBAQgvRUIzMi4wL///\- \//8BAPIFKgEAAAAjIQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAIP/S\- \XiIJZqvUyBY90z72dv6+/GG50R3vc3UAK8AHP89wChmkVP6nefjOt+sNyhbKk9zia47F08oTNtC0\- \OG1zyuXVFbNe//9/IAEAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//\- \//8MVAEBCC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqx\- \nwEYkK9zrAAAAAAAAAAgeQtE1s3YV/uS2jUouo3S9DJAVf5OGk+Nyx+No1mPH24b5JCkr/tSP0E/\- \NYVkVcE0ZHxbO/fu5wOd+8VolvPQYtUVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAA\- \AAAAAAAAAAAAAAAAAAAA/////wxVAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7Mqz\- \jsE+lp+i7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACBgtvss8QiesqxISt/1RJkykhGcLe2eCY49\- \b6CSNe2UMOVYGZ++uRCKvaJ2+jo7akr7XsdXCYSAmuw6DwSO8lvF1RWzXv//fyAAAAAAAQIAAAAB\- \AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFYBAQgvRUIzMi4wL/////8BAPIF\- \KgEAAAAjIQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAID92Jp1mAeny\- \N0dMCWoMyTiBk3sWT5VxzI75ycVflYkMCnXLFhuwrMdBbZmXJinAJBUpN7BV0XvlM2PRmb7HQebV\- \FbNe//9/IAEAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MVwEB\- \CC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9z\- \rAAAAAAAAAAgxEgEkhjf5p+ql8dETmdSCdCdk+vB26+V2SGLEuE1+kA1acGCdQoQBqec8P/knItJ\- \M213OIrDX6U5IB6fgIas7dYVs17//38gAQAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\- \AAAAAAAAAAAA/////wxYAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i\- \7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACDku4EB5X7htWpHg+aMzzW1AABttpNQTew7K3Aj2fh/\- \OuOCPhJApmcXq5o42tkksFSuhYvcfqaSHCuuFgjo6ohz1hWzXv//fyAAAAAAAQIAAAABAAAAAAAA\- \AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFkBAQgvRUIzMi4wL/////8BAPIFKgEAAAAj\- \IQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAIKWpAhOWbkEN9vWf1uCu\- \eXtVOZIE9V1OE87iC+H9atBRtY4LPgaWUSVMNh9SeZK1NViIFMklbjsfqYiC4eA/VuLWFbNe//9/\- \IAAAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MWgEBCC9FQjMy\- \LjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9zrAAAAAAA\- \AAAgZ4T81y9DXuJanHjsr8cY5HM6ZvbETRj5dvpViqc1yH0oN9OOruaO5mjdITJwweVCzjSQ5Wsl\- \vSOKaKvEX5j9l9YVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\- \AAAA/////wxbAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i7WOOwd/U\- \4eOCSrGfARiQr3OsAAAAAAAAACCV3J2A3qneSJ7Q/RuF8OPd8O1izIXvKElR/xg/+InGNEafu0Ul\- \3VYJR93zbAQuns9hUfAhA8MTBPk8bbDabDfo1hWzXv//fyAAAAAAAQIAAAABAAAAAAAAAAAAAAAA\- \AAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFwBAQgvRUIzMi4wL/////8BAPIFKgEAAAAjIQME7KZA\- \ozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAINcGedRly1+dXQrcCaZRXTIG2GHV\- \0tPCGpZtFnvfhuhSx8d3Azdv/MXRJgsb56qqmD5gsXiWUdi7ia7wsBZVylvWFbNe//9/IAEAAAAB\- \AgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MXQEBCC9FQjMyLjAv////\- \/wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9zrAAAAAAAAAAgDxu3\- \+7op0n6+s1ZJTqqzjHWH84YorH8hTbLiuYGgNyWIkhaj0zR7Vc+fSRm4UYUaPsefRhq3fUt8glyS\- \D8P/5tcVs17//38gAwAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////\- \/wxeAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i7WOOwd/U4eOCSrGf\- \ARiQr3OsAAAAAAAAACDYVJqsPyQ8MwR+LRafufm1LB97SQoyFJdvKVohBvNyfD4/FxT2i0rlYQcS\- \TQAvTnehousK2P8T9c0qx4Yj72lT1xWzXv//fyAAAAAAAQIAAAABAAAAAAAAAAAAAAAAAAAAAAAA\- \AAAAAAAAAAAAAAAAAAD/////DF8BAQgvRUIzMi4wL/////8BAPIFKgEAAAAjIQME7KZAozHsyrOO\- \wT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAA"
− test/Spec.hs
@@ -1,1 +0,0 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/common/Haskoin/Node/Test.hs view
@@ -0,0 +1,391 @@+-- | Helpers shared by the unit and regtest test suites.+module Haskoin.Node.Test+ ( -- * Logging+ withTestLogger,++ -- * Timing+ within,+ eventually,+ receiveWithin,+ expectNothingWithin,++ -- * Block chains+ mkBlocks,+ mkHeaders,+ coinbaseTx,+ serveHeaders,+ headersMessage,+ nowTimestamp,++ -- * Database+ withTestDB,+ withDBAt,++ -- * In-memory connections+ Remote (..),+ memoryConnection,+ remoteSend,+ remoteSendRaw,+ remoteReceive,+ remoteReceiveMatch,+ remoteClose,+ remoteVersion,+ respondHonestly,+ honestRemote,+ encodeMessage,++ -- * Nodes+ TestNode (..),+ withTestNode,+ setTimeout,+ setMaxPeerLife,+ waitForPeer,+ waitForDisconnect,+ waitForBest,+ zeroAddress,+ )+where++import Conduit (awaitForever, yield)+import Control.Monad (forM_, unless)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.Default (def)+import Data.List (elemIndex)+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import Data.Serialize (decode, encode, runGet, runPut)+import Data.Time.Clock (NominalDiffTime)+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word32, Word64)+import Database.RocksDB (DB)+import Database.RocksDB qualified as R+import Haskoin+import Haskoin.Node+import Haskoin.Node qualified as NC (NodeConfig (..))+import NQE+import Network.Socket (SockAddr (..))+import System.Environment (lookupEnv)+import System.Random (randomIO)+import Test.HUnit (assertFailure)+import UnliftIO+import UnliftIO.Concurrent (threadDelay)++-- | Run with logging to stderr if HASKOIN_TEST_LOG is set, silently otherwise.+withTestLogger :: (Logger -> IO a) -> IO a+withTestLogger f =+ lookupEnv "HASKOIN_TEST_LOG" >>= \case+ Just _ -> withLogger LevelDebug f+ Nothing -> f Logger {level = LevelError, source = "", push = \_ -> return ()}++-- | Fail the test if the action does not complete in the given seconds.+within :: Double -> String -> IO a -> IO a+within s what act =+ timeout (round (s * 1000000)) act >>= \case+ Nothing -> assertFailure $ "Timed out after " <> show s <> "s: " <> what+ Just x -> return x++-- | Fail the test unless the condition becomes true in the given seconds.+eventually :: Double -> String -> IO Bool -> IO ()+eventually s what cond = within s what go+ where+ go = cond >>= \b -> unless b (threadDelay 20000 >> go)++-- | Receive the first matching message in the given seconds.+receiveWithin ::+ (InChan mbox) =>+ Double ->+ String ->+ mbox msg ->+ (msg -> Maybe a) ->+ IO a+receiveWithin s what mbox f = within s what (receiveMatch mbox f)++-- | Fail the test if a matching message arrives in the given seconds.+expectNothingWithin ::+ (InChan mbox) =>+ Double ->+ String ->+ mbox msg ->+ (msg -> Maybe a) ->+ IO ()+expectNothingWithin s what mbox f =+ timeout (round (s * 1000000)) (receiveMatch mbox f) >>= \case+ Nothing -> return ()+ Just _ -> assertFailure $ "Unexpected: " <> what++-- | Current time as a block timestamp.+nowTimestamp :: IO Word32+nowTimestamp = floor <$> getPOSIXTime++-- | Coinbase-like transaction that makes each block unique.+coinbaseTx :: Word32 -> Int -> Tx+coinbaseTx tag i =+ Tx+ { version = 1,+ inputs =+ [ TxIn+ { outpoint = OutPoint {hash = z, index = maxBound},+ script = encode (tag, fromIntegral i :: Word64),+ sequence = maxBound+ }+ ],+ outputs = [TxOut {value = 5000000000, script = B.singleton 0x51}],+ witness = [],+ locktime = 0+ }+ where+ z = "0000000000000000000000000000000000000000000000000000000000000000"++-- | Mine blocks on top of a parent header. Timestamps start at the given+-- time and increase by one second. Use different tags to build forks.+mkBlocks :: Network -> Word32 -> BlockHeader -> Word32 -> Int -> [Block]+mkBlocks net tag parent t0 n = go parent 0+ where+ go p i+ | i >= n = []+ | otherwise =+ let tx = coinbaseTx tag i+ h =+ mineBlock net 0 $+ BlockHeader+ { version = 0x20000000,+ prev = headerHash p,+ merkle = either error id (buildMerkleRoot [txHash tx]),+ timestamp = max (t0 + fromIntegral i) (p.timestamp + 1),+ bits = net.genesisHeader.bits,+ nonce = 0+ }+ in Block {header = h, txs = [tx]} : go h (i + 1)++-- | Headers for 'mkBlocks'.+mkHeaders :: Network -> Word32 -> BlockHeader -> Word32 -> Int -> [BlockHeader]+mkHeaders net tag parent t0 n = map (.header) (mkBlocks net tag parent t0 n)++-- | Answer a header request like a full node would. The chain excludes the+-- genesis block.+serveHeaders :: Network -> [BlockHeader] -> GetHeaders -> [BlockHeader]+serveHeaders net chain gh =+ take 2000 (stop_at (drop start chain))+ where+ hashes = headerHash net.genesisHeader : map headerHash chain+ start =+ fromMaybe 0 $+ listToMaybe [i | l <- gh.locator, Just i <- [elemIndex l hashes]]+ z = "0000000000000000000000000000000000000000000000000000000000000000"+ stop_at hs+ | gh.stop == z = hs+ | otherwise = case break ((== gh.stop) . headerHash) hs of+ (a, b : _) -> a <> [b]+ (a, []) -> a++headersMessage :: [BlockHeader] -> Message+headersMessage hs = MHeaders (Headers [(h, VarInt 0) | h <- hs])++-- | Temporary RocksDB database.+withTestDB :: (DB -> IO a) -> IO a+withTestDB f =+ withSystemTempDirectory "haskoin-node-test-" $ \dir -> withDBAt dir f++-- | RocksDB database at a path, created if missing.+withDBAt :: FilePath -> (DB -> IO a) -> IO a+withDBAt dir = R.withDB dir def {R.createIfMissing = True}++-- | Remote end of an in-memory connection to a local peer process.+data Remote = Remote+ { net :: !Network,+ -- | bytes for the local peer, 'Nothing' closes the connection+ outbox :: !(Mailbox (Maybe ByteString)),+ -- | bytes from the local peer, 'Nothing' when it closed the connection+ inbox :: !(Inbox (Maybe ByteString)),+ buffer :: !(IORef ByteString),+ -- | set when the local peer closes the connection+ closed :: !(TVar Bool)+ }++-- | Connection to a remote peer implemented by the provided script. The+-- connection closes when the script returns.+memoryConnection :: Network -> (Remote -> IO ()) -> WithConnection+memoryConnection net script f = do+ (to_local_in, to_local) <- newMailbox+ (from_local_in, from_local) <- newMailbox+ buf <- newIORef B.empty+ closed <- newTVarIO False+ let remote =+ Remote+ { net = net,+ outbox = to_local,+ inbox = from_local_in,+ buffer = buf,+ closed = closed+ }+ src =+ receive to_local_in >>= \case+ Nothing -> return ()+ Just b -> yield b >> src+ snk = awaitForever $ \b -> Just b `send` from_local+ close_local = do+ atomically (writeTVar closed True)+ Nothing `send` from_local+ withAsync (script remote `finally` (Nothing `send` to_local)) $ \_ ->+ f (Conduits src snk) `finally` close_local++encodeMessage :: Network -> Message -> ByteString+encodeMessage net = runPut . putMessage net++remoteSend :: Remote -> Message -> IO ()+remoteSend r m = Just (encodeMessage r.net m) `send` r.outbox++remoteSendRaw :: Remote -> ByteString -> IO ()+remoteSendRaw r bs = Just bs `send` r.outbox++remoteClose :: Remote -> IO ()+remoteClose r = Nothing `send` r.outbox++-- | Receive next message from the local peer. 'Nothing' if it closed the+-- connection.+remoteReceive :: Remote -> IO (Maybe Message)+remoteReceive r =+ take_bytes 24 >>= \case+ Nothing -> return Nothing+ Just hdr -> do+ len <- case decode hdr of+ Left e -> assertFailure $ "Bad header from local peer: " <> e+ Right (MessageHeader _ _ len _) -> return len+ take_bytes (fromIntegral len) >>= \case+ Nothing -> return Nothing+ Just payload ->+ case runGet (getMessage r.net) (hdr <> payload) of+ Left e -> assertFailure $ "Bad message from local peer: " <> e+ Right m -> return (Just m)+ where+ take_bytes n = do+ b <- readIORef r.buffer+ if B.length b >= n+ then do+ let (x, y) = B.splitAt n b+ writeIORef r.buffer y+ return (Just x)+ else+ receive r.inbox >>= \case+ Nothing -> return Nothing+ Just more -> writeIORef r.buffer (b <> more) >> take_bytes n++-- | Receive messages from the local peer until one matches.+remoteReceiveMatch ::+ Double -> String -> Remote -> (Message -> Maybe a) -> IO a+remoteReceiveMatch s what r f = within s what go+ where+ go =+ remoteReceive r >>= \case+ Nothing -> assertFailure $ "Connection closed awaiting " <> what+ Just m -> maybe go return (f m)++zeroAddress :: Word64 -> NetworkAddress+zeroAddress srv = NetworkAddress srv (sockToHostAddress (SockAddrInet 0 0))++-- | Version message for a remote peer.+remoteVersion :: Network -> Word64 -> Word64 -> IO Version+remoteVersion net services nonce = do+ now <- nowTimestamp+ return $+ buildVersion+ net+ nonce+ 0+ (zeroAddress services)+ (zeroAddress 0)+ (fromIntegral now)++-- | React to a message like a well-behaved full node with the provided+-- blocks (excluding genesis).+respondHonestly :: Remote -> TVar [Block] -> Message -> IO ()+respondHonestly r blocks = \case+ MVersion _ -> remoteSend r MVerAck+ MPing (Ping n) -> remoteSend r (MPong (Pong n))+ MGetHeaders gh -> do+ bs <- readTVarIO blocks+ remoteSend r . headersMessage $ serveHeaders r.net (map (.header) bs) gh+ MGetData (GetData ivs) -> do+ bs <- readTVarIO blocks+ let find_block (InvVector t h)+ | t `elem` [InvBlock, InvWitnessBlock] =+ listToMaybe [b | b <- bs, (headerHash b.header).get == h]+ | otherwise = Nothing+ found = mapMaybe (\iv -> (,) iv <$> find_block iv) ivs+ missing = [iv | iv <- ivs, iv `notElem` map fst found]+ forM_ found $ \(_, b) -> remoteSend r (MBlock b)+ unless (null missing) $ remoteSend r (MNotFound (NotFound missing))+ _ -> return ()++-- | Remote that completes the handshake and serves the provided blocks.+honestRemote :: TVar [Block] -> Remote -> IO ()+honestRemote blocks r = do+ v <- remoteVersion r.net nodeNetwork =<< randomIO+ remoteSend r (MVersion v)+ let go =+ remoteReceive r >>= \case+ Nothing -> return ()+ Just m -> respondHonestly r blocks m >> go+ go++data TestNode = TestNode+ { node :: !Node,+ events :: !(Inbox NodeEvent),+ net :: !Network+ }++-- | Run a node against the provided peers using a temporary database. The+-- configuration can be adjusted before the node starts.+withTestNode ::+ Network ->+ [String] ->+ (SockAddr -> WithConnection) ->+ (NodeConfig -> NodeConfig) ->+ (TestNode -> IO a) ->+ IO a+withTestNode net peers connect modify f =+ withTestDB $ \db ->+ withTestLogger $ \logger ->+ withPublisher $ \pub ->+ withSubscription pub $ \sub -> do+ let cfg =+ NodeConfig+ { maxPeers = 20,+ db = db,+ cf = Nothing,+ peers = peers,+ discover = False,+ address = zeroAddress 0,+ net = net,+ pub = pub,+ timeout = 120,+ maxPeerLife = 48 * 3600,+ connect = connect,+ logger = logger+ }+ withNode (modify cfg) $ \n ->+ f TestNode {node = n, events = sub, net = net}++setTimeout :: NominalDiffTime -> NodeConfig -> NodeConfig+setTimeout t c = c {NC.timeout = t}++setMaxPeerLife :: NominalDiffTime -> NodeConfig -> NodeConfig+setMaxPeerLife t c = c {NC.maxPeerLife = t}++waitForPeer :: TestNode -> IO Peer+waitForPeer t =+ receiveWithin 30 "peer connection" t.events $ \case+ PeerEvent (PeerConnected p) -> Just p+ _ -> Nothing++waitForDisconnect :: TestNode -> Peer -> IO ()+waitForDisconnect t p =+ receiveWithin 30 ("disconnection of peer " <> show p) t.events $ \case+ PeerEvent (PeerDisconnected p') | p == p' -> Just ()+ _ -> Nothing++-- | Wait until the best block header satisfies the condition.+waitForBest :: Double -> TestNode -> (BlockNode -> Bool) -> IO BlockNode+waitForBest s t f = do+ eventually s "best block header" $ f <$> chainGetBest t.node.chain+ chainGetBest t.node.chain
+ test/regtest/Containerfile view
@@ -0,0 +1,49 @@+# Regtest image with official Bitcoin Core and Bitcoin Cash Node binaries.+#+# Build with:+# podman build -t haskoin-node-regtest regtest+#+# Hashes come from the official SHA256SUMS files published by each project:+# https://bitcoincore.org/bin/bitcoin-core-32.1/SHA256SUMS+# https://gitlab.com/bitcoin-cash-node/announcements/-/tree/master/release-sigs/29.2.0+FROM docker.io/library/ubuntu:26.04++ARG TARGETARCH+ARG CORE_VERSION=32.1+ARG CORE_SHA256_amd64=61545c17c3120fb7412f896d248b11993af04ec8817aad3649c55b2afb044963+ARG CORE_SHA256_arm64=7ed37c7373eb6aa3e3f72842596a1afeba1b223717033cade9842b25773e1922+ARG BCHN_VERSION=29.2.0+ARG BCHN_SHA256_amd64=23bbe9ff749136a9b832a2a59023ce76a5391322c1ea2f4f24b5b91f035081bc+ARG BCHN_SHA256_arm64=27e177e5a07dc78c2c0d18798ea2e3925111955509ecdc476e828e7ce8f46383++RUN apt-get update \+ && apt-get install -y --no-install-recommends ca-certificates curl libatomic1 \+ && rm -rf /var/lib/apt/lists/*++RUN set -eu; \+ arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \+ case "$arch" in \+ amd64) triple=x86_64-linux-gnu; core_sha="$CORE_SHA256_amd64"; bchn_sha="$BCHN_SHA256_amd64" ;; \+ arm64) triple=aarch64-linux-gnu; core_sha="$CORE_SHA256_arm64"; bchn_sha="$BCHN_SHA256_arm64" ;; \+ *) echo "unsupported architecture: $arch" >&2; exit 1 ;; \+ esac; \+ cd /tmp; \+ core="bitcoin-${CORE_VERSION}-${triple}.tar.gz"; \+ curl -fsSLO "https://bitcoincore.org/bin/bitcoin-core-${CORE_VERSION}/${core}"; \+ echo "${core_sha} ${core}" | sha256sum -c -; \+ mkdir -p /opt/bitcoin-core; \+ tar -xzf "$core" -C /opt/bitcoin-core --strip-components=1; \+ bchn="bitcoin-cash-node-${BCHN_VERSION}-${triple}.tar.gz"; \+ curl -fsSLO "https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v${BCHN_VERSION}/${bchn}"; \+ echo "${bchn_sha} ${bchn}" | sha256sum -c -; \+ mkdir -p /opt/bchn; \+ tar -xzf "$bchn" -C /opt/bchn --strip-components=1; \+ rm -f /tmp/*.tar.gz; \+ /opt/bitcoin-core/bin/bitcoind -version | head -1; \+ /opt/bchn/bin/bitcoind -version | head -1++# Peer-to-peer ports for up to three nodes per container.+EXPOSE 18444 18445 18446++# Nodes are started with "podman exec" by the test suite.+CMD ["sleep", "infinity"]
+ test/regtest/Main.hs view
@@ -0,0 +1,196 @@+-- | Test the library against Bitcoin Core and Bitcoin Cash Node in regtest+-- mode. Nodes run in Podman containers built from the Containerfile in this+-- directory. Set HASKOIN_NODE_SKIP_REGTEST to skip.+module Main (main) where++import Control.Monad (forM_, replicateM_, void)+import Data.List (isInfixOf)+import Data.Maybe (isJust)+import Data.String (fromString)+import Haskoin+import Haskoin.Node+import Haskoin.Node.Test+import Regtest.Podman+import System.Environment (lookupEnv)+import Test.Hspec++main :: IO ()+main = do+ skip <- isJust <$> lookupEnv "HASKOIN_NODE_SKIP_REGTEST"+ available <- podmanAvailable+ if+ | skip -> hspec $ it "regtest" $ pendingWith "HASKOIN_NODE_SKIP_REGTEST set"+ | not available -> hspec $ it "regtest" $ pendingWith "podman not available"+ | otherwise -> buildImage >> hspec spec++-- | Node connected to the given nodes in the container. The timeout is long+-- so that the stall recovery poll does not hide announcement handling bugs.+withRegtestNode :: Container -> [Int] -> (TestNode -> IO a) -> IO a+withRegtestNode c is f = do+ addrs <- mapM (hostAddress c) is+ withTestNode+ (implNet c.impl)+ addrs+ withConnection+ (setTimeout 600)+ f++-- | Wait until our best header is the tip of node number @i@.+waitForTip :: Container -> Int -> TestNode -> IO BlockNode+waitForTip c i t = do+ tip <- fromString <$> cli c i ["getbestblockhash"]+ waitForBest 60 t ((== tip) . headerHash . (.header))++blockHashAt :: Container -> Int -> Int -> IO BlockHash+blockHashAt c i h = fromString <$> cli c i ["getblockhash", show h]++-- | Node 0 with blocks mined so that it leaves initial block download.+withMinedNode :: Impl -> Int -> (Container -> IO a) -> IO a+withMinedNode impl n f =+ withContainer impl $ \c -> do+ startNode c 0 []+ _ <- mine c 0 n+ f c++spec :: Spec+spec = forM_ [BitcoinCore, BitcoinCashNode] $ \impl ->+ describe (implName impl) $ do+ let net = implNet impl++ it "completes the handshake" $ withMinedNode impl 1 $ \c ->+ withRegtestNode c [0] $ \t -> do+ p <- waitForPeer t+ Just o <- getOnlinePeer t.node.peerMgr p+ Just v <- return o.version+ v.version `shouldSatisfy` (>= 70012)+ let VarString ua = v.userAgent+ expected = case impl of+ BitcoinCore -> "Satoshi"+ BitcoinCashNode -> "Bitcoin Cash Node"+ show ua `shouldSatisfy` (expected `isInfixOf`)++ it "syncs a chain longer than one header batch" $+ withMinedNode impl 2100 $ \c ->+ withRegtestNode c [0] $ \t -> do+ bn <- receiveWithin 120 "synced" t.events $ \case+ ChainEvent (ChainSynced bn) -> Just bn+ _ -> Nothing+ bn.height `shouldBe` 2100+ void $ waitForTip c 0 t++ it "follows blocks mined one at a time" $ withMinedNode impl 1 $ \c ->+ withRegtestNode c [0] $ \t -> do+ _ <- waitForTip c 0 t+ replicateM_ 5 $ mine c 0 1 >> waitForTip c 0 t++ it "follows bursts of blocks without stalling" $+ withMinedNode impl 1 $ \c ->+ withRegtestNode c [0] $ \t -> do+ _ <- waitForTip c 0 t+ replicateM_ 3 $ mine c 0 99 >> waitForTip c 0 t++ it "follows a burst longer than a header batch" $+ withMinedNode impl 1 $ \c ->+ withRegtestNode c [0] $ \t -> do+ _ <- waitForTip c 0 t+ _ <- mine c 0 2500+ bn <- waitForTip c 0 t+ bn.height `shouldBe` 2501++ it "follows a reorganization" $ withMinedNode impl 10 $ \c ->+ withRegtestNode c [0] $ \t -> do+ old <- waitForTip c 0 t+ h8 <- cli c 0 ["getblockhash", "8"]+ void $ cli c 0 ["invalidateblock", h8]+ _ <- mineTo c 0 5 otherAddress+ new <- waitForTip c 0 t+ new.height `shouldBe` 12+ chainBlockMain t.node.chain (headerHash old.header) `shouldReturn` False+ split <- chainGetSplitBlock t.node.chain old new+ split.height `shouldBe` 7++ it "follows a reorganization found in a burst" $+ withMinedNode impl 120 $ \c ->+ withRegtestNode c [0] $ \t -> do+ _ <- waitForTip c 0 t+ h <- cli c 0 ["getblockhash", "70"]+ void $ cli c 0 ["invalidateblock", h]+ _ <- mineTo c 0 99 otherAddress+ new <- waitForTip c 0 t+ new.height `shouldBe` 168++ it "downloads blocks" $ withMinedNode impl 5 $ \c ->+ withRegtestNode c [0] $ \t -> do+ p <- waitForPeer t+ _ <- waitForTip c 0 t+ hs <- mapM (blockHashAt c 0) [1 .. 5]+ Just bs <- getBlocks net 10 p hs+ map (headerHash . (.header)) bs `shouldBe` hs+ forM_ bs $ \b ->+ buildMerkleRoot (map txHash b.txs) `shouldBe` Right b.header.merkle+ let unknown = headerHash (mkHeaders net 9 net.genesisHeader 0 1 !! 0)+ getBlocks net 10 p [unknown] `shouldReturn` Nothing++ it "downloads announced transactions" $ withContainer impl $ \c -> do+ startNode c 0 []+ -- Bitcoin Cash Node also loads a default wallet, so name ours+ let wallet args = cli c 0 ("-rpcwallet=w" : args)+ void $ cli c 0 ["createwallet", "w"]+ addr <- wallet ["getnewaddress"]+ void $ mineTo c 0 101 addr+ withRegtestNode c [0] $ \t -> do+ p <- waitForPeer t+ _ <- waitForTip c 0 t+ txid <- fromString <$> wallet ["sendtoaddress", mineAddress, "1"]+ receiveWithin 60 "transaction announcement" t.events $ \case+ PeerEvent (PeerMessage p' (MInv (Inv ivs)))+ | p == p' && any ((== txid) . TxHash . (.invHash)) ivs -> Just ()+ _ -> Nothing+ Just [tx] <- getTxs net 10 p [txid]+ txHash tx `shouldBe` txid++ it "pings a peer" $ withMinedNode impl 1 $ \c ->+ withRegtestNode c [0] $ \t -> do+ p <- waitForPeer t+ pingPeer 10 p `shouldReturn` True++ it "reconnects to a restarted peer and catches up" $+ withMinedNode impl 5 $ \c ->+ withRegtestNode c [0] $ \t -> do+ p <- waitForPeer t+ _ <- waitForTip c 0 t+ stopNode c 0+ waitForDisconnect t p+ startNode c 0 []+ _ <- mine c 0 3+ _ <- waitForPeer t+ bn <- waitForTip c 0 t+ bn.height `shouldBe` 8++ it "keeps following the chain when a peer goes away" $+ withMinedNode impl 5 $ \c -> do+ startNode c 1 ["-addnode=" <> nodeAddress c 0]+ eventually 60 "nodes in sync" $+ (== "5") <$> cli c 1 ["getblockcount"]+ withRegtestNode c [0, 1] $ \t -> do+ _ <- waitForPeer t+ _ <- waitForPeer t+ _ <- waitForTip c 0 t+ stopNode c 0+ _ <- mine c 1 5+ bn <- waitForTip c 1 t+ bn.height `shouldBe` 10++ it "follows bursts announced by several peers" $+ withMinedNode impl 1 $ \c -> do+ startNode c 1 ["-addnode=" <> nodeAddress c 0]+ withRegtestNode c [0, 1] $ \t -> do+ _ <- waitForPeer t+ _ <- waitForPeer t+ _ <- waitForTip c 0 t+ replicateM_ 3 $ mine c 0 99 >> waitForTip c 0 t+ eventually 60 "nodes in sync" $+ (== "298") <$> cli c 1 ["getblockcount"]+ expectNothingWithin 1 "peer disconnection" t.events $ \case+ PeerEvent (PeerDisconnected _) -> Just ()+ _ -> Nothing
+ test/regtest/Regtest/Podman.hs view
@@ -0,0 +1,231 @@+-- | Bitcoin Core and Bitcoin Cash Node regtest nodes in Podman containers.+module Regtest.Podman+ ( Impl (..),+ implName,+ implNet,+ Container (..),+ podmanAvailable,+ buildImage,+ withContainer,+ startNode,+ stopNode,+ cli,+ hostAddress,+ nodeAddress,+ mine,+ mineTo,+ mineAddress,+ otherAddress,+ )+where++import Control.Exception (bracket)+import Control.Monad (unless, void)+import Data.Char (isHexDigit, isSpace)+import Data.List (dropWhileEnd)+import Haskoin (Network, bchRegTest, btcRegTest)+import System.Directory (findExecutable)+import System.Exit (ExitCode (..))+import System.Process (readProcessWithExitCode)+import System.Random (randomRIO)+import UnliftIO (timeout)+import UnliftIO.Concurrent (threadDelay)++data Impl = BitcoinCore | BitcoinCashNode+ deriving stock (Eq, Show)++implName :: Impl -> String+implName BitcoinCore = "Bitcoin Core"+implName BitcoinCashNode = "Bitcoin Cash Node"++implNet :: Impl -> Network+implNet BitcoinCore = btcRegTest+implNet BitcoinCashNode = bchRegTest++implDir :: Impl -> FilePath+implDir BitcoinCore = "/opt/bitcoin-core/bin/"+implDir BitcoinCashNode = "/opt/bchn/bin/"++-- | Running container with ports for up to three nodes.+data Container = Container+ { cid :: !String,+ impl :: !Impl,+ -- | peer-to-peer port of each node inside the container+ p2pPorts :: ![Int],+ -- | RPC port of each node inside the container+ rpcPorts :: ![Int]+ }++-- | Number of nodes that can run in a container.+maxNodes :: Int+maxNodes = 3++image :: String+image = "localhost/haskoin-node-regtest:latest"++-- | Directory with the Containerfile, relative to the package root.+imageDir :: FilePath+imageDir = "test/regtest"++-- | Run podman and return its standard output. Throws on failure or if it+-- takes longer than two minutes.+podman :: [String] -> IO String+podman = podmanWithin 120++-- | Run podman with a timeout in seconds.+podmanWithin :: Int -> [String] -> IO String+podmanWithin secs args =+ timeout (secs * 1000000) (readProcessWithExitCode "podman" args "") >>= \case+ Nothing ->+ fail $+ "podman " <> unwords args <> " timed out after " <> show secs <> "s"+ Just (ExitSuccess, out, _) -> return (dropWhileEnd isSpace out)+ Just (ExitFailure c, out, err) ->+ fail $+ "podman "+ <> unwords args+ <> " exited with "+ <> show c+ <> ": "+ <> out+ <> err++podmanAvailable :: IO Bool+podmanAvailable =+ findExecutable "podman" >>= \case+ Nothing -> return False+ Just _ -> do+ (c, _, _) <- readProcessWithExitCode "podman" ["info"] ""+ return (c == ExitSuccess)++-- | Build the image. Cached layers make this quick after the first time.+buildImage :: IO ()+buildImage = void $ podmanWithin 1800 ["build", "--quiet", "--tag", image, imageDir]++-- | Run a container. Node ports inside it are random, and are published to+-- random ports on the loopback interface of the host.+withContainer :: Impl -> (Container -> IO a) -> IO a+withContainer impl = bracket start stop+ where+ start = do+ ports <- randomPorts (2 * maxNodes)+ let (p2p, rpc) = splitAt maxNodes ports+ cid <-+ podman $+ ["run", "--detach", "--rm"]+ <> concat [["--publish", "127.0.0.1::" <> show p] | p <- p2p]+ <> [image]+ return Container {cid = cid, impl = impl, p2pPorts = p2p, rpcPorts = rpc}+ stop :: Container -> IO ()+ stop c = void $ podman ["rm", "--force", "--time", "0", c.cid]++-- | Distinct random ports. They are at least two apart because nodes may+-- also listen on the next port up for Tor.+randomPorts :: Int -> IO [Int]+randomPorts n = go []+ where+ go ps+ | length ps == n = return (reverse ps)+ | otherwise = do+ p <- randomRIO (20000, 60000)+ if any (\q -> abs (p - q) < 2) ps then go ps else go (p : ps)++p2pPort :: Container -> Int -> Int+p2pPort c i = c.p2pPorts !! i++rpcPort :: Container -> Int -> Int+rpcPort c i = c.rpcPorts !! i++dataDir :: Int -> FilePath+dataDir i = "/data/node" <> show i++exec :: Container -> [String] -> IO String+exec c args = podman (["exec", c.cid] <> args)++-- | Like 'exec' with a timeout in seconds.+execWithin :: Int -> Container -> [String] -> IO String+execWithin secs c args = podmanWithin secs (["exec", c.cid] <> args)++-- | Start node number @i@ in the container with extra arguments. Data+-- persists across restarts.+startNode :: Container -> Int -> [String] -> IO ()+startNode c i extra = do+ void $ exec c ["mkdir", "-p", dataDir i]+ void . exec c $+ [ implDir c.impl <> "bitcoind",+ "-regtest",+ "-daemon",+ "-datadir=" <> dataDir i,+ "-port=" <> show (p2pPort c i),+ "-bind=0.0.0.0:" <> show (p2pPort c i),+ "-rpcport=" <> show (rpcPort c i),+ "-listen=1",+ "-listenonion=0",+ "-fallbackfee=0.0002",+ "-debug=net"+ ]+ <> extra+ void $ cli' 60 c i ["-rpcwait", "getblockcount"]++-- | Stop node number @i@ and wait until it exits.+stopNode :: Container -> Int -> IO ()+stopNode c i = do+ void $ cli c i ["stop"]+ let pidfile = dataDir i <> "/regtest/bitcoind.pid"+ wait n = do+ (code, _, _) <-+ readProcessWithExitCode "podman" ["exec", c.cid, "test", "-e", pidfile] ""+ unless (code /= ExitSuccess || n > (300 :: Int)) $+ threadDelay 100000 >> wait (n + 1)+ wait 0+ threadDelay 500000++cli' :: Int -> Container -> Int -> [String] -> IO String+cli' secs c i args =+ execWithin secs c $+ [ implDir c.impl <> "bitcoin-cli",+ "-regtest",+ "-datadir=" <> dataDir i,+ "-rpcport=" <> show (rpcPort c i)+ ]+ <> args++-- | Call RPC method on node number @i@.+cli :: Container -> Int -> [String] -> IO String+cli = cli' 120++-- | Address of node number @i@ for other nodes in the container.+nodeAddress :: Container -> Int -> String+nodeAddress c i = "127.0.0.1:" <> show (p2pPort c i)++-- | Address and port on the host for node number @i@.+hostAddress :: Container -> Int -> IO String+hostAddress c i = do+ out <- podman ["port", c.cid, show (p2pPort c i) <> "/tcp"]+ case lines out of+ a : _ -> return a+ [] -> fail $ "No port mapping for node " <> show i++-- | Regtest address valid for both implementations.+mineAddress :: String+mineAddress = "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn"++-- | Another regtest address. Mining a replacement chain to a different+-- address avoids recreating blocks identical to invalidated ones.+otherAddress :: String+otherAddress = "mfcGAzvis9JQAb6avB6WBGiGrgWzLxuGaC"++-- | Mine blocks on node number @i@ and return their hashes.+mine :: Container -> Int -> Int -> IO [String]+mine c i n = mineTo c i n mineAddress++-- | Mine blocks to an address on node number @i@ and return their hashes.+mineTo :: Container -> Int -> Int -> String -> IO [String]+mineTo c i n addr = do+ out <- cli' 600 c i ["generatetoaddress", show n, addr]+ return (filter is_hash (words (map clean out)))+ where+ clean x+ | isHexDigit x = x+ | otherwise = ' '+ is_hash w = length w == 64
+ test/unit/Haskoin/Node/AddressSpec.hs view
@@ -0,0 +1,45 @@+module Haskoin.Node.AddressSpec (spec) where++import Control.Monad (forM_)+import Haskoin (Network (..), btcRegTest)+import Haskoin.Node (toHostService, toSockAddr)+import Network.Socket (SockAddr (..))+import Test.Hspec+import Test.Hspec.QuickCheck (prop)++spec :: Spec+spec = do+ let net = btcRegTest+ describe "addresses" $ do+ it "parses host and service"+ $ forM_+ [ ("localhost", (Just "localhost", Nothing)),+ ("::1", (Just "::1", Nothing)),+ ("localhost:8080", (Just "localhost", Just "8080")),+ ("example.com", (Just "example.com", Nothing)),+ ("api.example.com:443", (Just "api.example.com", Just "443")),+ ("api.example.com:http", (Just "api.example.com", Just "http")),+ ("[::1]", (Just "::1", Nothing)),+ ("[::1]:8080", (Just "::1", Just "8080")),+ ("[2002::dead:beef]:ssh", (Just "2002::dead:beef", Just "ssh"))+ ]+ $ \(s, r) -> toHostService s `shouldBe` r++ it "resolves numeric addresses with the default port" $ do+ as <- toSockAddr net "127.0.0.1"+ map show as `shouldContain` ["127.0.0.1:" <> show net.defaultPort]++ it "returns no addresses for unresolvable hosts" $+ toSockAddr net "host.invalid" `shouldReturn` []++ it "keeps an explicit port" $ do+ as <- toSockAddr net "127.0.0.1:1234"+ map show as `shouldContain` ["127.0.0.1:1234"]++ -- IPv4 only: resolving an IPv6 address needs IPv6 on the host, which+ -- containers and CI runners often lack. The IPv6 forms are still parsed+ -- by the toHostService tests above, which need no resolver.+ prop "resolves any numeric IPv4 address to itself" $ \(e, w) -> do+ let a = SockAddrInet (toEnum (e `mod` 65536)) w+ s <- toSockAddr net (show a)+ s `shouldStartWith` [a]
+ test/unit/Haskoin/Node/Chain/SyncSpec.hs view
@@ -0,0 +1,154 @@+module Haskoin.Node.Chain.SyncSpec (spec) where++import Data.List (nub)+import Data.Maybe (isNothing)+import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (UTCTime (..), addUTCTime)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)+import Haskoin (BlockHeader (..), BlockNode (..), Network (..), btcRegTest, headerHash)+import Haskoin.Node.Chain.Sync+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++t0 :: UTCTime+t0 = UTCTime (fromGregorian 2026 1 1) 0++at :: Int -> UTCTime+at n = fromIntegral n `addUTCTime` t0++-- | Transitions applied to a few peers.+data Op+ = Connect Int+ | Disconnect Int+ | Request Int+ | Start Int+ | Refresh Int+ | Finish Int+ | Drop Int+ | Poll+ deriving stock (Show)++instance Arbitrary Op where+ arbitrary =+ oneof+ [ Connect <$> peer,+ Disconnect <$> peer,+ Request <$> peer,+ Start <$> peer,+ Refresh <$> peer,+ Finish <$> peer,+ Drop <$> peer,+ pure Poll+ ]+ where+ peer = chooseInt (1, 4)++apply :: SyncState Int -> Op -> SyncState Int+apply s = \case+ Connect p -> peerConnected p s+ Disconnect p -> peerDisconnected p s+ Request p -> requestSync p s+ Start p -> startSync (at 1) p s+ Refresh p -> refreshSync (at 2) p s+ Finish p -> finishSync p s+ Drop p -> dropPeer p s+ Poll -> pollPeers (at 3) s++states :: [Op] -> [SyncState Int]+states = scanl apply (emptySync t0)++run :: [Op] -> SyncState Int+run = foldl' apply (emptySync t0)++statuses :: SyncState Int -> [PeerStatus]+statuses s = [st | p <- [1 .. 4], Just st <- [peerStatus p s]]++isSyncingStatus :: PeerStatus -> Bool+isSyncingStatus = \case+ Syncing _ _ -> True+ _ -> False++spec :: Spec+spec = describe "header sync state" $ do+ it "queues peers in the order they connect" $+ queuedPeers (run [Connect 1, Connect 2, Connect 3]) `shouldBe` [1, 2, 3]++ it "syncs against one peer at a time" $ do+ let s = run [Connect 1, Connect 2, Start 1, Start 2]+ syncingPeer s `shouldBe` Just (1, at 1)+ queuedPeers s `shouldBe` [2]++ it "only starts syncing against queued peers" $+ syncingPeer (run [Connect 1, Start 1, Finish 1, Start 1]) `shouldBe` Nothing++ it "makes a peer idle when it finishes" $+ peerStatus 1 (run [Connect 1, Start 1, Finish 1]) `shouldBe` Just Idle++ it "queues a peer again if it announced blocks while syncing" $ do+ let s = run [Connect 1, Connect 2, Start 1, Request 1, Finish 1]+ queuedPeers s `shouldBe` [2, 1]++ it "queues an idle peer that announces blocks" $+ queuedPeers (run [Connect 1, Start 1, Finish 1, Request 1]) `shouldBe` [1]++ it "queues every idle peer when polling" $+ queuedPeers (run [Connect 1, Connect 2, Start 1, Finish 1, Start 2, Finish 2, Poll])+ `shouldBe` [1, 2]++ it "never queues a dropped peer again" $ do+ let s = run [Connect 1, Drop 1, Request 1, Poll, Connect 1]+ peerStatus 1 s `shouldBe` Just Dropped+ queuedPeers s `shouldBe` []+ connectedPeers s `shouldBe` []++ it "forgets disconnected peers" $+ peerStatus 1 (run [Connect 1, Start 1, Disconnect 1]) `shouldBe` Nothing++ it "records activity when requesting headers" $ do+ lastActivity (run [Connect 1, Start 1]) `shouldBe` at 1+ lastActivity (run [Connect 1, Start 1, Refresh 1]) `shouldBe` at 2+ lastActivity (touchSync (at 9) (emptySync t0) :: SyncState Int) `shouldBe` at 9++ describe "synced" $ do+ let net = btcRegTest+ tipAt t w =+ BlockNode+ { header = net.genesisHeader {timestamp = t},+ height = 1,+ work = w,+ skip = headerHash net.genesisHeader+ }+ now = at 0+ recent = tipAt (floor (utcTimeToPOSIXSeconds now) - 60) 1+ idle = run [Connect 1, Start 1, Finish 1]+ it "is synced with a recent tip, enough work and nothing to do" $+ isSynced net now recent idle `shouldBe` True+ it "is not synced with an old tip" $+ isSynced net (maxTipAge `addUTCTime` now) recent idle `shouldBe` False+ it "is not synced below the minimum chain work" $+ isSynced net {minimumChainWork = 2} now recent idle `shouldBe` False+ it "is not synced while a peer is queued or syncing" $ do+ isSynced net now recent (run [Connect 1]) `shouldBe` False+ isSynced net now recent (run [Connect 1, Start 1]) `shouldBe` False++ prop "never syncs against more than one peer" $ \ops ->+ all ((<= 1) . length . filter isSyncingStatus . statuses) (states ops)++ prop "lists each peer once" $ \ops ->+ all (\s -> let ps = [p | p <- [1 .. 4], Just _ <- [peerStatus p s]] in ps == nub ps) (states ops)++ prop "never queues or syncs a dropped peer until it reconnects" $+ \ops more -> forAll (chooseInt (1, 4)) $ \p ->+ let connected = run (ops <> [Connect p])+ s = foldl' apply (dropPeer p connected) (filter (not . disconnects p) more)+ in peerStatus p s === Just Dropped+ .&&. p `notElem` queuedPeers s+ .&&. fmap fst (syncingPeer s) =/= Just p+ .&&. p `notElem` connectedPeers s++ prop "forgets a peer after it disconnects" $ \ops (p :: Int) ->+ isNothing (peerStatus p (run (ops <> [Disconnect p])))+ where+ disconnects p (Disconnect q) = p == q+ disconnects _ _ = False
+ test/unit/Haskoin/Node/ChainSpec.hs view
@@ -0,0 +1,407 @@+{-# OPTIONS_GHC -Wno-deprecations #-}++-- | Header chain process driven by fake peers.+module Haskoin.Node.ChainSpec (spec) where++import Control.Monad (forM_, forever, void)+import Data.Text (Text)+import Data.Time.Clock (NominalDiffTime)+import Database.RocksDB (DB)+import Haskoin+import Haskoin.Node+import Haskoin.Node.Peer.Lock (Lock (..))+import Haskoin.Node.Test+import NQE+import Test.HUnit (assertFailure)+import Test.Hspec hiding (pending)+import UnliftIO++data ChainTest = ChainTest+ { chain :: !Chain,+ events :: !(Inbox ChainEvent),+ peerPub :: !(Publisher PeerEvent),+ net :: !Network+ }++-- | Peer whose outgoing messages are captured instead of sent.+data FakePeer = FakePeer+ { peer :: !Peer,+ inbox :: !(Inbox PeerMessage)+ }++withChainTest :: Network -> NominalDiffTime -> (ChainTest -> IO a) -> IO a+withChainTest net to f = withTestDB $ \db -> withChainOn net to db f++withChainOn ::+ Network -> NominalDiffTime -> DB -> (ChainTest -> IO a) -> IO a+withChainOn net to db f =+ withTestLogger $ \logger ->+ withPublisher $ \pub ->+ withSubscription pub $ \events ->+ withPublisher $ \peerPub ->+ withChain (ChainConfig db Nothing net pub to logger) $ \chain ->+ f ChainTest {chain, events, peerPub, net}++newFakePeer :: ChainTest -> Text -> IO FakePeer+newFakePeer ct label = do+ (inbox, mailbox) <- newMailbox+ busy <- newTVarIO Free+ pending <- newTVarIO 0+ return+ FakePeer+ { peer = Peer {mailbox, pub = ct.peerPub, label, busy, pending},+ inbox+ }++connectPeer :: ChainTest -> Text -> IO FakePeer+connectPeer ct label = do+ fp <- newFakePeer ct label+ chainPeerConnected ct.chain fp.peer+ return fp++describeMessage :: PeerMessage -> String+describeMessage KillPeer = "disconnection"+describeMessage (SendMessage m) = show (commandToString (msgType m))++-- | Next header request sent to the peer, skipping 'MSendHeaders'.+expectGetHeaders :: FakePeer -> IO GetHeaders+expectGetHeaders fp =+ within 10 ("header request to " <> show fp.peer) go+ where+ go =+ receive fp.inbox >>= \case+ SendMessage (MGetHeaders g) -> return g+ SendMessage MSendHeaders -> go+ m -> assertFailure $ "Expected header request, got " <> describeMessage m++expectSendHeaders :: FakePeer -> IO ()+expectSendHeaders fp =+ receiveWithin 10 "sendheaders" fp.inbox $ \case+ SendMessage MSendHeaders -> Just ()+ _ -> Nothing++expectKilled :: FakePeer -> IO ()+expectKilled fp =+ receiveWithin 10 ("disconnection of " <> show fp.peer) fp.inbox $ \case+ KillPeer -> Just ()+ _ -> Nothing++-- | Fail if the peer is asked for headers or disconnected in time window.+expectNoRequest :: Double -> FakePeer -> IO ()+expectNoRequest s fp =+ expectNothingWithin s ("request to " <> show fp.peer) fp.inbox $ \case+ SendMessage (MGetHeaders _) -> Just ()+ KillPeer -> Just ()+ _ -> Nothing++sendHeaders :: ChainTest -> FakePeer -> [BlockHeader] -> IO ()+sendHeaders ct fp = chainHeaders ct.chain fp.peer++-- | Wait for a header request and answer it from the provided chain.+answer :: ChainTest -> FakePeer -> [BlockHeader] -> IO [BlockHeader]+answer ct fp hs = do+ g <- expectGetHeaders fp+ let r = serveHeaders ct.net hs g+ sendHeaders ct fp r+ return r++-- | Answer all header requests from the chain in the variable. Counts the+-- requests.+withServer ::+ ChainTest -> FakePeer -> TVar [BlockHeader] -> (TVar Int -> IO a) -> IO a+withServer ct fp var f = do+ count <- newTVarIO 0+ let go = forever $ do+ g <- receiveMatch fp.inbox $ \case+ SendMessage (MGetHeaders g) -> Just g+ _ -> Nothing+ atomically $ modifyTVar count (+ 1)+ hs <- readTVarIO var+ sendHeaders ct fp (serveHeaders ct.net hs g)+ withAsync go $ \a -> link a >> f count++waitHeight :: ChainTest -> BlockHeight -> IO BlockNode+waitHeight ct h = do+ eventually 30 ("best height " <> show h) $+ (== h) . (.height) <$> chainGetBest ct.chain+ chainGetBest ct.chain++bestHash :: ChainTest -> IO BlockHash+bestHash ct = headerHash . (.header) <$> chainGetBest ct.chain++-- | Sync a fresh peer to the provided chain.+syncPeer :: ChainTest -> Text -> [BlockHeader] -> IO FakePeer+syncPeer ct label hs = do+ fp <- connectPeer ct label+ _ <- answer ct fp hs+ _ <- waitHeight ct (fromIntegral (length hs))+ expectSendHeaders fp+ return fp++invalidPow :: BlockHeader -> BlockHeader+invalidPow h = h {bits = 0x1d00ffff}++spec :: Spec+spec = forM_ [btcRegTest, bchRegTest] $ \net ->+ describe ("header chain on " <> net.name) $ do+ t0 <- runIO $ subtract 6000 <$> nowTimestamp+ let main = mkHeaders net 0 net.genesisHeader t0 4500+ short = take 20 main+ hashAt i = headerHash (main !! (i - 1))+ nodeHash = headerHash . (.header)+ fast = withChainTest net 1+ slow = withChainTest net 600++ describe "initial sync" $ do+ it "requests headers in batches until the peer runs out" $ slow $ \ct -> do+ a <- connectPeer ct "a"+ g1 <- expectGetHeaders a+ take 1 g1.locator `shouldBe` [headerHash net.genesisHeader]+ sendHeaders ct a (serveHeaders net main g1)+ g2 <- expectGetHeaders a+ take 1 g2.locator `shouldBe` [hashAt 2000]+ sendHeaders ct a (serveHeaders net main g2)+ g3 <- expectGetHeaders a+ take 1 g3.locator `shouldBe` [hashAt 4000]+ r3 <- return (serveHeaders net main g3)+ length r3 `shouldBe` 500+ sendHeaders ct a r3+ expectSendHeaders a+ expectNoRequest 0.5 a+ best <- waitHeight ct 4500+ nodeHash best `shouldBe` hashAt 4500++ it "publishes best block and synced events once" $ slow $ \ct -> do+ a <- connectPeer ct "a"+ var <- newTVarIO main+ withServer ct a var $ \count -> do+ bn <- receiveWithin 30 "synced event" ct.events $ \case+ ChainSynced bn -> Just bn+ _ -> Nothing+ bn.height `shouldBe` 4500+ chainIsSynced ct.chain `shouldReturn` True+ readTVarIO count `shouldReturn` 3+ expectNothingWithin 0.5 "second synced event" ct.events $ \case+ ChainSynced _ -> Just ()+ _ -> Nothing++ it "does not consider old headers synced" $ slow $ \ct -> do+ let old = mkHeaders net 0 net.genesisHeader 1500000000 10+ _ <- syncPeer ct "a" old+ chainIsSynced ct.chain `shouldReturn` False++ it "stops asking a peer that sends no headers" $ slow $ \ct -> do+ a <- connectPeer ct "a"+ _ <- expectGetHeaders a+ sendHeaders ct a []+ expectSendHeaders a+ expectNoRequest 1 a++ it "syncs against every peer that connects" $ slow $ \ct -> do+ _ <- syncPeer ct "a" (take 10 main)+ b <- connectPeer ct "b"+ _ <- answer ct b (take 30 main)+ void $ waitHeight ct 30++ it "ignores duplicate connection events" $ slow $ \ct -> do+ a <- connectPeer ct "a"+ chainPeerConnected ct.chain a.peer+ _ <- answer ct a short+ expectSendHeaders a+ expectNoRequest 1 a++ it "keeps headers across restarts" $ withTestDB $ \db -> do+ withChainOn net 600 db $ \ct -> void $ syncPeer ct "a" short+ withChainOn net 600 db $ \ct -> do+ bn <- receiveWithin 10 "best block" ct.events $ \case+ ChainBestBlock bn -> Just bn+ _ -> Nothing+ bn.height `shouldBe` 20+ bestHash ct `shouldReturn` hashAt 20++ describe "announcements" $ do+ it "requests headers when an unknown block is announced by inventory" $+ slow $ \ct -> do+ a <- syncPeer ct "a" (take 10 main)+ chainBlocksAnnounced ct.chain a.peer [hashAt 109]+ _ <- answer ct a (take 109 main)+ void $ waitHeight ct 109++ it "ignores inventory announcements of known blocks" $ slow $ \ct -> do+ a <- syncPeer ct "a" (take 10 main)+ chainBlocksAnnounced ct.chain a.peer [hashAt 5, hashAt 10]+ expectNoRequest 1 a++ it "imports announced headers that connect" $ slow $ \ct -> do+ a <- syncPeer ct "a" (take 10 main)+ sendHeaders ct a [main !! 10]+ _ <- waitHeight ct 11+ expectNoRequest 0.5 a++ it "requests headers when announced headers do not connect" $+ slow $ \ct -> do+ a <- syncPeer ct "a" (take 10 main)+ sendHeaders ct a (take 5 (drop 49 main))+ _ <- answer ct a (take 54 main)+ void $ waitHeight ct 54++ it "continues syncing a peer that sends a full batch unsolicited" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ _ <- expectGetHeaders a+ sendHeaders ct b (take 2000 main)+ _ <- waitHeight ct 2000+ sendHeaders ct a []+ g <- expectGetHeaders b+ take 1 g.locator `shouldBe` [hashAt 2000]++ it "keeps syncing peer when another peer announces headers" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ g <- expectGetHeaders a+ sendHeaders ct b (take 5 main)+ _ <- waitHeight ct 5+ sendHeaders ct a (serveHeaders net short g)+ _ <- waitHeight ct 20+ expectSendHeaders a+ -- b gets its turn once a is done+ void $ answer ct b (take 5 main)++ it "ignores messages from peers that are not connected" $+ slow $ \ct -> do+ x <- newFakePeer ct "x"+ sendHeaders ct x (take 5 main)+ chainBlocksAnnounced ct.chain x.peer [hashAt 5]+ expectNoRequest 1 x+ (.height) <$> chainGetBest ct.chain `shouldReturn` 0++ describe "reorganizations" $ do+ let fork n = mkHeaders net 1 (main !! 14) (t0 + 100) n+ it "switches to a fork with more work" $ slow $ \ct -> do+ a <- syncPeer ct "a" short+ sendHeaders ct a (fork 10)+ best <- waitHeight ct 25+ nodeHash best `shouldBe` headerHash (last (fork 10))+ chainBlockMain ct.chain (hashAt 20) `shouldReturn` False+ chainBlockMain ct.chain (hashAt 15) `shouldReturn` True+ Just old <- chainGetBlock ct.chain (hashAt 20)+ split <- chainGetSplitBlock ct.chain old best+ nodeHash split `shouldBe` hashAt 15++ it "stays on the chain with more work" $ slow $ \ct -> do+ a <- syncPeer ct "a" short+ sendHeaders ct a (fork 3)+ let tip = headerHash (last (fork 3))+ eventually 10 "fork stored" $+ (/= Nothing) <$> chainGetBlock ct.chain tip+ bestHash ct `shouldReturn` hashAt 20+ chainBlockMain ct.chain tip `shouldReturn` False++ describe "misbehaving peers" $ do+ it "disconnects a peer that sends headers with invalid proof of work" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ _ <- expectGetHeaders a+ sendHeaders ct a [invalidPow (main !! 0)]+ expectKilled a++ it "disconnects a peer that sends headers that do not form a chain" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ _ <- expectGetHeaders a+ sendHeaders ct a [main !! 0, main !! 2]+ expectKilled a++ it "syncs from another peer after disconnecting a misbehaving one" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ _ <- expectGetHeaders a+ sendHeaders ct a [invalidPow (main !! 0)]+ expectKilled a+ _ <- answer ct b short+ void $ waitHeight ct 20++ it "disconnects an unresponsive syncing peer and moves on" $+ fast $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ _ <- expectGetHeaders a+ expectKilled a+ _ <- answer ct b short+ void $ waitHeight ct 20++ it "moves on when the syncing peer disconnects" $ slow $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ _ <- expectGetHeaders a+ chainPeerDisconnected ct.chain a.peer+ _ <- answer ct b short+ void $ waitHeight ct 20++ it "never asks a peer for headers after it disconnects" $+ slow $ \ct -> do+ a <- connectPeer ct "a"+ b <- connectPeer ct "b"+ _ <- expectGetHeaders a+ -- queue a again while it is syncing+ chainBlocksAnnounced ct.chain a.peer [hashAt 5]+ chainPeerDisconnected ct.chain a.peer+ _ <- answer ct b short+ _ <- waitHeight ct 20+ expectNoRequest 1 a++ it "waits for a peer reserved by someone else" $ fast $ \ct -> do+ other <- newOwner "other"+ a <- newFakePeer ct "a"+ True <- reservePeer other a.peer+ chainPeerConnected ct.chain a.peer+ expectNoRequest 1 a+ True <- releasePeer other a.peer+ void $ answer ct a short++ it "keeps its reservation when others try to release the peer" $+ slow $ \ct -> do+ other <- newOwner "other"+ a <- connectPeer ct "a"+ _ <- expectGetHeaders a+ False <- releasePeer other a.peer+ setFree a.peer+ fmap ownerName <$> peerHolder a.peer `shouldReturn` Just "Chain"+ False <- reservePeer other a.peer+ sendHeaders ct a []+ eventually 10 "peer released" $ (== Nothing) . fmap ownerName <$> peerHolder a.peer++ describe "stall recovery" $ do+ it "asks peers for headers when nothing happens for a while" $+ fast $ \ct -> do+ a <- syncPeer ct "a" (take 10 main)+ -- the peer finds blocks but never announces them+ var <- newTVarIO (take 30 main)+ withServer ct a var $ \_ -> void (waitHeight ct 30)++ it "recovers when the syncing peer stops responding" $ fast $ \ct -> do+ a <- connectPeer ct "a"+ _ <- expectGetHeaders a+ expectKilled a+ chainPeerDisconnected ct.chain a.peer+ b <- connectPeer ct "b"+ void $ answer ct b short++ describe "queries" $ do+ it "answers questions about the chain" $ slow $ \ct -> do+ _ <- syncPeer ct "a" short+ tip <- chainGetBest ct.chain+ Just anc <- chainGetAncestor ct.chain 10 tip+ nodeHash anc `shouldBe` hashAt 10+ ps <- chainGetParents ct.chain 15 tip+ map (.height) ps `shouldBe` [15 .. 19]+ map nodeHash ps `shouldBe` map hashAt [15 .. 19]+ chainGetBlock ct.chain (headerHash (main !! 100)) `shouldReturn` Nothing+ chainBlockMain ct.chain (headerHash (main !! 100)) `shouldReturn` False+ chainBlockMain ct.chain (hashAt 20) `shouldReturn` True+ Just n5 <- chainGetBlock ct.chain (hashAt 5)+ split <- chainGetSplitBlock ct.chain n5 tip+ nodeHash split `shouldBe` hashAt 5
+ test/unit/Haskoin/Node/Peer/LockSpec.hs view
@@ -0,0 +1,60 @@+module Haskoin.Node.Peer.LockSpec (spec) where++import Haskoin.Node.Peer.Lock+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++-- | An attempt to reserve or release, by one of a few owners.+data Op = Acquire Int | Release Int+ deriving stock (Show)++instance Arbitrary Op where+ arbitrary = oneof [Acquire <$> owner, Release <$> owner]+ where+ owner = chooseInt (1, 3)++-- | Apply an operation, keeping the lock unchanged when it does not apply.+apply :: Lock Int -> Op -> Lock Int+apply l = \case+ Acquire o -> maybe l id (acquire o l)+ Release o -> maybe l id (release o l)++spec :: Spec+spec = describe "peer lock" $ do+ it "lets an owner reserve a free peer" $+ acquire 1 Free `shouldBe` Just (HeldBy (1 :: Int))++ it "refuses to reserve a held peer, even for its holder" $ do+ acquire 2 (HeldBy (1 :: Int)) `shouldBe` Nothing+ acquire 1 (HeldBy (1 :: Int)) `shouldBe` Nothing++ it "lets the holder release the peer" $+ release 1 (HeldBy (1 :: Int)) `shouldBe` Just Free++ it "refuses to release for anyone but the holder" $ do+ release 2 (HeldBy (1 :: Int)) `shouldBe` Nothing+ release 1 (Free :: Lock Int) `shouldBe` Nothing++ prop "only the last owner to reserve can release" $ \(ops :: [Op]) ->+ let final = foldl' apply Free ops+ model = foldl' step Nothing ops+ step Nothing (Acquire o) = Just o+ step (Just h) (Release o) | h == o = Nothing+ step m _ = m+ in holder final === model++ describe "owners" $ do+ it "are all different" $ do+ a <- newOwner "a"+ b <- newOwner "a"+ a `shouldNotBe` b+ a `shouldBe` a++ it "share the legacy owner" $+ legacyOwner `shouldBe` legacyOwner++ it "keep their names" $ do+ a <- newOwner "chain"+ ownerName a `shouldBe` "chain"+ ownerName legacyOwner `shouldBe` "legacy"
+ test/unit/Haskoin/Node/Peer/RequestSpec.hs view
@@ -0,0 +1,90 @@+module Haskoin.Node.Peer.RequestSpec (spec) where++import Haskoin+import Haskoin.Node.Peer.Request+import Haskoin.Node.Test (mkBlocks)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++blocks :: [Block]+blocks = mkBlocks btcRegTest 0 btcRegTest.genesisHeader 1700000000 5++blockInv :: Block -> InvVector+blockInv b = InvVector InvWitnessBlock (headerHash b.header).get++txInv :: Tx -> InvVector+txInv tx = InvVector InvWitnessTx (txHash tx).get++-- | Feed messages to a request for the given items.+feed :: [InvVector] -> [Message] -> RequestStep+feed ivs = foldl' step (newRequest 42 ivs)+ where+ step (Continue r) m = stepRequest r m+ step done _ = done++-- | Messages that have nothing to do with the request.+noise :: Gen Message+noise =+ elements+ [ MPing (Ping 1),+ MPong (Pong 1),+ MVerAck,+ MSendHeaders,+ MInv (Inv [blockInv (blocks !! 4)]),+ MNotFound (NotFound [txInv (coinbaseOf (blocks !! 4))])+ ]++coinbaseOf :: Block -> Tx+coinbaseOf b = case b.txs of+ tx : _ -> tx+ [] -> error "block without transactions"++spec :: Spec+spec = describe "data request" $ do+ let want = take 3 blocks+ ivs = map blockInv want+ done = Complete (map Right want)++ it "completes when all items arrive in order" $+ feed ivs (map MBlock want) `shouldBe` done++ it "completes immediately when nothing is requested" $+ newRequest 42 [] `shouldBe` Complete []++ it "receives transactions" $ do+ let txs = map coinbaseOf want+ feed (map txInv txs) (map MTx txs) `shouldBe` Complete (map Left txs)++ it "waits for missing items" $+ case feed ivs (map MBlock (take 2 want)) of+ Continue _ -> return ()+ other -> expectationFailure ("Expected to continue, got " <> show other)++ it "fails when items arrive out of order" $+ feed ivs [MBlock (want !! 1)] `shouldBe` Failed "block out of order"++ it "fails when a requested item is not found" $+ feed ivs [MBlock (head' want), MNotFound (NotFound [ivs !! 2])]+ `shouldBe` Failed "item not found"++ it "fails when the pong arrives before all items" $+ feed ivs [MBlock (head' want), MPong (Pong 42)]+ `shouldBe` Failed "incomplete response"++ it "ignores a pong for another ping" $+ feed ivs (MPong (Pong 7) : map MBlock want) `shouldBe` done++ it "does not accept a transaction for a block request" $+ case feed ivs [MTx (coinbaseOf (head' want))] of+ Continue _ -> return ()+ other -> expectationFailure ("Expected to continue, got " <> show other)++ prop "ignores unrelated messages between items" $+ forAll (vectorOf 3 (listOf noise)) $ \gaps ->+ let msgs = concat (zipWith (\gap b -> gap <> [MBlock b]) gaps want)+ in feed ivs msgs === done+ where+ head' xs = case xs of+ x : _ -> x+ [] -> error "empty"
+ test/unit/Haskoin/Node/PeerMgr/AddressesSpec.hs view
@@ -0,0 +1,139 @@+module Haskoin.Node.PeerMgr.AddressesSpec (spec) where++import Data.Maybe (isJust, isNothing)+import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime)+import Haskoin.Node.PeerMgr.Addresses+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++t0 :: UTCTime+t0 = UTCTime (fromGregorian 2026 1 1) 0++later :: NominalDiffTime -> UTCTime+later d = d `addUTCTime` t0++book :: [Int] -> AddressBook Int+book = foldl' (flip (addAddress Discovered)) emptyBook++-- | Take an address that must be available.+taken :: UTCTime -> AddressBook Int -> (Int, AddressBook Int)+taken now b = case takeAddress now b of+ Just x -> x+ Nothing -> error "no address to take"++-- | Take an address, end its connection, and return the new book.+cycleWith :: Outcome -> Int -> AddressBook Int -> AddressBook Int+cycleWith outcome a b = case takeAddress t0 b of+ Just (a', b') | a == a' -> finishAddress t0 outcome a b'+ _ -> error "address not taken"++-- | When an address can be tried again, if it is waiting.+retryAt :: Int -> AddressBook Int -> Maybe UTCTime+retryAt a b = case addressState a b of+ Just (Waiting t _) -> Just t+ _ -> Nothing++-- | Transitions applied to a few addresses at increasing times.+data Op+ = Add Source Int+ | Take NominalDiffTime+ | Give Int+ | Connected Int+ | Finish NominalDiffTime Outcome Int+ deriving stock (Show)++instance Arbitrary Op where+ arbitrary =+ oneof+ [ Add <$> elements [Configured, Discovered] <*> addr,+ Take <$> time,+ Give <$> addr,+ Connected <$> addr,+ Finish <$> time <*> elements [Disconnected, Failed, Misbehaved] <*> addr+ ]+ where+ addr = chooseInt (1, 4)+ time = fromIntegral <$> chooseInt (0, 100000)++apply :: AddressBook Int -> Op -> AddressBook Int+apply b = \case+ Add src a -> addAddress src a b+ Take d -> maybe b snd (takeAddress (later d) b)+ Give a -> returnAddress a b+ Connected a -> markConnected a b+ Finish d o a -> finishAddress (later d) o a b++spec :: Spec+spec = describe "address book" $ do+ it "hands out each address once" $ do+ let (a, b) = taken t0 (book [1])+ a `shouldBe` 1+ addressState 1 b `shouldBe` Just InUse+ takeAddress t0 b `shouldSatisfy` isNothing++ it "takes back an address that was not used" $ do+ let (_, b) = taken t0 (book [1])+ addressState 1 (returnAddress 1 b) `shouldBe` Just Ready++ it "waits briefly later a normal disconnection" $ do+ let b = cycleWith Disconnected 1 (book [1])+ retryAt 1 b `shouldBe` Just (later retryDelay)+ takeAddress t0 b `shouldSatisfy` isNothing+ takeAddress (later retryDelay) b `shouldSatisfy` isJust++ it "doubles the wait later each failure, up to a limit" $ do+ let waits = map (retryAt 1) (drop 1 (iterate (cycleFailed 1) (book [1])))+ cycleFailed a b = finishAddress t0 Failed a (snd (taken (later 1000000) b))+ take 4 waits `shouldBe` map (Just . later) [2, 4, 8, 16]+ waits !! 20 `shouldBe` Just (later maxBackoff)++ it "forgets failures once the peer comes online" $ do+ let failed = cycleWith Failed 1 (book [1])+ (_, b) = taken (later 1000) failed+ b' = finishAddress t0 Failed 1 (markConnected 1 b)+ retryAt 1 b' `shouldBe` Just (later 2)++ it "bans a discovered address that misbehaved" $+ retryAt 1 (cycleWith Misbehaved 1 (book [1])) `shouldBe` Just (later banTime)++ it "never bans a configured address" $ do+ let b = addAddress Configured 1 (book [1])+ retryAt 1 (cycleWith Misbehaved 1 b) `shouldBe` Just (later maxBackoff)++ it "keeps a bounded number of discovered addresses" $ do+ let b = book [1 .. maxDiscovered + 10]+ b' = addAddress Configured (maxDiscovered + 20) b+ length (knownAddresses b) `shouldBe` maxDiscovered+ length (knownAddresses b') `shouldBe` maxDiscovered + 1++ describe "loading" $ do+ it "loads when the book is empty" $+ needsReload t0 (emptyBook :: AddressBook Int) `shouldBe` True++ it "does not load while an address can be tried" $+ needsReload t0 (book [1]) `shouldBe` False++ it "does not load again too soon" $ do+ -- banned, so that nothing can be tried for a long time+ let b = markLoaded t0 (cycleWith Misbehaved 1 (book [1]))+ needsReload (later 1) b `shouldBe` False+ needsReload (later reloadInterval) b `shouldBe` True++ prop "only hands out addresses that can be tried" $ \(ops :: [Op]) (d :: Int) ->+ let b = foldl' apply emptyBook ops+ now = later (fromIntegral (abs d))+ in case takeAddress now b of+ Nothing -> property True+ Just (a, b') ->+ let ok = case addressState a b of+ Just Ready -> True+ Just (Waiting t _) -> t <= now+ _ -> False+ in ok .&&. addressState a b' === Just InUse++ prop "never forgets an address" $ \(ops :: [Op]) ->+ let b = foldl' apply emptyBook ops+ added = [a | Add _ a <- ops]+ in all (\a -> isJust (addressState a b)) added
+ test/unit/Haskoin/Node/PeerMgr/HandshakeSpec.hs view
@@ -0,0 +1,95 @@+module Haskoin.Node.PeerMgr.HandshakeSpec (spec) where++import Control.Monad (foldM)+import Data.Word (Word64)+import Haskoin+import Haskoin.Node (buildVersion, myVersion)+import Haskoin.Node.PeerMgr.Handshake+import Haskoin.Node.Test (zeroAddress)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++-- | Version of a full node with nonce 7.+version :: Version+version = versionWith nodeNetwork++versionWith :: Word64 -> Version+versionWith services =+ buildVersion btcRegTest 7 0 (zeroAddress services) (zeroAddress 0) 0++-- | Run events from the start of the handshake, collecting actions.+run :: [HandshakeEvent] -> Either HandshakeError (Handshake, [HandshakeAction])+run = foldM step (AwaitingBoth, [])+ where+ step (h, acc) e = fmap (acc <>) <$> stepHandshake h e++-- | Random handshake messages: 'True' is a version, 'False' a verack.+toEvents :: [Bool] -> [HandshakeEvent]+toEvents = map (\b -> if b then GotVersion version else GotVerAck)++-- | At most one version and at most one verack, in any order: every sequence+-- a well-behaved peer can send.+validEvents :: Gen [Bool]+validEvents = do+ msgs <- sublistOf [True, False]+ elements [msgs, reverse msgs]++count :: (Eq a) => a -> [a] -> Int+count x = length . filter (== x)++spec :: Spec+spec = describe "version handshake" $ do+ it "completes when the version comes first" $+ run [GotVersion version, GotVerAck]+ `shouldBe` Right (Established version, [SendVerAck, AnnounceConnected])++ it "completes when the verack comes first" $+ run [GotVerAck, GotVersion version]+ `shouldBe` Right (Established version, [SendVerAck, AnnounceConnected])++ it "waits for the verack after the version" $+ run [GotVersion version] `shouldBe` Right (AwaitingVerAck version, [SendVerAck])++ it "rejects a repeated verack" $ do+ run [GotVerAck, GotVerAck] `shouldBe` Left DuplicateVerAck+ run [GotVersion version, GotVerAck, GotVerAck]+ `shouldBe` Left DuplicateVerAck++ it "rejects a repeated version" $ do+ run [GotVersion version, GotVersion version] `shouldBe` Left DuplicateVersion+ run [GotVersion version, GotVerAck, GotVersion version]+ `shouldBe` Left DuplicateVersion++ prop "fails exactly when a message is repeated" $ \bs ->+ let repeated = count True bs > 1 || count False bs > 1+ in either (const True) (const False) (run (toEvents bs)) === repeated++ prop "acknowledges the version once" $+ forAll validEvents $ \bs ->+ fmap (count SendVerAck . snd) (run (toEvents bs)) === Right (count True bs)++ prop "announces the peer once, when both messages arrived" $+ forAll validEvents $ \bs ->+ let both = length bs == 2+ in fmap (\(h, as) -> (isEstablished h, count AnnounceConnected as)) (run (toEvents bs))+ === Right (both, if both then 1 else 0)++ prop "knows the version of the peer once received" $+ forAll validEvents $ \bs ->+ fmap (handshakeVersion . fst) (run (toEvents bs))+ === Right (if True `elem` bs then Just version else Nothing)++ prop "knows whether the peer acknowledged our version" $+ forAll validEvents $ \bs ->+ fmap (hasVerAck . fst) (run (toEvents bs)) === Right (False `elem` bs)++ describe "version checks" $ do+ it "accepts a full node" $+ checkVersion [1, 2] version `shouldBe` Right version+ it "rejects a peer that does not serve blocks" $+ checkVersion [] (versionWith 0) `shouldBe` Left NotFullNode+ it "rejects a connection to ourselves" $+ checkVersion [7] version `shouldBe` Left ConnectedToSelf+ it "uses our protocol version" $+ version.version `shouldBe` fromIntegral myVersion
+ test/unit/Haskoin/Node/PeerMgr/LifecycleSpec.hs view
@@ -0,0 +1,157 @@+module Haskoin.Node.PeerMgr.LifecycleSpec (spec) where++import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (UTCTime (..), addUTCTime)+import Haskoin (Version, btcRegTest, nodeNetwork)+import Haskoin.Node (buildVersion)+import Haskoin.Node.PeerMgr.Handshake+import Haskoin.Node.PeerMgr.Lifecycle+import Haskoin.Node.Test (zeroAddress)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck++t0 :: UTCTime+t0 = UTCTime (fromGregorian 2026 1 1) 0++-- | Seconds after the connection opened.+at :: Double -> UTCTime+at s = realToFrac s `addUTCTime` t0++version :: Version+version = buildVersion btcRegTest 7 0 (zeroAddress nodeNetwork) (zeroAddress 0) 0++-- | A ten second timeout and a hundred second maximum life.+limits :: Limits+limits = Limits {timeout = 10, maxLife = 100}++-- | Feed inputs to a new connection, collecting the actions.+run :: [PeerInput] -> (PeerState, [PeerAction])+run = feed (newPeerState t0)++-- | Feed inputs to a connection, collecting the actions.+feed :: PeerState -> [PeerInput] -> (PeerState, [PeerAction])+feed st0 = foldl' go (st0, [])+ where+ go (st, acc) i = let (st', as) = stepPeer limits i st in (st', acc <> as)++-- | A connection that completed the handshake.+online :: [PeerInput]+online = [GotHandshake (GotVersion version), GotHandshake GotVerAck]++-- | A random input.+newtype Input = Input PeerInput+ deriving stock (Show)++instance Arbitrary Input where+ arbitrary =+ Input+ <$> oneof+ [ Heard <$> time,+ GotHandshake <$> elements [GotVersion version, GotVerAck],+ Refused <$> elements [NotFullNode, ConnectedToSelf],+ GotPong <$> time <*> nonce,+ Tick <$> time <*> nonce+ ]+ where+ time = at <$> choose (0, 200)+ nonce = chooseBoundedIntegral (1, 3)++spec :: Spec+spec = describe "connection lifecycle" $ do+ describe "handshake" $ do+ it "goes online and announces the peer" $ do+ let (st, actions) = run online+ isOnline st `shouldBe` True+ st.announced `shouldBe` True+ stageVersion st `shouldBe` Just version+ actions `shouldBe` [ReplyVerAck, Announce]++ it "accepts the verack first" $+ snd (run [GotHandshake GotVerAck, GotHandshake (GotVersion version)])+ `shouldBe` [ReplyVerAck, Announce]++ it "disconnects a peer that repeats a handshake message once online" $+ snd (run (online <> [GotHandshake GotVerAck]))+ `shouldBe` [ReplyVerAck, Announce, Disconnect (Rejected DuplicateVerAck)]++ it "disconnects a peer whose version was refused" $ do+ let (st, actions) = run [Refused ConnectedToSelf]+ actions `shouldBe` [Disconnect (Rejected ConnectedToSelf)]+ st.stage `shouldBe` Disconnecting (Rejected ConnectedToSelf)++ it "disconnects a peer that does not complete the handshake in time" $+ -- chatter keeps the peer from being silent, but not from timing out+ snd (run [GotHandshake GotVerAck, Heard (at 9), Tick (at 11) 1])+ `shouldBe` [Disconnect HandshakeTimeout]++ describe "pings" $ do+ it "pings an online peer on a tick" $ do+ let (st, actions) = run (online <> [Tick (at 1) 5])+ drop 2 actions `shouldBe` [SendPing 5]+ pendingPing st `shouldBe` Just (at 1, 5)++ it "pings once until the pong arrives" $+ drop 2 (snd (run (online <> [Tick (at 1) 5, Tick (at 2) 6])))+ `shouldBe` [SendPing 5]++ it "records the round trip of a matching pong" $ do+ let (st, _) = run (online <> [Tick (at 1) 5, GotPong (at 1.5) 5])+ pendingPing st `shouldBe` Nothing+ st.pings `shouldBe` [0.5]++ it "ignores a pong with another nonce" $ do+ let (st, _) = run (online <> [Tick (at 1) 5, GotPong (at 1.5) 6])+ pendingPing st `shouldBe` Just (at 1, 5)+ st.pings `shouldBe` []++ it "keeps the last eleven round trips" $ do+ -- the pong also counts as hearing from the peer+ let ping i = [Tick (at i) 1, GotPong (at (i + i / 100)) 1, Heard (at (i + i / 100))]+ (st, _) = run (online <> concatMap ping [1 .. 15])+ -- in milliseconds, so that rounding in the times does not matter+ map (\d -> round (d * 1000) :: Int) st.pings `shouldBe` [150, 140 .. 50]++ it "disconnects a peer that does not answer a ping in time" $+ -- other traffic does not make up for a missing pong+ drop 3 (snd (run (online <> [Tick (at 1) 5, Heard (at 10), Tick (at 12) 6])))+ `shouldBe` [Disconnect NoPong]++ describe "timers" $ do+ it "disconnects a silent peer" $+ drop 2 (snd (run (online <> [Tick (at 11) 1])))+ `shouldBe` [Disconnect Silent]++ it "rotates a peer connected for too long" $+ drop 2 (snd (run (online <> [Heard (at 100), Tick (at 101) 1])))+ `shouldBe` [Disconnect TooOld]++ it "rotates before checking anything else" $+ snd (run [Tick (at 101) 1]) `shouldBe` [Disconnect TooOld]++ describe "properties" $ do+ prop "ignores everything once disconnecting" $ \(ins :: [Input]) ->+ let inputs = [i | Input i <- ins]+ in let (st, _) = run [Refused NotFullNode]+ in snd (feed st inputs) === []++ prop "announces a peer at most once, and remembers it" $ \(ins :: [Input]) ->+ let inputs = [i | Input i <- ins]+ in let (st, actions) = run inputs+ announces = length (filter (== Announce) actions)+ in announces <= 1 .&&. st.announced === (announces == 1)++ prop "disconnects at most once" $ \(ins :: [Input]) ->+ let inputs = [i | Input i <- ins]+ in let isDisconnect = \case+ Disconnect _ -> True+ _ -> False+ in length (filter isDisconnect (snd (run inputs))) <= 1++ prop "is online exactly when announced and not disconnecting" $ \(ins :: [Input]) ->+ let inputs = [i | Input i <- ins]+ in let (st, _) = run inputs+ disconnecting = case st.stage of+ Disconnecting _ -> True+ _ -> False+ in isOnline st === (st.announced && not disconnecting)
+ test/unit/Haskoin/Node/PeerMgrSpec.hs view
@@ -0,0 +1,228 @@+-- | Full node connected to scripted remote peers over in-memory connections.+module Haskoin.Node.PeerMgrSpec (spec) where++import Control.Monad (forever, void, when)+import Data.Time.Clock (diffUTCTime, getCurrentTime)+import Haskoin+import Haskoin.Node+import Haskoin.Node.Test+import Network.Socket (SockAddr (..))+import System.Random (randomIO)+import Test.Hspec+import UnliftIO+import UnliftIO.Concurrent (threadDelay)++-- | Node connected to a single remote running the given script. Each+-- connection attempt runs the script again with the attempt number.+withScriptedNode ::+ Network ->+ (NodeConfig -> NodeConfig) ->+ (Int -> Remote -> IO ()) ->+ (TestNode -> IO a) ->+ IO a+withScriptedNode net modify script f = do+ attempts <- newTVarIO (0 :: Int)+ let connect :: SockAddr -> WithConnection+ connect _ g = do+ n <- atomically $ stateTVar attempts (\n -> (n + 1, n + 1))+ memoryConnection net (script n) g+ withTestNode net ["127.0.0.1:1"] connect modify f++-- | Remote that sends the version and then runs the handler on each+-- message.+handshakeThen :: Version -> (Message -> IO ()) -> Remote -> IO ()+handshakeThen v react r = do+ remoteSend r (MVersion v)+ let go =+ remoteReceive r >>= \case+ Nothing -> return ()+ Just m -> react m >> go+ go++noPeerConnectedWithin :: Double -> TestNode -> IO ()+noPeerConnectedWithin s t =+ expectNothingWithin s "peer connection" t.events $ \case+ PeerEvent (PeerConnected _) -> Just ()+ _ -> Nothing++spec :: Spec+spec = do+ let net = bchRegTest+ t0 <- runIO $ subtract 3000 <$> nowTimestamp+ let main = mkBlocks net 0 net.genesisHeader t0 2500++ describe "node handshake" $ do+ it "connects to a full node" $ do+ blocks <- newTVarIO []+ withScriptedNode net id (const (honestRemote blocks)) $ \t -> do+ p <- waitForPeer t+ Just o <- getOnlinePeer t.node.peerMgr p+ fmap (.version) o.version `shouldBe` Just (fromIntegral myVersion)+ o.online `shouldBe` True+ map (.mailbox) <$> getPeers t.node.peerMgr `shouldReturn` [p]++ it "rejects a peer that is not a full node" $ do+ v <- remoteVersion net 0 =<< randomIO+ withScriptedNode net id (\_ -> handshakeThen v (const (return ()))) $+ noPeerConnectedWithin 2++ it "rejects a connection to itself" $ do+ -- reply with the nonce from our own version message+ let script _ r =+ remoteReceive r >>= \case+ Just (MVersion ours) -> do+ v <- remoteVersion net nodeNetwork ours.nonce+ remoteSend r (MVersion v)+ remoteSend r MVerAck+ void (remoteReceive r)+ _ -> return ()+ withScriptedNode net id script $ noPeerConnectedWithin 2++ it "disconnects a peer that sends its version twice" $ do+ v <- remoteVersion net nodeNetwork =<< randomIO+ let script n r = handshakeThen v (react n r) r+ react n r = \case+ MVersion _ -> do+ remoteSend r MVerAck+ when (n == 1) $ remoteSend r (MVersion v)+ _ -> return ()+ withScriptedNode net id script $ \t -> do+ p <- waitForPeer t+ waitForDisconnect t p++ it "disconnects a peer that does not complete the handshake" $ do+ remotes <- newEmptyMVar+ let script _ r = do+ putMVar remotes r+ void (remoteReceive r) -- our version+ void (remoteReceive r) -- wait until closed+ withScriptedNode net (setTimeout 1) script $ \t -> do+ r <- within 10 "connection" (readMVar remotes)+ eventually 10 "connection closed" (readTVarIO r.closed)+ noPeerConnectedWithin 0.5 t++ it "disconnects a peer that chatters without completing the handshake" $ do+ remotes <- newEmptyMVar+ let script _ r = do+ void (tryPutMVar remotes r)+ -- never sends a version, but keeps talking+ forever $ remoteSend r (MPing (Ping 1)) >> threadDelay 200000+ withScriptedNode net (setTimeout 1) script $ \t -> do+ r <- within 10 "connection" (readMVar remotes)+ eventually 10 "connection closed" (readTVarIO r.closed)+ noPeerConnectedWithin 0.5 t++ it "waits before retrying a peer that failed" $ do+ times <- newTVarIO []+ let script _ _ = getCurrentTime >>= \now -> atomically (modifyTVar' times (now :))+ withScriptedNode net id script $ \_ -> do+ eventually 20 "two attempts" $ (>= 2) . length <$> readTVarIO times+ -- the first failure waits two seconds; the connect loop alone could+ -- retry within a tenth of a second+ oldestFirst <- reverse <$> readTVarIO times+ case oldestFirst of+ first : second : _ ->+ (second `diffUTCTime` first) `shouldSatisfy` (>= 1.9)+ _ -> expectationFailure "fewer than two attempts"++ describe "node peer maintenance" $ do+ it "answers pings" $ do+ blocks <- newTVarIO []+ pongs <- newEmptyMVar+ let script _ r = do+ v <- remoteVersion net nodeNetwork =<< randomIO+ remoteSend r (MVersion v)+ remoteSend r (MPing (Ping 1234))+ let go =+ remoteReceive r >>= \case+ Nothing -> return ()+ Just (MPong (Pong n)) -> putMVar pongs n >> go+ Just m -> respondHonestly r blocks m >> go+ go+ withScriptedNode net id script $ \_ ->+ within 10 "pong" (takeMVar pongs) `shouldReturn` 1234++ it "disconnects a peer that stops responding" $ do+ v <- remoteVersion net nodeNetwork =<< randomIO+ let react r = \case+ MVersion _ -> remoteSend r MVerAck+ _ -> return () -- never answers pings+ script _ r = handshakeThen v (react r) r+ withScriptedNode net (setTimeout 2) script $ \t -> do+ p <- waitForPeer t+ waitForDisconnect t p++ it "disconnects a peer that has been connected for too long" $ do+ blocks <- newTVarIO []+ withScriptedNode+ net+ (setMaxPeerLife 1 . setTimeout 2)+ (const (honestRemote blocks))+ $ \t -> do+ p <- waitForPeer t+ waitForDisconnect t p++ it "reconnects after a peer disconnects" $ do+ blocks <- newTVarIO []+ let script n r+ | n == 1 = do+ v <- remoteVersion net nodeNetwork =<< randomIO+ remoteSend r (MVersion v)+ Just (MVersion _) <- remoteReceive r+ remoteSend r MVerAck+ Just MVerAck <- remoteReceive r+ return () -- close the connection+ | otherwise = honestRemote blocks r+ withScriptedNode net id script $ \t -> do+ p1 <- waitForPeer t+ waitForDisconnect t p1+ p2 <- waitForPeer t+ p2 `shouldNotBe` p1++ describe "node header sync" $ do+ it "syncs headers from a peer" $ do+ blocks <- newTVarIO main+ withScriptedNode net id (const (honestRemote blocks)) $ \t -> do+ bn <- receiveWithin 30 "synced" t.events $ \case+ ChainEvent (ChainSynced bn) -> Just bn+ _ -> Nothing+ headerHash bn.header `shouldBe` headerHash (last main).header++ it "follows blocks announced by inventory" $ do+ blocks <- newTVarIO (take 10 main)+ remotes <- newEmptyMVar+ let script _ r = putMVar remotes r >> honestRemote blocks r+ withScriptedNode net id script $ \t -> do+ r <- within 10 "remote" (readMVar remotes)+ _ <- waitForBest 30 t ((== 10) . (.height))+ atomically $ writeTVar blocks (take 109 main)+ let tip = headerHash (main !! 108).header+ remoteSend r (MInv (Inv [InvVector InvBlock tip.get]))+ void $ waitForBest 30 t ((== 109) . (.height))++ it "follows blocks announced by headers that do not connect" $ do+ blocks <- newTVarIO (take 10 main)+ remotes <- newEmptyMVar+ let script _ r = putMVar remotes r >> honestRemote blocks r+ withScriptedNode net id script $ \t -> do+ r <- within 10 "remote" (readMVar remotes)+ p <- waitForPeer t+ _ <- waitForBest 30 t ((== 10) . (.height))+ atomically $ writeTVar blocks (take 60 main)+ remoteSend r (headersMessage [(main !! 59).header])+ _ <- waitForBest 30 t ((== 60) . (.height))+ expectNothingWithin 0.5 "disconnection" t.events $ \case+ PeerEvent (PeerDisconnected p') | p == p' -> Just ()+ _ -> Nothing++ it "downloads blocks from a peer" $ do+ blocks <- newTVarIO main+ withScriptedNode net id (const (honestRemote blocks)) $ \t -> do+ p <- waitForPeer t+ let want = take 3 (drop 100 main)+ getBlocks net 10 p (map (headerHash . (.header)) want)+ `shouldReturn` Just want+ let unknown = case mkBlocks net 7 net.genesisHeader t0 1 of+ b : _ -> headerHash b.header+ [] -> error "no block"+ getBlocks net 10 p [unknown] `shouldReturn` Nothing
+ test/unit/Haskoin/Node/PeerSpec.hs view
@@ -0,0 +1,246 @@+-- | Peer process talking to a scripted remote over an in-memory connection.+module Haskoin.Node.PeerSpec (spec) where++import Control.Concurrent.Async (ExceptionInLinkedThread (..))+import Control.Monad (forM_)+import Data.ByteString qualified as B+import Data.Serialize (putWord32le, runPut)+import Data.Word (Word32)+import Haskoin+import Haskoin.Node+import Haskoin.Node.Peer.Lock (Lock (..))+import Haskoin.Node.Test+import NQE+import Test.HUnit (assertFailure)+import Test.Hspec hiding (pending)+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (Positive (..), ioProperty)+import UnliftIO+import UnliftIO.Concurrent (threadDelay)++data PeerTest = PeerTest+ { peer :: !Peer,+ events :: !(Inbox PeerEvent),+ remote :: !Remote,+ thread :: !(Async ())+ }++-- | Run a peer process connected to a remote that the test drives.+withPeerTest :: Network -> (PeerTest -> IO a) -> IO a+withPeerTest net f = do+ rv <- newEmptyMVar+ let script r = putMVar rv r >> forever_wait+ forever_wait = threadDelay maxBound >> forever_wait+ withTestLogger $ \logger ->+ withPublisher $ \pub ->+ withSubscription pub $ \events -> do+ (inbox, mailbox) <- newMailbox+ busy <- newTVarIO Free+ pending <- newTVarIO 0+ let cfg =+ PeerConfig+ { pub = pub,+ net = net,+ label = "remote",+ connect = memoryConnection net script,+ logger = logger+ }+ p = wrapPeer cfg busy pending mailbox+ withAsync (peer cfg p inbox) $ \thread -> do+ remote <- within 5 "connection" (readMVar rv)+ f PeerTest {peer = p, events, remote, thread}++-- | Next message received by the peer process from the remote.+received :: PeerTest -> IO Message+received t =+ receiveWithin 5 "message from remote" t.events $ \case+ PeerMessage p m | p == t.peer -> Just m+ _ -> Nothing++-- | The peer process must stop in time with an error that satisfies the+-- predicate.+expectFailure :: PeerTest -> (PeerException -> Bool) -> IO ()+expectFailure t ok =+ within 5 "peer process to stop" (waitCatch t.thread) >>= \case+ Right () -> assertFailure "Peer process stopped without error"+ Left e -> case peerException e of+ Just pe | ok pe -> return ()+ _ -> assertFailure ("Unexpected error: " <> show e)+ where+ peerException e =+ case fromException e of+ Just (ExceptionInLinkedThread _ inner) -> fromException inner+ Nothing -> fromException e++-- | The peer process must stop in time without error.+expectStopped :: PeerTest -> IO ()+expectStopped t =+ within 5 "peer process to stop" (waitCatch t.thread) >>= \case+ Right () -> return ()+ Left e -> assertFailure ("Unexpected error: " <> show e)++expectRunning :: PeerTest -> IO ()+expectRunning t =+ poll t.thread >>= \case+ Nothing -> return ()+ Just r -> assertFailure $ "Peer process stopped: " <> show r++-- | Chop a byte string into pieces of the given sizes.+chunks :: [Int] -> B.ByteString -> [B.ByteString]+chunks _ b | B.null b = []+chunks [] b = [b]+chunks (n : ns) b = let (x, y) = B.splitAt (max 1 n) b in x : chunks ns y++-- | Valid message with the payload length field replaced.+withLength :: Word32 -> B.ByteString -> B.ByteString+withLength len bs = B.take 16 bs <> runPut (putWord32le len) <> B.drop 20 bs++spec :: Spec+spec = do+ let net = btcRegTest+ t0 <- runIO nowTimestamp+ let blocks = mkBlocks net 0 net.genesisHeader t0 5+ b0 = blocks !! 0+ bhash :: Block -> BlockHash+ bhash b = headerHash b.header+ block_iv :: Block -> InvVector+ block_iv b = InvVector InvWitnessBlock (bhash b).get+ describe "peer message framing" $ do+ it "parses messages split across arbitrary chunks" $+ withPeerTest net $ \t -> do+ let bs = encodeMessage net (MPing (Ping 42))+ forM_ (chunks (repeat 1) bs) (remoteSendRaw t.remote)+ received t `shouldReturn` MPing (Ping 42)++ prop "parses messages regardless of how they are chunked" $+ \(sizes :: [Positive Int]) -> ioProperty $+ withPeerTest net $ \t -> do+ let msgs = [MPing (Ping 1), MBlock b0, MPong (Pong 2)]+ bs = B.concat (map (encodeMessage net) msgs)+ forM_ (chunks (map getPositive sizes) bs) (remoteSendRaw t.remote)+ mapM (const (received t)) msgs `shouldReturn` msgs++ it "parses several messages sent at once" $+ withPeerTest net $ \t -> do+ let msgs = [MPing (Ping 1), MVerAck, MPing (Ping 2)]+ remoteSendRaw t.remote (B.concat (map (encodeMessage net) msgs))+ mapM (const (received t)) msgs `shouldReturn` msgs++ it "accepts unknown commands" $ withPeerTest net $ \t -> do+ remoteSend t.remote (MOther "xyzzy" "\1\2\3")+ remoteSend t.remote (MPing (Ping 7))+ received t `shouldReturn` MOther "xyzzy" "\1\2\3"+ received t `shouldReturn` MPing (Ping 7)++ it "sends messages to the remote" $ withPeerTest net $ \t -> do+ MPing (Ping 9) `sendMessage` t.peer+ remoteReceiveMatch 5 "ping" t.remote Just `shouldReturn` MPing (Ping 9)++ describe "peer protocol violations" $ do+ it "disconnects on wrong network magic" $ withPeerTest net $ \t -> do+ remoteSendRaw t.remote (encodeMessage btc (MPing (Ping 1)))+ expectFailure t $ \case+ WrongNetwork m -> m == btc.magic+ _ -> False++ it "disconnects on oversized message" $ withPeerTest net $ \t -> do+ -- send only the header: the peer must not wait for the payload+ let bs = encodeMessage net (MBlock b0)+ remoteSendRaw t.remote (B.take 24 (withLength (33 * 1024 * 1024) bs))+ expectFailure t $ \case+ MessageTooLarge MCBlock _ -> True+ _ -> False++ it "disconnects on bad checksum" $ withPeerTest net $ \t -> do+ let bs = encodeMessage net (MPing (Ping 1))+ bad = B.take 20 bs <> "\0\0\0\0" <> B.drop 24 bs+ remoteSendRaw t.remote bad+ expectFailure t $ \case+ InvalidPayload MCPing _ -> True+ _ -> False++ it "disconnects on malformed payload" $ withPeerTest net $ \t -> do+ -- a ping payload is eight bytes, send three with a valid checksum+ let payload = "\1\2\3"+ bs = encodeMessage net (MOther "ping" payload)+ remoteSendRaw t.remote bs+ expectFailure t $ \case+ InvalidPayload MCPing _ -> True+ _ -> False++ it "disconnects when the remote closes the connection" $+ withPeerTest net $ \t -> do+ remoteClose t.remote+ expectFailure t $ \case+ ConnectionClosed -> True+ _ -> False++ it "disconnects when told to" $ withPeerTest net $ \t -> do+ expectRunning t+ killPeer t.peer+ expectStopped t+ eventually 5 "connection closed" (readTVarIO t.remote.closed)++ describe "peer data requests" $ do+ let serve :: PeerTest -> (Message -> [Message]) -> IO a -> IO a+ serve t f =+ withAsync (remote_loop t f) . const+ remote_loop :: PeerTest -> (Message -> [Message]) -> IO ()+ remote_loop t f =+ remoteReceive t.remote >>= \case+ Nothing -> return ()+ Just m -> mapM_ (remoteSend t.remote) (f m) >> remote_loop t f+ pong (MPing (Ping n)) = [MPong (Pong n)]+ pong _ = []+ honest = \case+ MGetData (GetData ivs) ->+ [MBlock b | iv <- ivs, b <- blocks, block_iv b == iv]+ m -> pong m++ it "downloads blocks" $ withPeerTest net $ \t -> serve t honest $ do+ r <- getBlocks net 5 t.peer (map bhash (take 3 blocks))+ r `shouldBe` Just (take 3 blocks)++ it "ignores unrelated messages while downloading" $+ withPeerTest net $ \t -> do+ let noisy = \case+ MGetData (GetData ivs) ->+ concat+ [ [MBlock b, MPing (Ping 5), MInv (Inv [block_iv b])]+ | iv <- ivs,+ b <- blocks,+ block_iv b == iv+ ]+ m -> pong m+ serve t noisy $ do+ r <- getBlocks net 5 t.peer (map bhash (take 3 blocks))+ r `shouldBe` Just (take 3 blocks)++ it "fails when a requested block is not found" $+ withPeerTest net $ \t -> do+ let f = \case+ MGetData (GetData (iv : _)) -> [MNotFound (NotFound [iv])]+ m -> pong m+ serve t f $+ getBlocks net 5 t.peer [bhash b0] `shouldReturn` Nothing++ it "times out when the peer does not answer" $+ withPeerTest net $ \t ->+ getBlocks net 1 t.peer [bhash b0] `shouldReturn` Nothing++ it "downloads transactions" $ withPeerTest net $ \t -> do+ let txs = concatMap (.txs) blocks+ tx_iv tx = InvVector InvWitnessTx (txHash tx).get+ f = \case+ MGetData (GetData ivs) ->+ [MTx tx | iv <- ivs, tx <- txs, tx_iv tx == iv]+ m -> pong m+ serve t f $+ getTxs net 5 t.peer (map txHash (take 2 txs))+ `shouldReturn` Just (take 2 txs)++ it "pings the peer" $ withPeerTest net $ \t -> do+ serve t pong $ pingPeer 5 t.peer `shouldReturn` True++ it "reports an unanswered ping" $ withPeerTest net $ \t ->+ pingPeer 1 t.peer `shouldReturn` False
+ test/unit/Haskoin/NodeSpec.hs view
@@ -0,0 +1,525 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}++module Haskoin.NodeSpec (spec) where++import Conduit+import Control.Monad (forM_, forever, replicateM)+import Data.ByteString (ByteString)+import Data.ByteString qualified as B+import Data.ByteString.Base64 (decodeBase64Lenient)+import Data.Default (def)+import Data.Either (fromRight)+import Data.List (find)+import Data.Maybe (listToMaybe, mapMaybe)+import Data.Serialize (decode, get, put, 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 (SockAddr (..), tupleToHostAddress, tupleToHostAddress6)+import System.Random (randomIO)+import Test.Hspec+import UnliftIO+import UnliftIO.Concurrent (threadDelay)++data TestNode = TestNode+ { testMgr :: PeerMgr,+ testChain :: Chain,+ nodeEvents :: Inbox NodeEvent,+ testDB :: R.DB,+ -- | how many getheaders messages the mock peer has received+ getHeadersCount :: TVar Int+ }++-- | 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 ::+ React ->+ TVar Int ->+ Network ->+ NetworkAddress ->+ SockAddr ->+ WithConnection+dummyPeerConnect react count net ad sa f = do+ r <- newInbox+ s <- newInbox+ let s' = inboxToMailbox s+ withAsync (go r s') $ \_ -> do+ let o = awaitForever (`send` r)+ i = forever (receive s >>= yield)+ f (Conduits i o) :: IO ()+ where+ go :: Inbox ByteString -> Mailbox ByteString -> IO ()+ go r s = do+ nonce <- randomIO+ now <- round <$> getPOSIXTime+ let rmt = NetworkAddress 0 (sockToHostAddress sa)+ ver = buildVersion net nonce 0 ad rmt now+ runPut (putMessage net (MVersion ver)) `send` s+ runConduit $+ forever (receive r >>= yield)+ .| inc+ .| iterMC count_getheaders+ .| concatMapC react+ .| outc+ .| awaitForever (`send` s)+ outc = mapMC $ return . either id (runPut . putMessage net)+ count_getheaders = \case+ MGetHeaders _ -> atomically (modifyTVar' count (+ 1))+ _ -> return ()+ inc =+ forever $ do+ x <- takeCE 24 .| foldC+ y <- case decode x of+ Left _ -> error "Dummy peer not decode message header"+ Right (MessageHeader _ _ len _) ->+ takeCE (fromIntegral len) .| foldC+ case runGet (getMessage net) $ x `B.append` y of+ Left e ->+ error $+ "Dummy peer could not decode payload: " <> show e+ Right m -> yield m++mockPeerReact :: [Block] -> Message -> [Message]+mockPeerReact _ (MPing (Ping n)) = [MPong (Pong n)]+mockPeerReact _ (MVersion _) = [MVerAck]+mockPeerReact blocks (MGetHeaders (GetHeaders _ _hs _)) = [MHeaders (Headers hs')]+ where+ f b = (b.header, (VarInt . fromIntegral . length) b.txs)+ hs' = map f blocks+mockPeerReact blocks (MGetData (GetData ivs)) = mapMaybe f ivs+ where+ f (InvVector InvBlock h) = MBlock <$> find (l h) blocks+ f _ = Nothing+ l h b = headerHash b.header == BlockHash h+mockPeerReact _ _ = []++spec :: Spec+spec = do+ let net = bchRegTest+ describe "peer manager on test network" $ do+ it "connects to a peer" $+ withTestNode net "connect-one-peer" $ \TestNode {..} -> do+ p <- waitForPeer nodeEvents+ Just o <- getOnlinePeer testMgr p+ fmap (.version) o.version `shouldSatisfy` maybe False (>= 70002)+ it "downloads some blocks" $+ withTestNode net "get-blocks" $ \TestNode {..} -> do+ let h1 =+ "3094ed3592a06f3d8e099eed2d9c1192329944f5df4a48acb29e08f12cfbb660"+ h2 =+ "0c89955fc5c9f98ecc71954f167b938138c90c6a094c4737f2e901669d26763f"+ p <- waitForPeer nodeEvents+ pbs <- getBlocks net 10 p [h1, h2]+ (b1, b2) <- case pbs of+ Just [b1, b2] -> return (b1, b2)+ _ -> fail ("Expected two blocks, got " <> show pbs)+ headerHash b1.header `shouldBe` h1+ headerHash b2.header `shouldBe` h2+ let ths b = map txHash b.txs+ testMerkle b = buildMerkleRoot (ths b) `shouldBe` Right b.header.merkle+ testMerkle b1+ testMerkle b2+ it "downloads a block larger than 2 MiB" $ do+ -- On Bitcoin Cash a block message may be up to twice the block size,+ -- while every other message is limited to 2 MiB (2097152 bytes).+ let script = B.replicate 3000000 0x6a+ tx = Tx 1 [TxIn nullOutPoint B.empty maxBound] [TxOut 0 script] [] 0+ big = case allBlocks of+ firstBlock : _ -> Block firstBlock.header [tx]+ [] -> error "no test blocks"+ withTestNodeBlocks [big] net "big-block" $ \TestNode {..} -> do+ p <- waitForPeer nodeEvents+ pbs <- getBlocks net 10 p [headerHash big.header]+ let scriptSizes =+ [[B.length o.script | o <- t.outputs] | b <- concat pbs, t <- b.txs]+ scriptSizes `shouldBe` [[3000000]]+ it "builds a version message with the given height and time" $ do+ let local = NetworkAddress nodeNetwork (sockToHostAddress (SockAddrInet 0 0))+ remote = NetworkAddress 0 (sockToHostAddress (SockAddrInet 8333 0x08080808))+ v = buildVersion net 7 650000 local remote 1700000000+ v.version `shouldBe` 70012+ v.startHeight `shouldBe` 650000+ v.timestamp `shouldBe` 1700000000+ 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 announces a tx message larger than 2 MiB" $ do+ -- Only the header is sent, with a length of 2 MiB plus one byte:+ -- 2 × 1024 × 1024 + 1 = 2097153. The node must refuse it there,+ -- without waiting for the payload.+ let oversized =+ runPut . put $+ MessageHeader net.magic MCTx 2097153 (checkSum32 B.empty)+ withTestNodeOpts+ (defOpts allBlocks) {react = reactAfterVerAck [Left oversized]}+ net+ "oversized-tx"+ $ \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+ -- peer does when the node already has its whole chain. The node must+ -- take that as the end of the sync instead of asking again.+ withTestNodeBlocks [] net "no-headers" $ \TestNode {..} -> do+ _ <- waitForPeer nodeEvents+ atomically $ readTVar getHeadersCount >>= checkSTM . (>= 1)+ threadDelay (1000 * 1000)+ readTVarIO getHeadersCount `shouldReturn` 1+ it "reports the headers as synced when the best header has the minimum chain work" $ do+ -- Every regtest header has a work of 2. Genesis and one header on top+ -- of it have 2 + 2 = 4, which is the minimum here.+ fresh <- freshHeader net+ let net4 = net {minimumChainWork = 4}+ withTestNodeBlocks [Block fresh []] net4 "min-work-reached" $+ \TestNode {..} -> do+ receiveMatch nodeEvents $ \case+ ChainEvent (ChainBestBlock bn) | bn.height == 1 -> Just ()+ _ -> Nothing+ chainSync testChain+ chainIsSynced testChain `shouldReturn` True+ it "does not report the headers as synced below the minimum chain work" $ do+ -- Same chain with a work of 2 + 2 = 4, one short of a minimum of 5.+ fresh <- freshHeader net+ let net5 = net {minimumChainWork = 5}+ withTestNodeBlocks [Block fresh []] net5 "min-work-missing" $+ \TestNode {..} -> do+ receiveMatch nodeEvents $ \case+ ChainEvent (ChainBestBlock bn) | bn.height == 1 -> Just ()+ _ -> Nothing+ chainSync testChain+ chainIsSynced testChain `shouldReturn` False+ it "syncs some headers" $+ withTestNode net "connect-sync" $ \TestNode {..} -> do+ let bh =+ "3bfa0c6da615fc45aa44ddea6854ac19d16f3ca167e0e21ac2cc262a49c9b002"+ ah =+ "7dc835a78a55fa76f9184dc4f6663a73e418c7afec789c5ae25e432fd7fc8467"+ bn <-+ receiveMatch nodeEvents $ \case+ ChainEvent (ChainBestBlock bn)+ | bn.height > 0 -> Just bn+ _ -> Nothing+ bb <- chainGetBest testChain+ bb.height `shouldSatisfy` (== 15)+ an <-+ maybe (error "No ancestor found") return+ =<< chainGetAncestor testChain 10 bn+ headerHash bn.header `shouldBe` bh+ headerHash an.header `shouldBe` ah+ it "downloads some block parents" $+ withTestNode net "parents" $ \TestNode {..} -> do+ let hs =+ [ "52e886df7b166d961ac2d3d2d561d806325d51a609dc0a5d9d5fcb65d47906d7",+ "2537a081b9e2b24d217fac2886f387758cb3aa4e4956b3be7ed229bafbb71b0f",+ "7c72f306215a296f9714320a497b1f2cb5f9b99f162d7e04333c243fac9a54d8"+ ]+ [_, bn] <-+ replicateM 2 $+ receiveMatch nodeEvents $ \case+ ChainEvent (ChainBestBlock bn) -> Just bn+ _ -> Nothing+ bn.height `shouldBe` 15+ ps <- chainGetParents testChain 12 bn+ length ps `shouldBe` 3+ 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) disconnected >>= \case+ Just () -> return ()+ Nothing -> expectationFailure "peer was not disconnected"+ where+ disconnected =+ 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)++-- | Valid header on top of genesis that is dated now. The node only counts+-- its headers as synced when the best one is less than two hours old, and+-- the headers of the other fixtures are years old.+freshHeader :: Network -> IO BlockHeader+freshHeader net = do+ now <- round <$> getPOSIXTime+ let genesis = net.genesisHeader+ return $+ mineBlock+ net+ 0+ genesis {version = 4, prev = headerHash genesis, timestamp = now}++waitForPeer :: Inbox NodeEvent -> IO Peer+waitForPeer inbox =+ receiveMatch inbox $ \case+ PeerEvent (PeerConnected p) -> Just p+ _ -> Nothing++withLifted :: ((a -> IO b) -> IO b) -> (a -> IO b) -> IO b+withLifted w m = withRunInIO $ \io -> w (io . m)++withTestNode ::+ Network -> String -> (TestNode -> IO a) -> IO a+withTestNode = withTestNodeBlocks allBlocks++-- | Like 'withTestNode', with the mock peer serving these blocks.+withTestNodeBlocks ::+ [Block] -> Network -> String -> (TestNode -> IO a) -> IO a+withTestNodeBlocks blocks = withTestNodeOpts (defOpts blocks)++withTestNodeOpts ::+ TestOpts -> Network -> String -> (TestNode -> IO a) -> IO a+withTestNodeOpts opts net str f =+ withSystemTempDirectory ("haskoin-node-test-" <> str <> "-") $ \w ->+ withPublisher $ \pub ->+ withSubscription pub $ \sub ->+ withLifted (R.withDBCF w cfg cols) $ \db -> do+ count <- newTVarIO 0+ opts.setup db+ withNode (nodeConfig count pub db) $ \(Node mgr ch) ->+ f+ TestNode+ { testMgr = mgr,+ testChain = ch,+ nodeEvents = sub,+ testDB = db,+ getHeadersCount = count+ }+ where+ nodeConfig count pub db =+ NodeConfig+ { maxPeers = 20,+ db = db,+ cf = listToMaybe (R.columnFamilies db),+ -- IPv4, so that the mock peer is reachable without IPv6+ peers = ["127.0.0.1:17486"],+ discover = False,+ address = NetworkAddress 0 anywhere,+ net = net,+ pub = pub,+ timeout = opts.timeout,+ maxPeerLife = 48 * 3600,+ connect =+ dummyPeerConnect+ opts.react+ count+ net+ (NetworkAddress nodeNetwork anywhere),+ logger = Logger {level = LevelError, source = "", push = \_ -> return ()}+ }+ anywhere = sockToHostAddress (SockAddrInet 0 0)+ cfg = def {R.createIfMissing = True, R.errorIfExists = True}+ cols = [("node", def)]++allBlocks :: [Block]+allBlocks =+ fromRight (error "Could not decode blocks") $+ runGet f (decodeBase64Lenient allBlocksBase64)+ where+ f = mapM (const get) [(1 :: Int) .. 15]++allBlocksBase64 :: ByteString+allBlocksBase64 =+ "AAAAIAYibkYRGgtZyq8SYEPrW78ow086XjMqH8eytzzxiJEPakRJalmWTFwdvzNuH8fHLZEjn+4N\+ \FNMANdB7ez2M4a3TFbNe//9/IAMAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\+ \AAAAAP////8MUQEBCC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf\+ \1OHjgkqxnwEYkK9zrAAAAAAAAAAge0RDjOrqVayGUoQsbNTJcTXUM+psaHpmuiFy6hwo2T8yn0CL\+ \7WDJw9hxl1kf5c4JySq3WJF8OPsoguzF7mXH3tQVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAA\+ \AAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wxSAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOym\+ \QKMx7MqzjsE+lp+i7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACCKlhzDaFkrsmO2FhmeQS9ONS8D\+ \QsU4H97yNxVhyIXYJuG3a9cyQpdeETjCQ6JybgkwI0OOfa4eYazf7WWI5UAk1BWzXv//fyAEAAAA\+ \AQIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFMBAQgvRUIzMi4wL///\+ \//8BAPIFKgEAAAAjIQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAIP/S\+ \XiIJZqvUyBY90z72dv6+/GG50R3vc3UAK8AHP89wChmkVP6nefjOt+sNyhbKk9zia47F08oTNtC0\+ \OG1zyuXVFbNe//9/IAEAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//\+ \//8MVAEBCC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqx\+ \nwEYkK9zrAAAAAAAAAAgeQtE1s3YV/uS2jUouo3S9DJAVf5OGk+Nyx+No1mPH24b5JCkr/tSP0E/\+ \NYVkVcE0ZHxbO/fu5wOd+8VolvPQYtUVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAA\+ \AAAAAAAAAAAAAAAAAAAA/////wxVAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7Mqz\+ \jsE+lp+i7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACBgtvss8QiesqxISt/1RJkykhGcLe2eCY49\+ \b6CSNe2UMOVYGZ++uRCKvaJ2+jo7akr7XsdXCYSAmuw6DwSO8lvF1RWzXv//fyAAAAAAAQIAAAAB\+ \AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFYBAQgvRUIzMi4wL/////8BAPIF\+ \KgEAAAAjIQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAID92Jp1mAeny\+ \N0dMCWoMyTiBk3sWT5VxzI75ycVflYkMCnXLFhuwrMdBbZmXJinAJBUpN7BV0XvlM2PRmb7HQebV\+ \FbNe//9/IAEAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MVwEB\+ \CC9FQjMyLjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9z\+ \rAAAAAAAAAAgxEgEkhjf5p+ql8dETmdSCdCdk+vB26+V2SGLEuE1+kA1acGCdQoQBqec8P/knItJ\+ \M213OIrDX6U5IB6fgIas7dYVs17//38gAQAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\+ \AAAAAAAAAAAA/////wxYAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i\+ \7WOOwd/U4eOCSrGfARiQr3OsAAAAAAAAACDku4EB5X7htWpHg+aMzzW1AABttpNQTew7K3Aj2fh/\+ \OuOCPhJApmcXq5o42tkksFSuhYvcfqaSHCuuFgjo6ohz1hWzXv//fyAAAAAAAQIAAAABAAAAAAAA\+ \AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFkBAQgvRUIzMi4wL/////8BAPIFKgEAAAAj\+ \IQME7KZAozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAIKWpAhOWbkEN9vWf1uCu\+ \eXtVOZIE9V1OE87iC+H9atBRtY4LPgaWUSVMNh9SeZK1NViIFMklbjsfqYiC4eA/VuLWFbNe//9/\+ \IAAAAAABAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MWgEBCC9FQjMy\+ \LjAv/////wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9zrAAAAAAA\+ \AAAgZ4T81y9DXuJanHjsr8cY5HM6ZvbETRj5dvpViqc1yH0oN9OOruaO5mjdITJwweVCzjSQ5Wsl\+ \vSOKaKvEX5j9l9YVs17//38gAAAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\+ \AAAA/////wxbAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i7WOOwd/U\+ \4eOCSrGfARiQr3OsAAAAAAAAACCV3J2A3qneSJ7Q/RuF8OPd8O1izIXvKElR/xg/+InGNEafu0Ul\+ \3VYJR93zbAQuns9hUfAhA8MTBPk8bbDabDfo1hWzXv//fyAAAAAAAQIAAAABAAAAAAAAAAAAAAAA\+ \AAAAAAAAAAAAAAAAAAAAAAAAAAD/////DFwBAQgvRUIzMi4wL/////8BAPIFKgEAAAAjIQME7KZA\+ \ozHsyrOOwT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAAAAAAINcGedRly1+dXQrcCaZRXTIG2GHV\+ \0tPCGpZtFnvfhuhSx8d3Azdv/MXRJgsb56qqmD5gsXiWUdi7ia7wsBZVylvWFbNe//9/IAEAAAAB\+ \AgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8MXQEBCC9FQjMyLjAv////\+ \/wEA8gUqAQAAACMhAwTspkCjMezKs47BPpafou1jjsHf1OHjgkqxnwEYkK9zrAAAAAAAAAAgDxu3\+ \+7op0n6+s1ZJTqqzjHWH84YorH8hTbLiuYGgNyWIkhaj0zR7Vc+fSRm4UYUaPsefRhq3fUt8glyS\+ \D8P/5tcVs17//38gAwAAAAECAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////\+ \/wxeAQEIL0VCMzIuMC//////AQDyBSoBAAAAIyEDBOymQKMx7MqzjsE+lp+i7WOOwd/U4eOCSrGf\+ \ARiQr3OsAAAAAAAAACDYVJqsPyQ8MwR+LRafufm1LB97SQoyFJdvKVohBvNyfD4/FxT2i0rlYQcS\+ \TQAvTnehousK2P8T9c0qx4Yj72lT1xWzXv//fyAAAAAAAQIAAAABAAAAAAAAAAAAAAAAAAAAAAAA\+ \AAAAAAAAAAAAAAAAAAD/////DF8BAQgvRUIzMi4wL/////8BAPIFKgEAAAAjIQME7KZAozHsyrOO\+ \wT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAA"
+ test/unit/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}