packages feed

haskoin-node 0.11.3 → 0.12.0

raw patch · 8 files changed

+250/−96 lines, 8 filesdep +base64

Dependencies added: base64

Files

CHANGELOG.md view
@@ -4,6 +4,10 @@ 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). +## 0.12.0+### Changed+- Add a test suite that simulates network instead of connecting to real one.+ ## 0.11.3 ### Changed - Revert including multiline decoding error text in logs.
haskoin-node.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 68c2f796296143516ef7f719674817c9233b3dec269de968a1ed75bc09da710d+-- hash: 5146a554596617bed514f4367a6d203381cfbe8e7848f1bd4835d92fa9727df9  name:           haskoin-node-version:        0.11.3+version:        0.12.0 synopsis:       Haskoin Node P2P library for Bitcoin and Bitcoin Cash description:    Bitcoin and Bitcoin Cash peer-to-peer protocol library featuring headers-first synchronisation. category:       Bitcoin, Finance, Network@@ -29,16 +29,17 @@ library   exposed-modules:       Haskoin.Node+  other-modules:       Haskoin.Node.Chain       Haskoin.Node.Common       Haskoin.Node.Manager       Haskoin.Node.Peer-  other-modules:       Paths_haskoin_node   hs-source-dirs:       src   build-depends:       base >=4.9 && <5+    , base64     , bytestring     , cereal     , conduit@@ -74,6 +75,7 @@   build-depends:       HUnit     , base >=4.9 && <5+    , base64     , bytestring     , cereal     , conduit
src/Haskoin/Node.hs view
@@ -21,6 +21,8 @@     , Peer     , Chain     , PeerManager+    , WithConnection+    , Conduits(..)     , ChainMessage(..)     , PeerManagerMessage(..)     , PeerMessage(..)@@ -47,7 +49,9 @@     , chainGetSplitBlock     , chainBlockMain     , chainIsSynced+    , withConnection     , myVersion+    , buildVersion     ) where  import           Control.Monad.Logger (MonadLoggerIO)@@ -55,13 +59,14 @@                                        Message (..)) import           Haskoin.Node.Chain   (chain) import           Haskoin.Node.Common  (Chain, ChainConfig (..), ChainEvent (..),-                                       ChainMessage (..), Host, HostPort,-                                       NodeConfig (..), NodeEvent (..),-                                       OnlinePeer (..), Peer, PeerEvent (..),-                                       PeerException (..), PeerManager,-                                       PeerManagerConfig (..),+                                       ChainMessage (..), Conduits (..), Host,+                                       HostPort, NodeConfig (..),+                                       NodeEvent (..), OnlinePeer (..), Peer,+                                       PeerEvent (..), PeerException (..),+                                       PeerManager, PeerManagerConfig (..),                                        PeerManagerMessage (..),-                                       PeerMessage (..), Port, chainBlockMain,+                                       PeerMessage (..), Port, WithConnection,+                                       buildVersion, chainBlockMain,                                        chainGetAncestor, chainGetBest,                                        chainGetBlock, chainGetParents,                                        chainGetSplitBlock, chainIsSynced,@@ -69,7 +74,7 @@                                        managerGetPeers, managerPeerText,                                        myVersion, peerGetBlocks,                                        peerGetPublisher, peerGetTxs,-                                       sendMessage)+                                       sendMessage, withConnection) import           Haskoin.Node.Manager (peerManager) import           NQE                  (Inbox, inboxToMailbox, newInbox, sendSTM) import           UnliftIO             (MonadUnliftIO, link, withAsync)@@ -108,6 +113,7 @@                 , peerManagerEvents = mgr_events                 , peerManagerTimeout = nodeConfTimeout cfg                 , peerManagerTooOld = nodeConfPeerOld cfg+                , peerManagerConnect = nodeConfConnect cfg                 }     withAsync (peerManager mgr_config mgr_inbox) $ \mgr_async -> do         link mgr_async
src/Haskoin/Node/Chain.hs view
@@ -142,7 +142,8 @@     getSyncingPeer >>= \case         Nothing -> do             nextPeer >>= \case-                Nothing -> return ()+                Nothing ->+                    $(logDebugS) "Chain" "No more peers to sync headers against"                 Just p -> syncPeer p         Just _ -> return () @@ -153,19 +154,25 @@  syncPeer :: MonadChain m => Peer -> m () syncPeer p = do+    mgr <- asks (chainConfManager . myReader)     bb <-         chainSyncingPeer >>= \case-            Just ChainSync {chainSyncPeer = p', chainHighest = Just g}-                | p == p' -> return g+            Just ChainSync {chainSyncPeer = p2, chainHighest = Just g}+                | p == p2 -> return g             _ -> getBestBlockHeader     now <- round <$> liftIO getPOSIXTime     gh <- syncHeaders now bb p+    p' <- managerPeerText p mgr+    $(logDebugS) "Chain" $ "Syncing headers against peer: " <> p'     MGetHeaders gh `sendMessage` p  chainMessage :: MonadChain m => ChainMessage -> m () chainMessage (ChainGetBest reply) = getBestBlockHeader >>= atomically . reply chainMessage (ChainHeaders p hs) = processHeaders p hs chainMessage (ChainPeerConnected p _) = do+    mgr <- asks (chainConfManager . myReader)+    p' <- managerPeerText p mgr+    $(logDebugS) "Chain" $ "New peer connected: " <> p'     addPeer p     syncNewPeer chainMessage (ChainPeerDisconnected p _) = do
src/Haskoin/Node/Common.hs view
@@ -15,16 +15,19 @@ -} module Haskoin.Node.Common where +import           Conduit                   (ConduitT) import           Control.Monad             (join) import           Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import           Data.Conduit.Network      (AppData, clientSettings,-                                            runGeneralTCPClient)+import           Data.ByteString           (ByteString)+import           Data.Conduit.Network      (appSink, appSource, clientSettings,+                                            runTCPClient) import           Data.Function             (on) import           Data.List                 (union) import           Data.Maybe                (fromMaybe, isJust) import           Data.String.Conversions   (cs) import           Data.Text                 (Text) import           Data.Time.Clock           (NominalDiffTime, UTCTime)+import           Data.Void                 (Void) import           Data.Word                 (Word32, Word64) import           Database.RocksDB          (DB) import           Haskoin                   (Block (..), BlockHash,@@ -106,6 +109,14 @@ -- | Mailbox for peer manager process. type PeerManager = Mailbox PeerManagerMessage +data Conduits =+    Conduits+        { inboundConduit  :: ConduitT () ByteString IO ()+        , outboundConduit :: ConduitT ByteString Void IO ()+        }++type WithConnection = SockAddr -> (Conduits -> IO ()) -> IO ()+ -- | General node configuration. data NodeConfig = NodeConfig     { nodeConfMaxPeers :: !Int@@ -126,6 +137,7 @@       -- ^ timeout in seconds     , nodeConfPeerOld  :: !Int       -- ^ peer disconnect after seconds+    , nodeConfConnect  :: !WithConnection     }  -- | Peer manager configuration.@@ -147,6 +159,7 @@       -- ^ timeout in seconds         , peerManagerTooOld   :: !Int       -- ^ disconnect peers after connected for so long+        , peerManagerConnect  :: !WithConnection         }  -- | Messages that can be sent to the peer manager.@@ -232,6 +245,7 @@       -- ^ network constants     , peerConfAddress :: !SockAddr       -- ^ peer address+    , peerConfConnect :: !WithConnection     }  -- | Reasons why a peer may stop working.@@ -532,14 +546,13 @@ chainHeaders p hs ch = ChainHeaders p hs `send` ch  -- | Connect to a socket via TCP.-withConnection ::-       MonadUnliftIO m => SockAddr -> (AppData -> m a) -> m a+withConnection :: WithConnection withConnection na f =     fromSockAddr na >>= \case         Nothing -> throwIO PeerAddressInvalid-        Just (host, port) ->+        Just (host, port) -> do             let cset = clientSettings port (cs host)-             in runGeneralTCPClient cset f+            runTCPClient cset $ \ad -> f (Conduits (appSource ad) (appSink ad))  -- | Calculate the median value from a list. The list must not be empty. median :: Fractional a => [a] -> Maybe a
src/Haskoin/Node/Manager.hs view
@@ -354,6 +354,7 @@                               } <- asks myConfig             mgr <- asks myMailbox             sup <- asks mySupervisor+            conn <- asks (peerManagerConnect . myConfig)             nonce <- liftIO randomIO             bb <- getBestBlock             now <- round <$> liftIO getPOSIXTime@@ -365,6 +366,7 @@                         { peerConfListen = pub                         , peerConfNetwork = net                         , peerConfAddress = sa+                        , peerConfConnect = conn                         }             a <- withRunInIO $ \io -> sup `addChild` io (launch mgr pc inbox p)             MVersion ver `sendMessage` p@@ -403,9 +405,9 @@   where     go =         forever $ do-            threadDelay =<<-                liftIO (randomRIO (2 * 1000 * 1000, 10 * 1000 * 1000))             PeerManagerConnect `send` mgr+            threadDelay =<<+                liftIO (randomRIO (100 * 1000, 10 * 500 * 1000))  -- | Add a peer. newPeer :: (MonadIO m, MonadManager m) => SockAddr -> m ()
src/Haskoin/Node/Peer.hs view
@@ -20,25 +20,24 @@     ) where  import           Conduit                 (ConduitT, awaitForever, foldC, mapM_C,-                                          runConduit, takeCE, yield, (.|))+                                          runConduit, takeCE, transPipe, yield,+                                          (.|)) import           Control.Monad           (forever, when) import           Control.Monad.Logger    (MonadLoggerIO, logErrorS)-import           Control.Monad.Reader    (runReaderT) import           Data.ByteString         (ByteString) import qualified Data.ByteString         as B-import           Data.Conduit.Network    (appSink, appSource) import           Data.Serialize          (decode, runGet, runPut) import           Data.String.Conversions (cs) import           Haskoin                 (Message, MessageHeader (..), Network,                                           getMessage, putMessage)-import           Haskoin.Node.Common     (PeerConfig (..), PeerException (..),-                                          PeerMessage (..), peerString,-                                          withConnection)+import           Haskoin.Node.Common     (Conduits (..), PeerConfig (..),+                                          PeerException (..), PeerMessage (..),+                                          peerString) import           Network.Socket          (SockAddr) import           NQE                     (Inbox, PublisherMessage (..), receive,                                           send)-import           UnliftIO                (MonadUnliftIO, atomically, link,-                                          throwIO, withAsync)+import           UnliftIO                (MonadUnliftIO, atomically, liftIO,+                                          link, throwIO, withAsync, withRunInIO)  -- | Run peer process in current thread. peer ::@@ -46,15 +45,15 @@     => PeerConfig     -> Inbox PeerMessage     -> m ()-peer pc inbox = withConnection a $ \ad -> runReaderT (peer_session ad) pc+peer pc inbox = withRunInIO $ \r -> connect a (r . peer_session)   where+    connect = peerConfConnect pc     a = peerConfAddress pc-    go = forever $ do-      receive inbox >>= dispatchMessage pc+    go = forever $ receive inbox >>= dispatchMessage pc     net = peerConfNetwork pc     peer_session ad =-        let ins = appSource ad-            ons = appSink ad+        let ins = transPipe liftIO (inboundConduit ad)+            ons = transPipe liftIO (outboundConduit ad)             src = runConduit $ ins .| inPeerConduit net a .| mapM_C send_msg             snk = outPeerConduit net .| ons          in withAsync src $ \as -> do
test/Haskoin/NodeSpec.hs view
@@ -3,33 +3,57 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings     #-} {-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE TemplateHaskell       #-} module Haskoin.NodeSpec     ( spec     ) where -import           Control.Monad        (forM_, replicateM, replicateM_)-import           Control.Monad.Logger (runNoLoggingT)-import           Control.Monad.Trans  (lift)-import           Data.Maybe           (isJust)-import qualified Database.RocksDB     as R-import           Haskoin              (Block (..), BlockHeader (..),-                                       BlockNode (..), Network,-                                       NetworkAddress (..), Version (..),-                                       btcTest, buildMerkleRoot,-                                       getGenesisHeader, headerHash,-                                       sockToHostAddress, txHash)-import           Haskoin.Node         (Chain, ChainEvent (..), NodeConfig (..),-                                       NodeEvent (..), OnlinePeer (..), Peer,-                                       PeerEvent (..), PeerManager,-                                       chainGetAncestor, chainGetBest,-                                       chainGetParents, managerGetPeer,-                                       peerGetBlocks, peerGetTxs, withNode)-import           Network.Socket       (SockAddr (..))-import           NQE                  (Inbox, newInbox, receiveMatch, sendSTM)-import           Test.Hspec           (Spec, describe, it, shouldBe,-                                       shouldReturn, shouldSatisfy)-import           UnliftIO             (MonadIO, MonadUnliftIO, throwString,-                                       withSystemTempDirectory)+import           Conduit                 (awaitForever, concatMapC, foldC, mapC,+                                          mapMC, runConduit, takeCE, yield,+                                          (.|))+import           Control.Monad           (forM_, forever, replicateM,+                                          replicateM_)+import           Control.Monad.Logger    (logDebug, runNoLoggingT)+import           Control.Monad.Trans     (lift)+import           Data.ByteString         (ByteString)+import qualified Data.ByteString         as B+import           Data.ByteString.Base64  (decodeBase64Lenient)+import           Data.Either             (fromRight)+import           Data.Maybe              (isJust, listToMaybe, mapMaybe)+import           Data.Serialize          (decode, get, runGet, runPut)+import           Data.String.Conversions (cs)+import           Data.Time.Clock.POSIX   (getCurrentTime, getPOSIXTime)+import qualified Database.RocksDB        as R+import           Haskoin                 (Block (..), BlockHash (..),+                                          BlockHeader (..), BlockNode (..),+                                          GetData (..), GetHeaders (..),+                                          Headers (..), InvType (..),+                                          InvVector (..), Message (..),+                                          MessageHeader (..), Network (..),+                                          NetworkAddress (..), Ping (..),+                                          Pong (..), VarInt (..), Version (..),+                                          bchRegTest, btcTest, buildMerkleRoot,+                                          getGenesisHeader, getMessage,+                                          headerHash, nodeNetwork, putMessage,+                                          sockToHostAddress, txHash)+import           Haskoin.Node            (Chain, ChainEvent (..), Conduits (..),+                                          NodeConfig (..), NodeEvent (..),+                                          OnlinePeer (..), Peer, PeerEvent (..),+                                          PeerManager, WithConnection,+                                          buildVersion, chainGetAncestor,+                                          chainGetBest, chainGetParents,+                                          managerGetPeer, peerGetBlocks,+                                          peerGetTxs, withConnection, withNode)+import           Network.Socket          (SockAddr (..))+import           NQE                     (Inbox, Mailbox, inboxToMailbox,+                                          newInbox, receive, receiveMatch, send,+                                          sendSTM)+import           System.Random           (randomIO, randomRIO)+import           Test.Hspec              (Spec, describe, it, shouldBe,+                                          shouldReturn, shouldSatisfy)+import           UnliftIO                (MonadIO, MonadUnliftIO, liftIO, link,+                                          throwString, withAsync,+                                          withSystemTempDirectory)  data TestNode = TestNode     { testMgr    :: PeerManager@@ -37,9 +61,59 @@     , nodeEvents :: Inbox NodeEvent     } +dummyPeerConnect :: Network -> NetworkAddress -> WithConnection+dummyPeerConnect 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 <$> liftIO 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 .| concatMapC mockPeerReact .|+            outc .|+            awaitForever (`send` s)+    outc = mapMC $ \msg -> return $ runPut (putMessage net msg)+    inc =+        forever $ do+            x <- takeCE 24 .| foldC+            case decode x of+                Left _ -> error "Dummy peer not decode message header"+                Right (MessageHeader _ _ len _) -> do+                    y <- 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 :: Message -> [Message]+mockPeerReact (MPing (Ping n)) = [MPong (Pong n)]+mockPeerReact (MVersion _) = [MVerAck]+mockPeerReact (MGetHeaders (GetHeaders _ _hs _)) = [MHeaders (Headers hs')]+  where+    f b = (blockHeader b, VarInt (fromIntegral (length (blockTxns b))))+    hs' = map f allBlocks+mockPeerReact (MGetData (GetData ivs)) = mapMaybe f ivs+  where+    f (InvVector InvBlock h) = MBlock <$> listToMaybe (filter (l h) allBlocks)+    f _                      = Nothing+    l h b = headerHash (blockHeader b) == BlockHash h+mockPeerReact _ = []++ spec :: Spec spec = do-    let net = btcTest+    let net = bchRegTest     describe "peer manager on test network" $ do         it "connects to a peer" $             withTestNode net "connect-one-peer" $ \TestNode {..} -> do@@ -50,11 +124,11 @@         it "downloads some blocks" $             withTestNode net "get-blocks" $ \TestNode {..} -> do                 let h1 =-                        "000000000babf10e26f6cba54d9c282983f1d1ce7061f7e875b58f8ca47db932"+                        "3094ed3592a06f3d8e099eed2d9c1192329944f5df4a48acb29e08f12cfbb660"                     h2 =-                        "00000000851f278a8b2c466717184aae859af5b83c6f850666afbc349cf61577"+                        "0c89955fc5c9f98ecc71954f167b938138c90c6a094c4737f2e901669d26763f"                 p <- waitForPeer nodeEvents-                pbs <- peerGetBlocks net 30 p [h1, h2]+                pbs <- peerGetBlocks net 10 p [h1, h2]                 pbs `shouldSatisfy` isJust                 let Just [b1, b2] = pbs                 headerHash (blockHeader b1) `shouldBe` h1@@ -64,53 +138,39 @@                         buildMerkleRoot (map txHash (blockTxns b))                 testMerkle b1                 testMerkle b2-        it "connects to two peers" $-            withTestNode net "connect-peers" $ \TestNode {..} ->-                replicateM_ 2 (waitForPeer nodeEvents) `shouldReturn` ()-        it "attempts to get inexistent things" $-            withTestNode net "download-fail" $ \TestNode {..} -> do-                let hs =-                        [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"-                        , "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"-                        ]-                p <- waitForPeer nodeEvents-                peerGetTxs net 30 p hs `shouldReturn` Nothing     describe "chain on test network" $ do         it "syncs some headers" $             withTestNode net "connect-sync" $ \TestNode {..} -> do-                let h =-                        "000000009ec921df4bb16aedd11567e27ede3c0b63835b257475d64a059f102b"-                    hs =-                        [ headerHash (getGenesisHeader net)-                        , "0000000005bdbddb59a3cd33b69db94fa67669c41d9d32751512b5d7b68c71cf"-                        , "00000000185b36fa6e406626a722793bea80531515e0b2a99ff05b73738901f1"-                        , "000000001ab69b12b73ccdf46c9fbb4489e144b54f1565e42e481c8405077bdd"-                        ]-                bns <--                    replicateM 4 . receiveMatch nodeEvents $ \case-                        ChainEvent (ChainBestBlock bn) -> Just bn+                let bh =+                        "3bfa0c6da615fc45aa44ddea6854ac19d16f3ca167e0e21ac2cc262a49c9b002"+                    ah =+                        "7dc835a78a55fa76f9184dc4f6663a73e418c7afec789c5ae25e432fd7fc8467"+                bn <-+                    receiveMatch nodeEvents $ \case+                        ChainEvent (ChainBestBlock bn)+                            | nodeHeight bn > 0 -> Just bn                         _ -> Nothing                 bb <- chainGetBest testChain-                nodeHeight bb `shouldSatisfy` (>= 6000)+                nodeHeight bb `shouldSatisfy` (== 15)                 an <-                     maybe (throwString "No ancestor found") return =<<-                    chainGetAncestor 2357 (last bns) testChain-                map (headerHash . nodeHeader) bns `shouldBe` hs-                headerHash (nodeHeader an) `shouldBe` h+                    chainGetAncestor 10 bn testChain+                headerHash (nodeHeader bn) `shouldBe` bh+                headerHash (nodeHeader an) `shouldBe` ah         it "downloads some block parents" $             withTestNode net "parents" $ \TestNode {..} -> do                 let hs =-                        [ "00000000c74a24e1b1f2c04923c514ed88fc785cf68f52ed0ccffd3c6fe3fbd9"-                        , "000000007e5c5f40e495186ac4122f2e4ee25788cc36984a5760c55ecb376cb1"-                        , "00000000a6299059b2bff3479bc569019792e75f3c0f39b10a0bc85eac1b1615"+                        [ "52e886df7b166d961ac2d3d2d561d806325d51a609dc0a5d9d5fcb65d47906d7"+                        , "2537a081b9e2b24d217fac2886f387758cb3aa4e4956b3be7ed229bafbb71b0f"+                        , "7c72f306215a296f9714320a497b1f2cb5f9b99f162d7e04333c243fac9a54d8"                         ]                 [_, bn] <-                     replicateM 2 $                     receiveMatch nodeEvents $ \case                         ChainEvent (ChainBestBlock bn) -> Just bn                         _ -> Nothing-                nodeHeight bn `shouldBe` 2000-                ps <- chainGetParents 1997 bn testChain+                nodeHeight bn `shouldBe` 15+                ps <- chainGetParents 12 bn testChain                 length ps `shouldBe` 3                 forM_ (zip ps hs) $ \(p, h) ->                     headerHash (nodeHeader p) `shouldBe` h@@ -121,7 +181,6 @@         PeerEvent (PeerConnected p _) -> Just p         _ -> Nothing - withTestNode ::        MonadUnliftIO m     => Network@@ -140,18 +199,80 @@                     , R.errorIfExists = True                     , R.compression = R.SnappyCompression                     }-        let cfg =+        let ad = NetworkAddress nodeNetwork (sockToHostAddress (SockAddrInet 0 0))+            cfg =                 NodeConfig                     { nodeConfMaxPeers = 20                     , nodeConfDB = db-                    , nodeConfPeers = []-                    , nodeConfDiscover = True+                    , nodeConfPeers = [("127.0.0.1", 17486)]+                    , nodeConfDiscover = False                     , nodeConfNetAddr = NetworkAddress 0 (sockToHostAddress (SockAddrInet 0 0))                     , nodeConfNet = net                     , nodeConfEvents = (`sendSTM` node_inbox)                     , nodeConfTimeout = 120                     , nodeConfPeerOld = 48 * 3600+                    , nodeConfConnect = dummyPeerConnect net ad                     }         withNode cfg $ \(mgr, ch) ->             lift $             f TestNode {testMgr = mgr, testChain = ch, nodeEvents = node_inbox}++allBlocks :: [Block]+allBlocks =+    fromRight (error "Could not decode blocks") $+    runGet f (decodeBase64Lenient allBlocksBase64)+  where+    f = mapM (const get) [1..15]++allBlocksBase64 :: ByteString+allBlocksBase64 =+    "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\+    \wT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAA"