haskoin-store-1.6.4: test/Haskoin/StoreSpec.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoFieldSelectors #-}
module Haskoin.StoreSpec (spec) where
import Conduit
import Control.Monad
import Control.Monad.Reader
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.ByteString.Base64
import Data.Either
import Data.HashMap.Strict qualified as HashMap
import Data.Maybe
import Data.Serialize
import Data.Time.Clock.POSIX
import Data.Word
import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)
import System.Mem (performMajorGC)
import Haskoin
import Haskoin.Node
import Haskoin.Store
import Haskoin.Store.TestBlocks (nextBlock)
import NQE
import Network.Socket
import System.Random
import Test.Hspec
import UnliftIO
data TestStore = TestStore
{ testStoreDB :: !DatabaseReader,
testStoreBlockStore :: !BlockStore,
testStoreChain :: !Chain,
testStoreEvents :: !(Inbox StoreEvent),
-- | How many blocks each getdata sent to the mock peer asked for, oldest first.
testStoreBlockRequests :: !(TVar [Int])
}
spec :: Spec
spec = prepareContext $ \ctx -> do
describe "Download" $ do
it "gets 8 blocks" $
withTestStore bchRegTest ctx "eight-blocks" $ \TestStore {..} -> do
bs <- replicateM 8 . receiveMatch testStoreEvents $ \case
StoreBestBlock b -> Just b
_ -> Nothing
let bestHash = last bs
bestNodeM <- liftIO $ chainGetBlock testStoreChain bestHash
bestNodeM `shouldSatisfy` isJust
let bestNode = fromJust bestNodeM
bestHeight = bestNode.height
bestHeight `shouldBe` 8
it "get a block and its transactions" $
withTestStore bchRegTest ctx "get-block-txs" $ \TestStore {..} ->
flip runReaderT testStoreDB $ do
let h1 = "5369ef2386c72acdf513ffd80aeba2a1774e2f004d120761e54a8bf614173f3e"
get_the_block h =
receive testStoreEvents >>= \case
StoreBestBlock b
| h <= 1 -> return b
| otherwise ->
get_the_block ((h :: Int) - 1)
_ -> get_the_block h
bh <- get_the_block 15
m <- getBlock bh
let bd = fromMaybe (error "Could not get block") m
t1 <- getTransaction h1
lift $ do
bd.height `shouldBe` 15
length bd.txs `shouldBe` 1
listToMaybe bd.txs `shouldBe` Just h1
fmap (txHash . transactionData) t1 `shouldBe` Just h1
it "downloads 80 blocks with a request for 64 and one for the remaining 16" $
withTestStore bchRegTest ctx "block-window" $ \TestStore {..} -> do
bs <- replicateM 80 . receiveMatch testStoreEvents $ \case
StoreBestBlock b -> Just b
_ -> Nothing
bestNodeM <- liftIO $ chainGetBlock testStoreChain (last bs)
fmap (.height) bestNodeM `shouldBe` Just 80
readTVarIO testStoreBlockRequests `shouldReturn` [64, 16]
describe "Memory" $
it "keeps the heap flat while syncing" $ do
enabled <- getRTSStatsEnabled
unless enabled $ pendingWith "run with +RTS -T to measure the heap"
-- Each block passes messages through the peer, node and store loops.
-- When forever went through a dictionary, each of six loops kept 33 to
-- 107 bytes per message, a few hundred bytes per block in all: over
-- the 5000 blocks measured, megabytes. The heap normally grows by about
-- 330 kB here, and a leak of 500 bytes per block fails the test. The
-- length is a multiple of 2000, so the last headers message the mock
-- peer sends is empty, which haskoin-node before 2.1.2 did not handle.
let blocks = chainOf 6000
withTestStoreBlocks blocks bchRegTest ctx "heap" $ \TestStore {..} -> do
let wait_for height =
receive testStoreEvents >>= \case
StoreBestBlock b ->
chainGetBlock testStoreChain b >>= \case
Just n | n.height >= height -> return ()
_ -> wait_for height
_ -> wait_for height
live = do
performMajorGC
(.gc.gcdetails_live_bytes) <$> getRTSStats
wait_for 1000
live_before <- live
wait_for 6000
live_after <- live
let growth = fromIntegral live_after - fromIntegral live_before :: Integer
putStrLn $ "heap growth over 5000 blocks: " <> show growth <> " bytes"
growth `shouldSatisfy` (< 1024 * 1024)
withTestStore ::
(MonadUnliftIO m) =>
Network ->
Ctx ->
String ->
(TestStore -> m a) ->
m a
withTestStore = withTestStoreBlocks allBlocks
-- | Like 'withTestStore', with the mock peer serving these blocks.
withTestStoreBlocks ::
(MonadUnliftIO m) =>
[Block] ->
Network ->
Ctx ->
String ->
(TestStore -> m a) ->
m a
withTestStoreBlocks blocks net ctx t f =
withSystemTempDirectory ("haskoin-store-test-" <> t <> "-") $ \w -> do
requests <- newTVarIO []
let ad =
NetworkAddress
nodeNetwork
(sockToHostAddress (SockAddrInet 0 0))
cfg =
StoreConfig
{ maxPeers = 20,
initPeers = [],
discover = True,
db = w,
net = net,
ctx = ctx,
redis = Nothing,
gap = gap,
initGap = 20,
redisMinAddrs = 100,
redisMaxKeys = 100 * 1000 * 1000,
noMempool = False,
wipeMempool = False,
syncMempool = False,
blockWindow = 64,
mempoolTimeout = 14,
peerTimeout = 60,
maxPeerLife = 48 * 3600,
connect = dummyPeerConnect blocks requests net ad,
stats = Nothing,
redisSyncInterval = 30,
bloom = True,
maxOpenFiles = Nothing,
writeBufferSize = Nothing,
maxWalSize = Nothing,
logger = Logger {level = LevelError, source = "", push = \_ -> return ()}
}
withStore cfg $ \store ->
withSubscription store.pub $ \sub ->
f
TestStore
{ testStoreDB = store.db,
testStoreBlockStore = store.block,
testStoreChain = store.chain,
testStoreEvents = sub,
testStoreBlockRequests = requests
}
gap :: Word32
gap = 32
-- | The 15 canned blocks followed by 65 mined on top of them: more than one
-- 64-block window, so that a download needs a second request.
allBlocks :: [Block]
allBlocks = chainOf 80
-- | The canned blocks followed by blocks mined on top of them, this many in
-- all.
chainOf :: Int -> [Block]
chainOf n = take n (cannedBlocks <> mined (length cannedBlocks) (last cannedBlocks))
where
mined height parent = block : mined (height + 1) block
where
(block, _) = nextBlock bchRegTest (fromIntegral height) parent.header []
cannedBlocks :: [Block]
cannedBlocks =
fromRight (error "Could not decode blocks") $
runGet f (decodeBase64Lenient cannedBlocksBase64)
where
f = mapM (const get) [(1 :: Int) .. 15]
cannedBlocksBase64 :: ByteString
cannedBlocksBase64 =
"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\
\wT6Wn6LtY47B39Th44JKsZ8BGJCvc6wAAAAA"
dummyPeerConnect :: [Block] -> TVar [Int] -> Network -> NetworkAddress -> SockAddr -> WithConnection
dummyPeerConnect blocks requests 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
.| iterMC (recordBlockRequest requests)
.| concatMapC (mockPeerReact blocks)
.| 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
Right msg' -> yield msg'
Left e ->
error $
"Dummy peer could not decode payload: " <> show e
-- | Note how many blocks a getdata asks for.
recordBlockRequest :: TVar [Int] -> Message -> IO ()
recordBlockRequest requests (MGetData (GetData ivs)) =
atomically $ modifyTVar' requests (<> [length [() | InvVector InvBlock _ <- ivs]])
recordBlockRequest _ _ = return ()
mockPeerReact :: [Block] -> Message -> [Message]
mockPeerReact _ (MPing (Ping n)) = [MPong (Pong n)]
mockPeerReact _ (MVersion _) = [MVerAck]
mockPeerReact blocks (MGetHeaders (GetHeaders _ locator _)) = [MHeaders (Headers hs')]
where
f b = (b.header, VarInt $ fromIntegral $ length b.txs)
-- Up to 2000 headers after the first block of the locator that the
-- peer has, like a real peer; from the start if it has none of them.
heights = HashMap.fromList (zip (map (headerHash . (.header)) blocks) [1 :: Int ..])
start = fromMaybe 0 (listToMaybe (mapMaybe (`HashMap.lookup` heights) locator))
hs' = map f (take 2000 (drop start blocks))
mockPeerReact blocks (MGetData (GetData ivs)) = mapMaybe f ivs
where
f (InvVector InvBlock h) = MBlock <$> HashMap.lookup (BlockHash h) byHash
f _ = Nothing
byHash = HashMap.fromList [(headerHash b.header, b) | b <- blocks]
mockPeerReact _ _ = []