packages feed

haskoin-node-2.4.0: test/regtest/Main.hs

-- | Test the library against Bitcoin Core and Bitcoin Cash Node in regtest
-- mode. Nodes run in Podman containers built from the Containerfile in this
-- directory. Set HASKOIN_NODE_SKIP_REGTEST to skip.
module Main (main) where

import Control.Monad (forM_, replicateM_, void)
import Data.List (isInfixOf)
import Data.Maybe (isJust)
import Data.String (fromString)
import Haskoin
import Haskoin.Node
import Haskoin.Node.Test
import Regtest.Podman
import System.Environment (lookupEnv)
import Test.Hspec

main :: IO ()
main = do
  skip <- isJust <$> lookupEnv "HASKOIN_NODE_SKIP_REGTEST"
  available <- podmanAvailable
  if
    | skip -> hspec $ it "regtest" $ pendingWith "HASKOIN_NODE_SKIP_REGTEST set"
    | not available -> hspec $ it "regtest" $ pendingWith "podman not available"
    | otherwise -> buildImage >> hspec spec

-- | Node connected to the given nodes in the container. The timeout is long
-- so that the stall recovery poll does not hide announcement handling bugs.
withRegtestNode :: Container -> [Int] -> (TestNode -> IO a) -> IO a
withRegtestNode c is f = do
  addrs <- mapM (hostAddress c) is
  withTestNode
    (implNet c.impl)
    addrs
    withConnection
    (setTimeout 600)
    f

-- | Wait until our best header is the tip of node number @i@.
waitForTip :: Container -> Int -> TestNode -> IO BlockNode
waitForTip c i t = do
  tip <- fromString <$> cli c i ["getbestblockhash"]
  waitForBest 60 t ((== tip) . headerHash . (.header))

blockHashAt :: Container -> Int -> Int -> IO BlockHash
blockHashAt c i h = fromString <$> cli c i ["getblockhash", show h]

-- | Node 0 with blocks mined so that it leaves initial block download.
withMinedNode :: Impl -> Int -> (Container -> IO a) -> IO a
withMinedNode impl n f =
  withContainer impl $ \c -> do
    startNode c 0 []
    _ <- mine c 0 n
    f c

spec :: Spec
spec = forM_ [BitcoinCore, BitcoinCashNode] $ \impl ->
  describe (implName impl) $ do
    let net = implNet impl

    it "completes the handshake" $ withMinedNode impl 1 $ \c ->
      withRegtestNode c [0] $ \t -> do
        p <- waitForPeer t
        Just o <- getOnlinePeer t.node.peerMgr p
        Just v <- return o.version
        v.version `shouldSatisfy` (>= 70012)
        let VarString ua = v.userAgent
            expected = case impl of
              BitcoinCore -> "Satoshi"
              BitcoinCashNode -> "Bitcoin Cash Node"
        show ua `shouldSatisfy` (expected `isInfixOf`)

    it "syncs a chain longer than one header batch" $
      withMinedNode impl 2100 $ \c ->
        withRegtestNode c [0] $ \t -> do
          bn <- receiveWithin 120 "synced" t.events $ \case
            ChainEvent (ChainSynced bn) -> Just bn
            _ -> Nothing
          bn.height `shouldBe` 2100
          void $ waitForTip c 0 t

    it "follows blocks mined one at a time" $ withMinedNode impl 1 $ \c ->
      withRegtestNode c [0] $ \t -> do
        _ <- waitForTip c 0 t
        replicateM_ 5 $ mine c 0 1 >> waitForTip c 0 t

    it "follows bursts of blocks without stalling" $
      withMinedNode impl 1 $ \c ->
        withRegtestNode c [0] $ \t -> do
          _ <- waitForTip c 0 t
          replicateM_ 3 $ mine c 0 99 >> waitForTip c 0 t

    it "follows a burst longer than a header batch" $
      withMinedNode impl 1 $ \c ->
        withRegtestNode c [0] $ \t -> do
          _ <- waitForTip c 0 t
          _ <- mine c 0 2500
          bn <- waitForTip c 0 t
          bn.height `shouldBe` 2501

    it "follows a reorganization" $ withMinedNode impl 10 $ \c ->
      withRegtestNode c [0] $ \t -> do
        old <- waitForTip c 0 t
        h8 <- cli c 0 ["getblockhash", "8"]
        void $ cli c 0 ["invalidateblock", h8]
        _ <- mineTo c 0 5 otherAddress
        new <- waitForTip c 0 t
        new.height `shouldBe` 12
        chainBlockMain t.node.chain (headerHash old.header) `shouldReturn` False
        split <- chainGetSplitBlock t.node.chain old new
        split.height `shouldBe` 7

    it "follows a reorganization found in a burst" $
      withMinedNode impl 120 $ \c ->
        withRegtestNode c [0] $ \t -> do
          _ <- waitForTip c 0 t
          h <- cli c 0 ["getblockhash", "70"]
          void $ cli c 0 ["invalidateblock", h]
          _ <- mineTo c 0 99 otherAddress
          new <- waitForTip c 0 t
          new.height `shouldBe` 168

    it "downloads blocks" $ withMinedNode impl 5 $ \c ->
      withRegtestNode c [0] $ \t -> do
        p <- waitForPeer t
        _ <- waitForTip c 0 t
        hs <- mapM (blockHashAt c 0) [1 .. 5]
        Just bs <- getBlocks net 10 p hs
        map (headerHash . (.header)) bs `shouldBe` hs
        forM_ bs $ \b ->
          buildMerkleRoot (map txHash b.txs) `shouldBe` Right b.header.merkle
        let unknown = headerHash (mkHeaders net 9 net.genesisHeader 0 1 !! 0)
        getBlocks net 10 p [unknown] `shouldReturn` Nothing

    it "downloads announced transactions" $ withContainer impl $ \c -> do
      startNode c 0 []
      -- Bitcoin Cash Node also loads a default wallet, so name ours
      let wallet args = cli c 0 ("-rpcwallet=w" : args)
      void $ cli c 0 ["createwallet", "w"]
      addr <- wallet ["getnewaddress"]
      void $ mineTo c 0 101 addr
      withRegtestNode c [0] $ \t -> do
        p <- waitForPeer t
        _ <- waitForTip c 0 t
        txid <- fromString <$> wallet ["sendtoaddress", mineAddress, "1"]
        receiveWithin 60 "transaction announcement" t.events $ \case
          PeerEvent (PeerMessage p' (MInv (Inv ivs)))
            | p == p' && any ((== txid) . TxHash . (.invHash)) ivs -> Just ()
          _ -> Nothing
        Just [tx] <- getTxs net 10 p [txid]
        txHash tx `shouldBe` txid

    it "pings a peer" $ withMinedNode impl 1 $ \c ->
      withRegtestNode c [0] $ \t -> do
        p <- waitForPeer t
        pingPeer 10 p `shouldReturn` True

    it "reconnects to a restarted peer and catches up" $
      withMinedNode impl 5 $ \c ->
        withRegtestNode c [0] $ \t -> do
          p <- waitForPeer t
          _ <- waitForTip c 0 t
          stopNode c 0
          waitForDisconnect t p
          startNode c 0 []
          _ <- mine c 0 3
          _ <- waitForPeer t
          bn <- waitForTip c 0 t
          bn.height `shouldBe` 8

    it "keeps following the chain when a peer goes away" $
      withMinedNode impl 5 $ \c -> do
        startNode c 1 ["-addnode=" <> nodeAddress c 0]
        eventually 60 "nodes in sync" $
          (== "5") <$> cli c 1 ["getblockcount"]
        withRegtestNode c [0, 1] $ \t -> do
          _ <- waitForPeer t
          _ <- waitForPeer t
          _ <- waitForTip c 0 t
          stopNode c 0
          _ <- mine c 1 5
          bn <- waitForTip c 1 t
          bn.height `shouldBe` 10

    it "follows bursts announced by several peers" $
      withMinedNode impl 1 $ \c -> do
        startNode c 1 ["-addnode=" <> nodeAddress c 0]
        withRegtestNode c [0, 1] $ \t -> do
          _ <- waitForPeer t
          _ <- waitForPeer t
          _ <- waitForTip c 0 t
          replicateM_ 3 $ mine c 0 99 >> waitForTip c 0 t
          eventually 60 "nodes in sync" $
            (== "298") <$> cli c 1 ["getblockcount"]
          expectNothingWithin 1 "peer disconnection" t.events $ \case
            PeerEvent (PeerDisconnected _) -> Just ()
            _ -> Nothing