packages feed

haskoin-store-0.64.16: src/Haskoin/Store/BlockStore.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}

module Haskoin.Store.BlockStore
  ( -- * Block Store
    BlockStore,
    BlockStoreConfig (..),
    withBlockStore,
    blockStorePeerConnect,
    blockStorePeerConnectSTM,
    blockStorePeerDisconnect,
    blockStorePeerDisconnectSTM,
    blockStoreHead,
    blockStoreHeadSTM,
    blockStoreBlock,
    blockStoreBlockSTM,
    blockStoreNotFound,
    blockStoreNotFoundSTM,
    blockStoreTx,
    blockStoreTxSTM,
    blockStoreTxHash,
    blockStoreTxHashSTM,
    blockStorePendingTxs,
    blockStorePendingTxsSTM,
  )
where

import Control.Monad
  ( forM,
    forM_,
    forever,
    mzero,
    unless,
    void,
    when,
  )
import Control.Monad.Except
  ( ExceptT (..),
    MonadError,
    catchError,
    runExceptT,
  )
import Control.Monad.Logger
  ( MonadLoggerIO,
    logDebugS,
    logErrorS,
    logInfoS,
    logWarnS,
  )
import Control.Monad.Reader
  ( MonadReader,
    ReaderT (..),
    ask,
    asks,
  )
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Maybe (runMaybeT)
import qualified Data.ByteString as B
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.HashSet (HashSet)
import qualified Data.HashSet as HashSet
import Data.List (delete)
import Data.Maybe
  ( catMaybes,
    fromJust,
    isJust,
    mapMaybe,
  )
import Data.Serialize (encode)
import Data.String (fromString)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock
  ( NominalDiffTime,
    UTCTime,
    diffUTCTime,
    getCurrentTime,
  )
import Data.Time.Clock.POSIX
  ( posixSecondsToUTCTime,
    utcTimeToPOSIXSeconds,
  )
import Data.Time.Format
  ( defaultTimeLocale,
    formatTime,
    iso8601DateFormat,
  )
import Haskoin
  ( Block (..),
    BlockHash (..),
    BlockHeader (..),
    BlockHeight,
    BlockNode (..),
    GetData (..),
    InvType (..),
    InvVector (..),
    Message (..),
    Network (..),
    OutPoint (..),
    Tx (..),
    TxHash (..),
    TxIn (..),
    blockHashToHex,
    headerHash,
    txHash,
    txHashToHex,
  )
import Haskoin.Node
  ( Chain,
    OnlinePeer (..),
    Peer,
    PeerException (..),
    PeerManager,
    chainBlockMain,
    chainGetAncestor,
    chainGetBest,
    chainGetBlock,
    chainGetParents,
    getPeers,
    killPeer,
    peerText,
    sendMessage,
    setBusy,
    setFree,
  )
import Haskoin.Store.Common
import Haskoin.Store.Data
import Haskoin.Store.Database.Reader
import Haskoin.Store.Database.Writer
import Haskoin.Store.Logic
  ( ImportException (Orphan),
    deleteUnconfirmedTx,
    importBlock,
    initBest,
    newMempoolTx,
    revertBlock,
  )
import Haskoin.Store.Stats
import NQE
  ( Listen,
    Mailbox,
    Publisher,
    inboxToMailbox,
    newInbox,
    publish,
    query,
    receive,
    send,
    sendSTM,
  )
import qualified System.Metrics as Metrics
import qualified System.Metrics.Gauge as Metrics (Gauge)
import qualified System.Metrics.Gauge as Metrics.Gauge
import System.Random (randomRIO)
import UnliftIO
  ( Exception,
    MonadIO,
    MonadUnliftIO,
    STM,
    TVar,
    async,
    atomically,
    liftIO,
    link,
    modifyTVar,
    newTVarIO,
    readTVar,
    readTVarIO,
    throwIO,
    withAsync,
    writeTVar,
  )
import UnliftIO.Concurrent (threadDelay)

data BlockStoreMessage
  = BlockNewBest !BlockNode
  | BlockPeerConnect !Peer
  | BlockPeerDisconnect !Peer
  | BlockReceived !Peer !Block
  | BlockNotFound !Peer ![BlockHash]
  | TxRefReceived !Peer !Tx
  | TxRefAvailable !Peer ![TxHash]
  | BlockPing !(Listen ())

data BlockException
  = BlockNotInChain !BlockHash
  | Uninitialized
  | CorruptDatabase
  | AncestorNotInChain !BlockHeight !BlockHash
  | MempoolImportFailed
  deriving (Show, Eq, Ord, Exception)

data Syncing = Syncing
  { syncingPeer :: !Peer,
    syncingTime :: !UTCTime,
    syncingBlocks :: ![BlockHash]
  }

data PendingTx = PendingTx
  { pendingTxTime :: !UTCTime,
    pendingTx :: !Tx,
    pendingDeps :: !(HashSet TxHash)
  }
  deriving (Show, Eq, Ord)

-- | Block store process state.
data BlockStore = BlockStore
  { myMailbox :: !(Mailbox BlockStoreMessage),
    myConfig :: !BlockStoreConfig,
    myPeer :: !(TVar (Maybe Syncing)),
    myTxs :: !(TVar (HashMap TxHash PendingTx)),
    requested :: !(TVar (HashSet TxHash)),
    myMetrics :: !(Maybe StoreMetrics)
  }

data StoreMetrics = StoreMetrics
  { storeHeight :: !Metrics.Gauge,
    headersHeight :: !Metrics.Gauge,
    storePendingTxs :: !Metrics.Gauge,
    storePeersConnected :: !Metrics.Gauge,
    storeMempoolSize :: !Metrics.Gauge
  }

newStoreMetrics :: MonadIO m => Metrics.Store -> m StoreMetrics
newStoreMetrics s = liftIO $ do
  storeHeight <- g "blockchain.height"
  headersHeight <- g "blockchain.headers"
  storePendingTxs <- g "mempool.pending_txs"
  storePeersConnected <- g "network.peers_connected"
  storeMempoolSize <- g "mempool.size"
  return StoreMetrics {..}
  where
    g x = Metrics.createGauge ("store." <> x) s

setStoreHeight :: MonadIO m => BlockT m ()
setStoreHeight =
  asks myMetrics >>= \case
    Nothing -> return ()
    Just m ->
      getBestBlock >>= \case
        Nothing -> setit m 0
        Just bb ->
          getBlock bb >>= \case
            Nothing -> setit m 0
            Just b -> setit m (blockDataHeight b)
  where
    setit m i = liftIO $ storeHeight m `Metrics.Gauge.set` fromIntegral i

setHeadersHeight :: MonadIO m => BlockT m ()
setHeadersHeight =
  asks myMetrics >>= \case
    Nothing -> return ()
    Just m -> do
      h <- fmap nodeHeight $ chainGetBest =<< asks (blockConfChain . myConfig)
      liftIO $ headersHeight m `Metrics.Gauge.set` fromIntegral h

setPendingTxs :: MonadIO m => BlockT m ()
setPendingTxs =
  asks myMetrics >>= \case
    Nothing -> return ()
    Just m -> do
      s <- asks myTxs >>= \t -> atomically (HashMap.size <$> readTVar t)
      liftIO $ storePendingTxs m `Metrics.Gauge.set` fromIntegral s

setPeersConnected :: MonadIO m => BlockT m ()
setPeersConnected =
  asks myMetrics >>= \case
    Nothing -> return ()
    Just m -> do
      ps <- fmap length $ getPeers =<< asks (blockConfManager . myConfig)
      liftIO $ storePeersConnected m `Metrics.Gauge.set` fromIntegral ps

setMempoolSize :: MonadIO m => BlockT m ()
setMempoolSize =
  asks myMetrics >>= \case
    Nothing -> return ()
    Just m -> do
      s <- length <$> getMempool
      liftIO $ storeMempoolSize m `Metrics.Gauge.set` fromIntegral s

-- | Configuration for a block store.
data BlockStoreConfig = BlockStoreConfig
  { -- | peer manager from running node
    blockConfManager :: !PeerManager,
    -- | chain from a running node
    blockConfChain :: !Chain,
    -- | listener for store events
    blockConfListener :: !(Publisher StoreEvent),
    -- | RocksDB database handle
    blockConfDB :: !DatabaseReader,
    -- | network constants
    blockConfNet :: !Network,
    -- | do not index new mempool transactions
    blockConfNoMempool :: !Bool,
    -- | wipe mempool at start
    blockConfWipeMempool :: !Bool,
    -- | sync mempool from peers
    blockConfSyncMempool :: !Bool,
    -- | disconnect syncing peer if inactive for this long
    blockConfPeerTimeout :: !NominalDiffTime,
    blockConfStats :: !(Maybe Metrics.Store)
  }

type BlockT m = ReaderT BlockStore m

runImport ::
  MonadLoggerIO m =>
  WriterT (ExceptT ImportException m) a ->
  BlockT m (Either ImportException a)
runImport f =
  ReaderT $ \r -> runExceptT $ runWriter (blockConfDB (myConfig r)) f

runRocksDB :: ReaderT DatabaseReader m a -> BlockT m a
runRocksDB f =
  ReaderT $ runReaderT f . blockConfDB . myConfig

instance MonadIO m => StoreReadBase (BlockT m) where
  getNetwork =
    runRocksDB getNetwork
  getBestBlock =
    runRocksDB getBestBlock
  getBlocksAtHeight =
    runRocksDB . getBlocksAtHeight
  getBlock =
    runRocksDB . getBlock
  getTxData =
    runRocksDB . getTxData
  getSpender =
    runRocksDB . getSpender
  getUnspent =
    runRocksDB . getUnspent
  getBalance =
    runRocksDB . getBalance
  getMempool =
    runRocksDB getMempool

instance MonadUnliftIO m => StoreReadExtra (BlockT m) where
  getMaxGap =
    runRocksDB getMaxGap
  getInitialGap =
    runRocksDB getInitialGap
  getAddressesTxs as =
    runRocksDB . getAddressesTxs as
  getAddressesUnspents as =
    runRocksDB . getAddressesUnspents as
  getAddressUnspents a =
    runRocksDB . getAddressUnspents a
  getAddressTxs a =
    runRocksDB . getAddressTxs a
  getNumTxData =
    runRocksDB . getNumTxData
  getBalances =
    runRocksDB . getBalances
  xPubBals =
    runRocksDB . xPubBals
  xPubUnspents x l =
    runRocksDB . xPubUnspents x l
  xPubTxs x l =
    runRocksDB . xPubTxs x l
  xPubTxCount x =
    runRocksDB . xPubTxCount x

-- | Run block store process.
withBlockStore ::
  (MonadUnliftIO m, MonadLoggerIO m) =>
  BlockStoreConfig ->
  (BlockStore -> m a) ->
  m a
withBlockStore cfg action = do
  pb <- newTVarIO Nothing
  ts <- newTVarIO HashMap.empty
  rq <- newTVarIO HashSet.empty
  inbox <- newInbox
  metrics <- mapM newStoreMetrics (blockConfStats cfg)
  let r =
        BlockStore
          { myMailbox = inboxToMailbox inbox,
            myConfig = cfg,
            myPeer = pb,
            myTxs = ts,
            requested = rq,
            myMetrics = metrics
          }
  withAsync (runReaderT (go inbox) r) $ \a -> do
    link a
    action r
  where
    go inbox = do
      ini
      wipe
      run inbox
    del txs = do
      $(logInfoS) "BlockStore" $
        "Deleting " <> cs (show (length txs)) <> " transactions"
      forM_ txs $ \(_, th) -> deleteUnconfirmedTx False th
    wipe_it txs = do
      let (txs1, txs2) = splitAt 1000 txs
      unless (null txs1) $
        runImport (del txs1) >>= \case
          Left e -> do
            $(logErrorS) "BlockStore" $
              "Could not wipe mempool: " <> cs (show e)
            throwIO e
          Right () -> wipe_it txs2
    wipe
      | blockConfWipeMempool cfg =
        getMempool >>= wipe_it
      | otherwise =
        return ()
    ini =
      runImport initBest >>= \case
        Left e -> do
          $(logErrorS) "BlockStore" $
            "Could not initialize: " <> cs (show e)
          throwIO e
        Right () -> return ()
    run inbox =
      withAsync (pingMe (inboxToMailbox inbox)) $
        const $
          forever $
            receive inbox
              >>= ReaderT . runReaderT . processBlockStoreMessage

isInSync :: MonadLoggerIO m => BlockT m Bool
isInSync =
  getBestBlock >>= \case
    Nothing -> do
      $(logErrorS) "BlockStore" "Block database uninitialized"
      throwIO Uninitialized
    Just bb -> do
      cb <- asks (blockConfChain . myConfig) >>= chainGetBest
      if headerHash (nodeHeader cb) == bb
        then clearSyncingState >> return True
        else return False

guardMempool :: Monad m => BlockT m () -> BlockT m ()
guardMempool f = do
  n <- asks (blockConfNoMempool . myConfig)
  unless n f

syncMempool :: Monad m => BlockT m () -> BlockT m ()
syncMempool f = do
  s <- asks (blockConfSyncMempool . myConfig)
  when s f

mempool :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()
mempool p =
  guardMempool . syncMempool $
    isInSync >>= \s -> when s $ do
      $(logDebugS) "BlockStore" $
        "Requesting mempool from peer: " <> peerText p
      MMempool `sendMessage` p

processBlock ::
  (MonadUnliftIO m, MonadLoggerIO m) =>
  Peer ->
  Block ->
  BlockT m ()
processBlock peer block = void . runMaybeT $ do
  checkPeer peer >>= \case
    True -> return ()
    False -> do
      $(logErrorS) "BlockStore" $
        "Non-syncing peer " <> peerText peer
          <> " sent me a block: "
          <> blockHashToHex blockhash
      PeerMisbehaving "Sent unexpected block" `killPeer` peer
      mzero
  node <-
    getBlockNode blockhash >>= \case
      Just b -> return b
      Nothing -> do
        $(logErrorS) "BlockStore" $
          "Peer " <> peerText peer
            <> " sent unknown block: "
            <> blockHashToHex blockhash
        PeerMisbehaving "Sent unknown block" `killPeer` peer
        mzero
  $(logDebugS) "BlockStore" $
    "Processing block: " <> blockText node Nothing
      <> " from peer: "
      <> peerText peer
  lift . notify (Just block) $
    runImport (importBlock block node) >>= \case
      Left e -> failure e
      Right _ -> success node
  where
    header = blockHeader block
    blockhash = headerHash header
    hexhash = blockHashToHex blockhash
    success node = do
      $(logInfoS) "BlockStore" $
        "Best block: " <> blockText node (Just block)
      removeSyncingBlock $ headerHash $ nodeHeader node
      touchPeer
      isInSync >>= \case
        False -> syncMe
        True -> do
          updateOrphans
          mempool peer
    failure e = do
      $(logErrorS) "BlockStore" $
        "Error importing block " <> hexhash
          <> " from peer: "
          <> peerText peer
          <> ": "
          <> cs (show e)
      killPeer (PeerMisbehaving (show e)) peer

setSyncingBlocks ::
  (MonadReader BlockStore m, MonadIO m) =>
  [BlockHash] ->
  m ()
setSyncingBlocks hs =
  asks myPeer >>= \box ->
    atomically $
      modifyTVar box $ \case
        Nothing -> Nothing
        Just x -> Just x {syncingBlocks = hs}

getSyncingBlocks :: (MonadReader BlockStore m, MonadIO m) => m [BlockHash]
getSyncingBlocks =
  asks myPeer >>= readTVarIO >>= \case
    Nothing -> return []
    Just x -> return $ syncingBlocks x

addSyncingBlocks ::
  (MonadReader BlockStore m, MonadIO m) =>
  [BlockHash] ->
  m ()
addSyncingBlocks hs =
  asks myPeer >>= \box ->
    atomically $
      modifyTVar box $ \case
        Nothing -> Nothing
        Just x -> Just x {syncingBlocks = syncingBlocks x <> hs}

removeSyncingBlock ::
  (MonadReader BlockStore m, MonadIO m) =>
  BlockHash ->
  m ()
removeSyncingBlock h = do
  box <- asks myPeer
  atomically $
    modifyTVar box $ \case
      Nothing -> Nothing
      Just x -> Just x {syncingBlocks = delete h (syncingBlocks x)}

checkPeer :: (MonadLoggerIO m, MonadReader BlockStore m) => Peer -> m Bool
checkPeer p =
  fmap syncingPeer <$> getSyncingState >>= \case
    Nothing -> return False
    Just p' -> return $ p == p'

getBlockNode ::
  (MonadLoggerIO m, MonadReader BlockStore m) =>
  BlockHash ->
  m (Maybe BlockNode)
getBlockNode blockhash =
  chainGetBlock blockhash =<< asks (blockConfChain . myConfig)

processNoBlocks ::
  MonadLoggerIO m =>
  Peer ->
  [BlockHash] ->
  BlockT m ()
processNoBlocks p hs = do
  forM_ (zip [(1 :: Int) ..] hs) $ \(i, h) ->
    $(logErrorS) "BlockStore" $
      "Block "
        <> cs (show i)
        <> "/"
        <> cs (show (length hs))
        <> " "
        <> blockHashToHex h
        <> " not found by peer: "
        <> peerText p
  killPeer (PeerMisbehaving "Did not find requested block(s)") p

processTx :: MonadLoggerIO m => Peer -> Tx -> BlockT m ()
processTx p tx = guardMempool $ do
  t <- liftIO getCurrentTime
  $(logDebugS) "BlockManager" $
    "Received tx " <> txHashToHex (txHash tx)
      <> " by peer: "
      <> peerText p
  addPendingTx $ PendingTx t tx HashSet.empty

pruneOrphans :: MonadIO m => BlockT m ()
pruneOrphans = guardMempool $ do
  ts <- asks myTxs
  now <- liftIO getCurrentTime
  atomically . modifyTVar ts . HashMap.filter $ \p ->
    now `diffUTCTime` pendingTxTime p > 600

addPendingTx :: MonadIO m => PendingTx -> BlockT m ()
addPendingTx p = do
  ts <- asks myTxs
  rq <- asks requested
  atomically $ do
    modifyTVar ts $ HashMap.insert th p
    modifyTVar rq $ HashSet.delete th
    HashMap.size <$> readTVar ts
  setPendingTxs
  where
    th = txHash (pendingTx p)

addRequestedTx :: MonadIO m => TxHash -> BlockT m ()
addRequestedTx th = do
  qbox <- asks requested
  atomically $ modifyTVar qbox $ HashSet.insert th
  liftIO . void . async $ do
    threadDelay 20000000
    atomically $ modifyTVar qbox $ HashSet.delete th

isPending :: MonadIO m => TxHash -> BlockT m Bool
isPending th = do
  tbox <- asks myTxs
  qbox <- asks requested
  atomically $ do
    ts <- readTVar tbox
    rs <- readTVar qbox
    return $
      th `HashMap.member` ts
        || th `HashSet.member` rs

pendingTxs :: MonadIO m => Int -> BlockT m [PendingTx]
pendingTxs i = do
  selected <-
    asks myTxs >>= \box -> atomically $ do
      pending <- readTVar box
      let (selected, rest) = select pending
      writeTVar box rest
      return (selected)
  setPendingTxs
  return selected
  where
    select pend =
      let eligible = HashMap.filter (null . pendingDeps) pend
          orphans = HashMap.difference pend eligible
          selected = take i $ sortit eligible
          remaining = HashMap.filter (`notElem` selected) eligible
       in (selected, remaining <> orphans)
    sortit m =
      let sorted = sortTxs $ map pendingTx $ HashMap.elems m
          txids = map (txHash . snd) sorted
       in mapMaybe (`HashMap.lookup` m) txids

fulfillOrphans :: MonadIO m => BlockStore -> TxHash -> m ()
fulfillOrphans block_read th =
  atomically $ modifyTVar box (HashMap.map fulfill)
  where
    box = myTxs block_read
    fulfill p = p {pendingDeps = HashSet.delete th (pendingDeps p)}

updateOrphans ::
  ( StoreReadBase m,
    MonadLoggerIO m,
    MonadReader BlockStore m
  ) =>
  m ()
updateOrphans = do
  box <- asks myTxs
  pending <- readTVarIO box
  let orphans = HashMap.filter (not . null . pendingDeps) pending
  updated <- forM orphans $ \p -> do
    let tx = pendingTx p
    exists (txHash tx) >>= \case
      True -> return Nothing
      False -> Just <$> fill_deps p
  let pruned = HashMap.map fromJust $ HashMap.filter isJust updated
  atomically $ writeTVar box pruned
  where
    exists th =
      getTxData th >>= \case
        Nothing -> return False
        Just TxData {txDataDeleted = True} -> return False
        Just TxData {txDataDeleted = False} -> return True
    prev_utxos tx = catMaybes <$> mapM (getUnspent . prevOutput) (txIn tx)
    fulfill p unspent =
      let unspent_hash = outPointHash (unspentPoint unspent)
          new_deps = HashSet.delete unspent_hash (pendingDeps p)
       in p {pendingDeps = new_deps}
    fill_deps p = do
      let tx = pendingTx p
      unspents <- prev_utxos tx
      return $ foldl fulfill p unspents

newOrphanTx ::
  MonadLoggerIO m =>
  BlockStore ->
  UTCTime ->
  Tx ->
  WriterT m ()
newOrphanTx block_read time tx = do
  $(logDebugS) "BlockStore" $
    "Import tx "
      <> txHashToHex (txHash tx)
      <> ": Orphan"
  let box = myTxs block_read
  unspents <- catMaybes <$> mapM getUnspent prevs
  let unspent_set = HashSet.fromList (map unspentPoint unspents)
      missing_set = HashSet.difference prev_set unspent_set
      missing_txs = HashSet.map outPointHash missing_set
  atomically . modifyTVar box $
    HashMap.insert
      (txHash tx)
      PendingTx
        { pendingTxTime = time,
          pendingTx = tx,
          pendingDeps = missing_txs
        }
  where
    prev_set = HashSet.fromList prevs
    prevs = map prevOutput (txIn tx)

importMempoolTx ::
  (MonadLoggerIO m, MonadError ImportException m) =>
  BlockStore ->
  UTCTime ->
  Tx ->
  WriterT m Bool
importMempoolTx block_read time tx =
  catchError new_mempool_tx handle_error
  where
    tx_hash = txHash tx
    handle_error Orphan = do
      newOrphanTx block_read time tx
      return False
    handle_error _ = return False
    seconds = floor (utcTimeToPOSIXSeconds time)
    new_mempool_tx =
      newMempoolTx tx seconds >>= \case
        True -> do
          $(logInfoS) "BlockStore" $
            "Import tx " <> txHashToHex (txHash tx)
              <> ": OK"
          fulfillOrphans block_read tx_hash
          return True
        False -> do
          $(logDebugS) "BlockStore" $
            "Import tx " <> txHashToHex (txHash tx)
              <> ": Already imported"
          return False

notify :: MonadIO m => Maybe Block -> BlockT m a -> BlockT m a
notify block go = do
  old <- HashSet.union e . HashSet.fromList . map snd <$> getMempool
  x <- go
  new <- HashSet.union e . HashSet.fromList . map snd <$> getMempool
  l <- asks (blockConfListener . myConfig)
  forM_ (old `HashSet.difference` new) $ \h ->
    publish (StoreMempoolDelete h) l
  forM_ (new `HashSet.difference` old) $ \h ->
    publish (StoreMempoolNew h) l
  case block of
    Just b -> publish (StoreBestBlock (headerHash (blockHeader b))) l
    Nothing -> return ()
  return x
  where
    e = case block of
      Just b -> HashSet.fromList (map txHash (blockTxns b))
      Nothing -> HashSet.empty

processMempool :: MonadLoggerIO m => BlockT m ()
processMempool = guardMempool . notify Nothing $ do
  txs <- pendingTxs 2000
  block_read <- ask
  unless (null txs) (import_txs block_read txs)
  where
    run_import block_read p =
      let t = pendingTx p
          h = txHash t
       in importMempoolTx block_read (pendingTxTime p) (pendingTx p)
    import_txs block_read txs =
      let r = mapM (run_import block_read) txs
       in runImport r >>= \case
            Left e -> report_error e
            Right _ -> return ()
    report_error e = do
      $(logErrorS) "BlockImport" $
        "Error processing mempool: " <> cs (show e)
      throwIO e

processTxs ::
  MonadLoggerIO m =>
  Peer ->
  [TxHash] ->
  BlockT m ()
processTxs p hs = guardMempool $ do
  s <- isInSync
  when s $ do
    $(logDebugS) "BlockStore" $
      "Received inventory with "
        <> cs (show (length hs))
        <> " transactions from peer: "
        <> peerText p
    xs <- catMaybes <$> zip_counter process_tx
    unless (null xs) $ go xs
  where
    len = length hs
    zip_counter = forM (zip [(1 :: Int) ..] hs) . uncurry
    process_tx i h =
      isPending h >>= \case
        True -> do
          $(logDebugS) "BlockStore" $
            "Tx " <> cs (show i) <> "/" <> cs (show len)
              <> " "
              <> txHashToHex h
              <> ": "
              <> "Pending"
          return Nothing
        False ->
          getActiveTxData h >>= \case
            Just _ -> do
              $(logDebugS) "BlockStore" $
                "Tx " <> cs (show i) <> "/" <> cs (show len)
                  <> " "
                  <> txHashToHex h
                  <> ": "
                  <> "Already Imported"
              return Nothing
            Nothing -> do
              $(logDebugS) "BlockStore" $
                "Tx " <> cs (show i) <> "/" <> cs (show len)
                  <> " "
                  <> txHashToHex h
                  <> ": "
                  <> "Requesting"
              return (Just h)
    go xs = do
      mapM_ addRequestedTx xs
      net <- asks (blockConfNet . myConfig)
      let inv = if getSegWit net then InvWitnessTx else InvTx
          vec = map (InvVector inv . getTxHash) xs
          msg = MGetData (GetData vec)
      msg `sendMessage` p

touchPeer ::
  ( MonadIO m,
    MonadReader BlockStore m
  ) =>
  m ()
touchPeer =
  getSyncingState >>= \case
    Nothing -> return ()
    Just _ -> do
      box <- asks myPeer
      now <- liftIO getCurrentTime
      atomically $
        modifyTVar box $
          fmap $ \x -> x {syncingTime = now}

checkTime :: MonadLoggerIO m => BlockT m ()
checkTime =
  asks myPeer >>= readTVarIO >>= \case
    Nothing -> return ()
    Just
      Syncing
        { syncingTime = t,
          syncingPeer = p
        } -> do
        now <- liftIO getCurrentTime
        peer_time_out <- asks (blockConfPeerTimeout . myConfig)
        when (now `diffUTCTime` t > peer_time_out) $ do
          $(logErrorS) "BlockStore" $
            "Syncing peer timeout: " <> peerText p
          killPeer PeerTimeout p

revertToMainChain :: MonadLoggerIO m => BlockT m ()
revertToMainChain = do
  h <- headerHash . nodeHeader <$> getBest
  ch <- asks (blockConfChain . myConfig)
  chainBlockMain h ch >>= \x -> unless x $ do
    $(logWarnS) "BlockStore" $
      "Reverting best block: "
        <> blockHashToHex h
    runImport (revertBlock h) >>= \case
      Left e -> do
        $(logErrorS) "BlockStore" $
          "Could not revert block "
            <> blockHashToHex h
            <> ": "
            <> cs (show e)
        throwIO e
      Right () -> setSyncingBlocks []
    revertToMainChain

getBest :: MonadLoggerIO m => BlockT m BlockNode
getBest = do
  bb <-
    getBestBlock >>= \case
      Just b -> return b
      Nothing -> do
        $(logErrorS) "BlockStore" "No best block set"
        throwIO Uninitialized
  ch <- asks (blockConfChain . myConfig)
  chainGetBlock bb ch >>= \case
    Just x -> return x
    Nothing -> do
      $(logErrorS) "BlockStore" $
        "Header not found for best block: "
          <> blockHashToHex bb
      throwIO (BlockNotInChain bb)

getSyncBest :: MonadLoggerIO m => BlockT m BlockNode
getSyncBest = do
  bb <-
    getSyncingBlocks >>= \case
      [] ->
        getBestBlock >>= \case
          Just b -> return b
          Nothing -> do
            $(logErrorS) "BlockStore" "No best block set"
            throwIO Uninitialized
      hs -> return $ last hs
  ch <- asks (blockConfChain . myConfig)
  chainGetBlock bb ch >>= \case
    Just x -> return x
    Nothing -> do
      $(logErrorS) "BlockStore" $
        "Header not found for block: "
          <> blockHashToHex bb
      throwIO (BlockNotInChain bb)

shouldSync :: MonadLoggerIO m => BlockT m (Maybe Peer)
shouldSync =
  isInSync >>= \case
    True -> return Nothing
    False ->
      getSyncingState >>= \case
        Nothing -> return Nothing
        Just Syncing {syncingPeer = p, syncingBlocks = bs}
          | 100 > length bs -> return (Just p)
          | otherwise -> return Nothing

syncMe :: MonadLoggerIO m => BlockT m ()
syncMe = do
  revertToMainChain
  shouldSync >>= \case
    Nothing -> return ()
    Just p -> do
      bb <- getSyncBest
      bh <- getbh
      when (bb /= bh) $ do
        bns <- sel bb bh
        iv <- getiv bns
        $(logDebugS) "BlockStore" $
          "Requesting "
            <> fromString (show (length iv))
            <> " blocks from peer: "
            <> peerText p
        addSyncingBlocks $ map (headerHash . nodeHeader) bns
        MGetData (GetData iv) `sendMessage` p
  where
    getiv bns = do
      w <- getSegWit <$> asks (blockConfNet . myConfig)
      let i = if w then InvWitnessBlock else InvBlock
          f = InvVector i . getBlockHash . headerHash . nodeHeader
      return $ map f bns
    getbh =
      chainGetBest =<< asks (blockConfChain . myConfig)
    sel bb bh = do
      let sh = geth bb bh
      t <- top sh bh
      ch <- asks (blockConfChain . myConfig)
      ps <- chainGetParents (nodeHeight bb + 1) t ch
      return $
        if 500 > length ps
          then ps <> [bh]
          else ps
    geth bb bh =
      min
        (nodeHeight bb + 501)
        (nodeHeight bh)
    top sh bh =
      if sh == nodeHeight bh
        then return bh
        else findAncestor sh bh

findAncestor ::
  (MonadLoggerIO m, MonadReader BlockStore m) =>
  BlockHeight ->
  BlockNode ->
  m BlockNode
findAncestor height target = do
  ch <- asks (blockConfChain . myConfig)
  chainGetAncestor height target ch >>= \case
    Just ancestor -> return ancestor
    Nothing -> do
      let h = headerHash $ nodeHeader target
      $(logErrorS) "BlockStore" $
        "Could not find header for ancestor of block "
          <> blockHashToHex h
          <> " at height "
          <> cs (show (nodeHeight target))
      throwIO $ AncestorNotInChain height h

finishPeer ::
  (MonadLoggerIO m, MonadReader BlockStore m) =>
  Peer ->
  m ()
finishPeer p = do
  box <- asks myPeer
  readTVarIO box >>= \case
    Just Syncing {syncingPeer = p'} | p == p' -> reset_it box
    _ -> return ()
  where
    reset_it box = do
      atomically $ writeTVar box Nothing
      $(logDebugS) "BlockStore" $ "Releasing peer: " <> peerText p
      setFree p

trySetPeer :: MonadLoggerIO m => Peer -> BlockT m Bool
trySetPeer p =
  getSyncingState >>= \case
    Just _ -> return False
    Nothing -> set_it
  where
    set_it =
      setBusy p >>= \case
        False -> return False
        True -> do
          $(logDebugS) "BlockStore" $
            "Locked peer: " <> peerText p
          box <- asks myPeer
          now <- liftIO getCurrentTime
          atomically . writeTVar box $
            Just
              Syncing
                { syncingPeer = p,
                  syncingTime = now,
                  syncingBlocks = []
                }
          return True

trySyncing :: MonadLoggerIO m => BlockT m ()
trySyncing =
  isInSync >>= \case
    True -> return ()
    False ->
      getSyncingState >>= \case
        Just _ -> return ()
        Nothing -> online_peer
  where
    recurse [] = return ()
    recurse (p : ps) =
      trySetPeer p >>= \case
        False -> recurse ps
        True -> syncMe
    online_peer = do
      ops <- getPeers =<< asks (blockConfManager . myConfig)
      let ps = map onlinePeerMailbox ops
      recurse ps

trySyncingPeer :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()
trySyncingPeer p =
  isInSync >>= \case
    True -> mempool p
    False ->
      trySetPeer p >>= \case
        False -> return ()
        True -> syncMe

getSyncingState ::
  (MonadIO m, MonadReader BlockStore m) => m (Maybe Syncing)
getSyncingState =
  readTVarIO =<< asks myPeer

clearSyncingState ::
  (MonadLoggerIO m, MonadReader BlockStore m) => m ()
clearSyncingState =
  asks myPeer >>= readTVarIO >>= \case
    Nothing -> return ()
    Just Syncing {syncingPeer = p} -> finishPeer p

processBlockStoreMessage ::
  (MonadUnliftIO m, MonadLoggerIO m) =>
  BlockStoreMessage ->
  BlockT m ()
processBlockStoreMessage (BlockNewBest _) =
  trySyncing
processBlockStoreMessage (BlockPeerConnect p) =
  trySyncingPeer p
processBlockStoreMessage (BlockPeerDisconnect p) =
  finishPeer p
processBlockStoreMessage (BlockReceived p b) =
  processBlock p b
processBlockStoreMessage (BlockNotFound p bs) =
  processNoBlocks p bs
processBlockStoreMessage (TxRefReceived p tx) =
  processTx p tx
processBlockStoreMessage (TxRefAvailable p ts) =
  processTxs p ts
processBlockStoreMessage (BlockPing r) = do
  setStoreHeight
  setHeadersHeight
  setPendingTxs
  setPeersConnected
  setMempoolSize
  trySyncing
  processMempool
  pruneOrphans
  checkTime
  atomically (r ())

pingMe :: MonadLoggerIO m => Mailbox BlockStoreMessage -> m ()
pingMe mbox =
  forever $ do
    BlockPing `query` mbox
    delay <-
      liftIO $
        randomRIO
          ( 100 * 1000,
            1000 * 1000
          )
    threadDelay delay

blockStorePeerConnect :: MonadIO m => Peer -> BlockStore -> m ()
blockStorePeerConnect peer store =
  BlockPeerConnect peer `send` myMailbox store

blockStorePeerDisconnect ::
  MonadIO m => Peer -> BlockStore -> m ()
blockStorePeerDisconnect peer store =
  BlockPeerDisconnect peer `send` myMailbox store

blockStoreHead ::
  MonadIO m => BlockNode -> BlockStore -> m ()
blockStoreHead node store =
  BlockNewBest node `send` myMailbox store

blockStoreBlock ::
  MonadIO m => Peer -> Block -> BlockStore -> m ()
blockStoreBlock peer block store =
  BlockReceived peer block `send` myMailbox store

blockStoreNotFound ::
  MonadIO m => Peer -> [BlockHash] -> BlockStore -> m ()
blockStoreNotFound peer blocks store =
  BlockNotFound peer blocks `send` myMailbox store

blockStoreTx ::
  MonadIO m => Peer -> Tx -> BlockStore -> m ()
blockStoreTx peer tx store =
  TxRefReceived peer tx `send` myMailbox store

blockStoreTxHash ::
  MonadIO m => Peer -> [TxHash] -> BlockStore -> m ()
blockStoreTxHash peer txhashes store =
  TxRefAvailable peer txhashes `send` myMailbox store

blockStorePeerConnectSTM ::
  Peer -> BlockStore -> STM ()
blockStorePeerConnectSTM peer store =
  BlockPeerConnect peer `sendSTM` myMailbox store

blockStorePeerDisconnectSTM ::
  Peer -> BlockStore -> STM ()
blockStorePeerDisconnectSTM peer store =
  BlockPeerDisconnect peer `sendSTM` myMailbox store

blockStoreHeadSTM ::
  BlockNode -> BlockStore -> STM ()
blockStoreHeadSTM node store =
  BlockNewBest node `sendSTM` myMailbox store

blockStoreBlockSTM ::
  Peer -> Block -> BlockStore -> STM ()
blockStoreBlockSTM peer block store =
  BlockReceived peer block `sendSTM` myMailbox store

blockStoreNotFoundSTM ::
  Peer -> [BlockHash] -> BlockStore -> STM ()
blockStoreNotFoundSTM peer blocks store =
  BlockNotFound peer blocks `sendSTM` myMailbox store

blockStoreTxSTM ::
  Peer -> Tx -> BlockStore -> STM ()
blockStoreTxSTM peer tx store =
  TxRefReceived peer tx `sendSTM` myMailbox store

blockStoreTxHashSTM ::
  Peer -> [TxHash] -> BlockStore -> STM ()
blockStoreTxHashSTM peer txhashes store =
  TxRefAvailable peer txhashes `sendSTM` myMailbox store

blockStorePendingTxs ::
  MonadIO m => BlockStore -> m Int
blockStorePendingTxs =
  atomically . blockStorePendingTxsSTM

blockStorePendingTxsSTM ::
  BlockStore -> STM Int
blockStorePendingTxsSTM BlockStore {..} = do
  x <- HashMap.keysSet <$> readTVar myTxs
  y <- readTVar requested
  return $ HashSet.size $ x `HashSet.union` y

blockText :: BlockNode -> Maybe Block -> Text
blockText bn mblock = case mblock of
  Nothing ->
    height <> sep <> time <> sep <> hash
  Just block ->
    height <> sep <> time <> sep <> hash <> sep <> size block
  where
    height = cs $ show (nodeHeight bn)
    systime =
      posixSecondsToUTCTime $
        fromIntegral $
          blockTimestamp $
            nodeHeader bn
    time =
      cs $
        formatTime
          defaultTimeLocale
          (iso8601DateFormat (Just "%H:%M"))
          systime
    hash = blockHashToHex (headerHash (nodeHeader bn))
    sep = " | "
    size = (<> " bytes") . cs . show . B.length . encode