packages feed

haskoin-node-0.3.0: tests/Network/Haskoin/Node/Units.hs

{-# LANGUAGE OverloadedStrings #-}
module Network.Haskoin.Node.Units where
import           Control.Monad                   (forM_, when)
import           Control.Monad.Logger            (NoLoggingT)
import           Control.Monad.Trans             (MonadIO, liftIO)
import           Control.Monad.Trans.Resource    (ResourceT)
import           Data.Maybe                      (fromJust, isNothing,
                                                  maybeToList)
import           Data.Word                       (Word32)
import           Database.Persist.Sqlite         (SqlPersistT,
                                                  runMigrationSilent, runSqlite)
import           Network.Haskoin.Block
import           Network.Haskoin.Constants
import           Network.Haskoin.Node.HeaderTree
import           Test.Framework                  (Test, testGroup)
import           Test.Framework.Providers.HUnit  (testCase)
import           Test.HUnit                      (Assertion, assertBool,
                                                  assertEqual, assertFailure)

-- TODO: Make sure that evalNewChain for a partially overlapping best chain
-- properly evaluates to BestChain.

type App = SqlPersistT (NoLoggingT (ResourceT IO))

tests :: [Test]
tests =
    [ testGroup "Header Tree"
        [ testCase "Initalization successful"    $ runUnit initialize
        , testCase "Add second block"            $ runUnit addSecondBlock
        , testCase "Blockchain head correct"     $ runUnit blockChainHead
        , testCase "Find fork node"              $ runUnit forkNode
        , testCase "Find fork node (non-head)"   $ runUnit forkNodeNonHead
        , testCase "Find fork node (same chain)" $ runUnit forkNodeSameChain
        , testCase "Get best chain"              $ runUnit getBestChain
        , testCase "Get side chain"              $ runUnit getSideChain
        , testCase "Nodes at height"             $ runUnit getNodesHeight
        , testCase "Block locator to head"       $ runUnit blockLocatorToHead
        , testCase "Block locator to non-head"   $ runUnit blockLocatorToNode
        , testCase "Find split node"             $ runUnit splitNode
        ]
    ]

initialize :: App ()
initialize = do
    initHeaderTree
    bM <- getBlockByHash (headerHash genesisHeader)
    liftIO $ assertEqual "Genesis node in header tree" (Just genesisBlock) bM
    hs <- getHeads
    liftIO $ assertEqual "Genesis node is only head" [genesisBlock] hs
    bh <- getBestBlock
    liftIO $ assertEqual "Genesis node matches best header" genesisBlock bh

addSecondBlock :: App ()
addSecondBlock = do
    initHeaderTree
    let block = head chain0
    liftIO $ assertEqual "Block builds on genesis block"
        (headerHash genesisHeader)
        (nodePrev block)
    putBlock block
    block' <- getBlockByHash $ nodeHash block
    liftIO $ assertEqual "Block can be retrieved" (Just block) block'

blockChainHead :: App ()
blockChainHead = mockBlockChain >> do
    heads <- getHeads
    liftIO $ assertEqual "Heads match"
        [last chain0, last chain1, last chain2, last chain3]
        heads
    bh <- getBestBlock
    liftIO $ assertEqual "Best block has correct hash"
        (nodeHash $ last chain3) (nodeHash bh)
    liftIO $ assertEqual "Best block height is right"
        (nodeBlockHeight $ last chain3) (nodeBlockHeight bh)

forkNode :: App ()
forkNode = mockBlockChain >> do
    let l = last chain2
        r = last chain3
    bn <- splitBlock l r

    liftIO $ assertEqual "Split block are correct"
        (chain0 !! 1) bn

    commonLM <- getBlockByHeight l $ nodeBlockHeight bn
    when (isNothing commonLM) $ liftIO $
        assertFailure "Could not find fork on left side"
    let commonL = fromJust commonLM

    commonRM <- getBlockByHeight r $ nodeBlockHeight bn
    when (isNothing commonRM) $ liftIO $
        assertFailure "Could not find fork on right side"
    let commonR = fromJust commonRM

    firstLM  <- getBlockByHeight l (nodeBlockHeight bn + 1)
    when (isNothing firstLM) $ liftIO $
        assertFailure "Could not find fork child on left side"
    let firstL = fromJust firstLM

    firstRM  <- getBlockByHeight r (nodeBlockHeight bn + 1)
    when (isNothing firstLM) $ liftIO $
        assertFailure "Could not find fork child on right side"
    let firstR = fromJust firstRM

    liftIO $ assertEqual "Fork node is same in both sides" commonL commonR
    liftIO $ assertEqual "Fork node connect with left side"
        (nodeHash commonL)
        (nodePrev firstL)
    liftIO $ assertEqual "Fork node connect with right side"
        (nodeHash commonR)
        (nodePrev firstR)
    liftIO $ assertBool "After-fork chains diverge" $ firstL /= firstR
    liftIO $ assertEqual "Fork node matches hardcoded one"
        (chain0 !! 1) commonL

forkNodeNonHead :: App ()
forkNodeNonHead = mockBlockChain >> do
    let l = chain2 !! 1
        r = chain1 !! 1
    height <- nodeBlockHeight <$> splitBlock l r
    splitM <- getBlockByHeight l height
    liftIO $ assertEqual "Fork node is correct" (Just $ chain1 !! 1) splitM

forkNodeSameChain :: App ()
forkNodeSameChain = mockBlockChain >> do
    let l = chain3 !! 5
        r = chain3 !! 3
    height <- nodeBlockHeight <$> splitBlock l r
    splitM <- getBlockByHeight r height
    liftIO $ assertEqual "Fork node is correct" (Just $ chain3 !! 3) splitM

getBestChain :: App ()
getBestChain = mockBlockChain >> do
    h <- getBestBlock
    ch <- getBlocksFromHeight h 0 0
    liftIO $ assertEqual "Best chain correct" bch ch
  where
    bch = genesisBlock : take 2 chain0 ++ chain3

getSideChain :: App ()
getSideChain = mockBlockChain >> do
    ch <- getBlocksFromHeight (chain2 !! 1) 0 0
    liftIO $ assertEqual "Side chain correct" sch ch
  where
    sch = genesisBlock :
        take 3 chain0 ++ take 2 chain1 ++ take 2 chain2

getNodesHeight :: App ()
getNodesHeight = mockBlockChain >> do
    ns <- getBlocksAtHeight 3
    liftIO $ assertEqual "Nodes at height match" hns ns
  where
    hns = [chain0 !! 2, head chain3]

blockLocatorToHead :: App ()
blockLocatorToHead = do
    mockBlockChain
    putBlocks bs
    h <- getBestBlock
    liftIO $ assertEqual "Head matches" (last bs) h
    ls <- blockLocator h
    liftIO $ assertEqual "Last is genesis"
        (last ls)
        (headerHash genesisHeader)
    liftIO $ assertEqual "First is current head"
        (head ls)
        (nodeHash h)
    last10 <- map nodeHash . reverse <$>
        getBlocksFromHeight h 0 (nodeBlockHeight h - 9)
    liftIO $ assertEqual "Last ten blocks contiguous"
        last10
        (take 10 ls)
    let h10 = nodeBlockHeight h - 10
    bhs <- map (nodeHash . fromJust) <$>
        mapM (getBlockByHeight h)
        [h10, h10 - 2, h10 - 6, h10 - 14, h10 - 30, h10 - 62]
    liftIO $ assertEqual "All block hashes correct"
        (last10 ++ bhs ++ [headerHash genesisHeader])
        ls
  where
    bs = manyBlocks $ last chain1

blockLocatorToNode :: App ()
blockLocatorToNode = do
    mockBlockChain
    putBlocks bs
    n <- fromJust <$> getBlockByHash (nodeHash $ chain3 !! 4)
    ls <- blockLocator n
    xs <- map nodeHash . reverse <$>
        getBlocksFromHeight n 0 0
    liftIO $ assertEqual "Block locator for non-head node is correct" xs ls
  where
    bs = manyBlocks $ last chain1

splitNode :: App ()
splitNode = do
    mockBlockChain
    (split, ls, rs) <- splitChains (last chain2, 0) (last chain3, 0)
    liftIO $ assertEqual "Split node correct" (chain0 !! 1) split
    liftIO $ assertEqual "Left correct"
        ([chain0 !! 2] ++ take 2 chain1 ++ chain2)
        ls
    liftIO $ assertEqual "Right correct" chain3 rs

runUnit :: App () -> Assertion
runUnit action = runSqlite ":memory:" $ do
    _ <- runMigrationSilent migrateHeaderTree
    action

mockBlockChain :: MonadIO m => SqlPersistT m ()
mockBlockChain = do
    initHeaderTree
    forM_ (concat [chain0, chain1, chain2, chain3]) putBlock

manyBlocks :: NodeBlock -> [NodeBlock]
manyBlocks b =
    tail $ reverse $ foldBlock (Just b) $ zip [18..117] (repeat 4)


chain0 :: [NodeBlock]
chain0 =
    tail $ reverse $ foldBlock Nothing $ zip [1..4] (repeat 0)

chain1 :: [NodeBlock]
chain1 =
    tail $ reverse $ foldBlock (Just $ chain0 !! 2) $ zip [5..7] (repeat 1)

chain2 :: [NodeBlock]
chain2 =
    tail $ reverse $ foldBlock (Just $ chain1 !! 1) $ zip [8..10] (repeat 2)

chain3 :: [NodeBlock]
chain3 =
    tail $ reverse $ foldBlock (Just $ chain0 !! 1) $ zip [11..17] (repeat 3)

foldBlock :: Maybe NodeBlock -> [(Word32, Word32)] -> [NodeBlock]
foldBlock nM =
    foldl f (maybeToList nM)
  where
    f [] _ = [genesisBlock]
    f ls@(l:_) (n, chain) = mockBlock l chain n : ls

mockBlock :: NodeBlock -> Word32 -> Word32 -> NodeBlock
mockBlock parent chain n = nodeBlock parent chain bh
  where
    bh = BlockHeader
        { blockVersion   = blockVersion $ nodeHeader parent
        , prevBlock      = nodeHash parent
        , merkleRoot     = z
        , blockTimestamp = nodeTimestamp parent + 600
        , blockBits      = blockBits $ nodeHeader parent
        , bhNonce        = n
        }
    z = "0000000000000000000000000000000000000000000000000000000000000000"