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