haskoin-node-2.4.0: test/unit/Haskoin/Node/PeerMgrSpec.hs
-- | 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