packages feed

haskoin-store-0.64.0: src/Haskoin/Store/Database/Reader.hs

{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
module Haskoin.Store.Database.Reader
    ( -- * RocksDB Database Access
      DatabaseReader (..)
    , DatabaseReaderT
    , withDatabaseReader
    , addrTxCF
    , addrOutCF
    , txCF
    , spenderCF
    , unspentCF
    , blockCF
    , heightCF
    , balanceCF
    ) where

import           Conduit                      (ConduitT, dropWhileC, lift, mapC,
                                               runConduit, sinkList, (.|))
import           Control.Monad.Except         (runExceptT, throwError)
import           Control.Monad.Reader         (ReaderT, ask, asks, runReaderT)
import           Data.Bits                    ((.&.))
import qualified Data.ByteString              as BS
import           Data.Default                 (def)
import           Data.Function                (on)
import           Data.List                    (sortOn)
import           Data.Maybe                   (fromMaybe)
import           Data.Ord                     (Down (..))
import           Data.Serialize               (encode)
import           Data.Word                    (Word32, Word64)
import           Database.RocksDB             (ColumnFamily, Config (..),
                                               DB (..), Iterator, withDBCF,
                                               withIterCF)
import           Database.RocksDB.Query       (insert, matching,
                                               matchingAsListCF, matchingSkip,
                                               retrieve, retrieveCF)
import           Haskoin                      (Address, BlockHash, BlockHeight,
                                               Network, OutPoint (..), TxHash,
                                               pubSubKey, txHash)
import           Haskoin.Store.Common
import           Haskoin.Store.Data
import           Haskoin.Store.Database.Types
import qualified System.Metrics               as Metrics
import           System.Metrics.Counter       (Counter)
import qualified System.Metrics.Counter       as Counter
import           UnliftIO                     (MonadIO, MonadUnliftIO, liftIO)

type DatabaseReaderT = ReaderT DatabaseReader

data DatabaseReader =
    DatabaseReader
        { databaseHandle     :: !DB
        , databaseMaxGap     :: !Word32
        , databaseInitialGap :: !Word32
        , databaseNetwork    :: !Network
        , databaseMetrics    :: !(Maybe DataMetrics)
        }

incrementCounter :: MonadIO m
                 => (DataMetrics -> Counter)
                 -> Int
                 -> ReaderT DatabaseReader m ()
incrementCounter f i =
    asks databaseMetrics >>= \case
        Just s  -> liftIO $ Counter.add (f s) (fromIntegral i)
        Nothing -> return ()

dataVersion :: Word32
dataVersion = 17

withDatabaseReader :: MonadUnliftIO m
                   => Network
                   -> Word32
                   -> Word32
                   -> FilePath
                   -> Maybe DataMetrics
                   -> DatabaseReaderT m a
                   -> m a
withDatabaseReader net igap gap dir stats f =
    withDBCF dir cfg columnFamilyConfig $ \db -> do
    let bdb =
            DatabaseReader
                { databaseHandle = db
                , databaseMaxGap = gap
                , databaseNetwork = net
                , databaseInitialGap = igap
                , databaseMetrics = stats
                }
    initRocksDB bdb
    runReaderT f bdb
  where
    cfg = def{createIfMissing = True, maxFiles = Just (-1)}

columnFamilyConfig :: [(String, Config)]
columnFamilyConfig =
          [ ("addr-tx",     def{prefixLength = Just 22, bloomFilter = True})
          , ("addr-out",    def{prefixLength = Just 22, bloomFilter = True})
          , ("tx",          def{prefixLength = Just 33, bloomFilter = True})
          , ("spender",     def{prefixLength = Just 33, bloomFilter = True})
          , ("unspent",     def{prefixLength = Just 37, bloomFilter = True})
          , ("block",       def{prefixLength = Just 33, bloomFilter = True})
          , ("height",      def{prefixLength = Nothing, bloomFilter = True})
          , ("balance",     def{prefixLength = Just 22, bloomFilter = True})
          ]

addrTxCF :: DB -> ColumnFamily
addrTxCF = head . columnFamilies

addrOutCF :: DB -> ColumnFamily
addrOutCF db = columnFamilies db !! 1

txCF :: DB -> ColumnFamily
txCF db = columnFamilies db !! 2

spenderCF :: DB -> ColumnFamily
spenderCF db = columnFamilies db !! 3

unspentCF :: DB -> ColumnFamily
unspentCF db = columnFamilies db !! 4

blockCF :: DB -> ColumnFamily
blockCF db = columnFamilies db !! 5

heightCF :: DB -> ColumnFamily
heightCF db = columnFamilies db !! 6

balanceCF :: DB -> ColumnFamily
balanceCF db = columnFamilies db !! 7

initRocksDB :: MonadIO m => DatabaseReader -> m ()
initRocksDB DatabaseReader{databaseHandle = db} = do
    e <-
        runExceptT $
        retrieve db VersionKey >>= \case
            Just v
                | v == dataVersion -> return ()
                | otherwise -> throwError "Incorrect RocksDB database version"
            Nothing -> setInitRocksDB db
    case e of
        Left s   -> error s
        Right () -> return ()

setInitRocksDB :: MonadIO m => DB -> m ()
setInitRocksDB db = insert db VersionKey dataVersion

addressConduit :: MonadUnliftIO m
               => Address
               -> Maybe Start
               -> Iterator
               -> ConduitT i TxRef (DatabaseReaderT m) ()
addressConduit a s it  =
    x .| mapC (uncurry f)
  where
    f (AddrTxKey _ t) () = t
    f _ _                = undefined
    x = case s of
        Nothing ->
            matching it (AddrTxKeyA a)
        Just (AtBlock bh) ->
            matchingSkip
                it
                (AddrTxKeyA a)
                (AddrTxKeyB a (BlockRef bh maxBound))
        Just (AtTx txh) ->
            lift (getTxData txh) >>= \case
                Just TxData {txDataBlock = b@BlockRef{}} ->
                    matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)
                Just TxData {txDataBlock = MemRef{}} ->
                    let cond (AddrTxKey _a (TxRef MemRef{} th)) =
                            th /= txh
                        cond (AddrTxKey _a (TxRef BlockRef{} _th)) =
                            False
                    in matching it (AddrTxKeyA a) .|
                       (dropWhileC (cond . fst) >> mapC id)
                Nothing -> return ()

unspentConduit :: MonadUnliftIO m
               => Address
               -> Maybe Start
               -> Iterator
               -> ConduitT i Unspent (DatabaseReaderT m) ()
unspentConduit a s it =
    x .| mapC (uncurry toUnspent)
  where
    x = case s of
        Nothing ->
            matching it (AddrOutKeyA a)
        Just (AtBlock h) ->
            matchingSkip
                it
                (AddrOutKeyA a)
                (AddrOutKeyB a (BlockRef h maxBound))
        Just (AtTx txh) ->
            lift (getTxData txh) >>= \case
                Just TxData {txDataBlock = b@BlockRef{}} ->
                    matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)
                Just TxData {txDataBlock = MemRef{}} ->
                    let cond (AddrOutKey _a MemRef{} p) =
                            outPointHash p /= txh
                        cond (AddrOutKey _a BlockRef{} _p) =
                            False
                    in matching it (AddrOutKeyA a) .|
                       (dropWhileC (cond . fst) >> mapC id)
                Nothing -> return ()

instance MonadIO m => StoreReadBase (DatabaseReaderT m) where
    getNetwork = asks databaseNetwork

    getTxData th = do
        db <- asks databaseHandle
        retrieveCF db (txCF db) (TxKey th) >>= \case
            Nothing -> return Nothing
            Just t  -> do
              incrementCounter dataTxCount 1
              return (Just t)

    getSpender op = do
        db <- asks databaseHandle
        retrieveCF db (spenderCF db) (SpenderKey op) >>= \case
            Nothing -> return Nothing
            Just s -> do
                incrementCounter dataSpenderCount 1
                return (Just s)

    getUnspent p = do
        db <- asks databaseHandle
        fmap (valToUnspent p) <$> retrieveCF db (unspentCF db) (UnspentKey p) >>= \case
            Nothing -> return Nothing
            Just u  -> do
                incrementCounter dataUnspentCount 1
                return (Just u)

    getBalance a = do
        db <- asks databaseHandle
        incrementCounter dataBalanceCount 1
        fmap (valToBalance a) <$> retrieveCF db (balanceCF db) (BalKey a)

    getMempool = do
        db <- asks databaseHandle
        incrementCounter dataMempoolCount 1
        fromMaybe [] <$> retrieve db MemKey

    getBestBlock = do
        incrementCounter dataBestCount 1
        asks databaseHandle >>= (`retrieve` BestKey)

    getBlocksAtHeight h = do
        db <- asks databaseHandle
        retrieveCF db (heightCF db) (HeightKey h) >>= \case
            Nothing -> return []
            Just ls -> do
              incrementCounter dataBlockCount (length ls)
              return ls

    getBlock h = do
        db <- asks databaseHandle
        retrieveCF db (blockCF db) (BlockKey h) >>= \case
            Nothing -> return Nothing
            Just b  -> do
              incrementCounter dataBlockCount 1
              return (Just b)

instance MonadUnliftIO m => StoreReadExtra (DatabaseReaderT m) where
    getAddressesTxs addrs limits = do
        txs <- applyLimits limits . sortOn Down . concat <$> mapM f addrs
        incrementCounter dataAddrTxCount (length txs)
        return txs
      where
        l = deOffset limits
        f a = do
            db <- asks databaseHandle
            withIterCF db (addrTxCF db) $ \it ->
                runConduit $
                addressConduit a (start l) it .|
                applyLimitC (limit l) .|
                sinkList

    getAddressesUnspents addrs limits = do
        us <- applyLimits limits . sortOn Down . concat <$> mapM f addrs
        incrementCounter dataUnspentCount (length us)
        return us
      where
        l = deOffset limits
        f a = do
            db <- asks databaseHandle
            withIterCF db (addrOutCF db) $ \it ->
                runConduit $
                unspentConduit a (start l) it .|
                applyLimitC (limit l) .|
                sinkList

    getAddressUnspents a limits = do
          db <- asks databaseHandle
          us <- withIterCF db (addrOutCF db) $ \it -> runConduit $
              x it .| applyLimitsC limits .| mapC (uncurry toUnspent) .| sinkList
          incrementCounter dataUnspentCount (length us)
          return us
        where
          x it = case start limits of
              Nothing ->
                  matching it (AddrOutKeyA a)
              Just (AtBlock h) ->
                  matchingSkip
                      it
                      (AddrOutKeyA a)
                      (AddrOutKeyB a (BlockRef h maxBound))
              Just (AtTx txh) ->
                  lift (getTxData txh) >>= \case
                      Just TxData {txDataBlock = b@BlockRef{}} ->
                          matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)
                      Just TxData {txDataBlock = MemRef{}} ->
                          let cond (AddrOutKey _a MemRef{} p) =
                                  outPointHash p /= txh
                              cond (AddrOutKey _a BlockRef{} _p) =
                                  False
                          in matching it (AddrOutKeyA a) .|
                            (dropWhileC (cond . fst) >> mapC id)
                      _ -> matching it (AddrOutKeyA a)

    getAddressTxs a limits = do
        db <- asks databaseHandle
        txs <- withIterCF db (addrTxCF db) $ \it ->
            runConduit $
            addressConduit a (start limits) it .|
            applyLimitsC limits .|
            sinkList
        incrementCounter dataAddrTxCount (length txs)
        return txs

    getMaxGap = asks databaseMaxGap

    getInitialGap = asks databaseInitialGap

    getNumTxData i = do
        db <- asks databaseHandle
        let (sk, w) = decodeTxKey i
        ls <- liftIO $ matchingAsListCF db (txCF db) (TxKeyS sk)
        let f t = let bs = encode $ txHash (txData t)
                      b = BS.head (BS.drop 6 bs)
                      w' = b .&. 0xf8
                  in w == w'
            txs = filter f $ map snd ls
        incrementCounter dataTxCount (length txs)
        return txs

    getBalances as = do
        zipWith f as <$> mapM getBalance as
      where
        f a Nothing  = zeroBalance a
        f _ (Just b) = b

    xPubBals xpub = do
        igap <- getInitialGap
        gap <- getMaxGap
        ext1 <- derive_until_gap gap 0 (take (fromIntegral igap) (aderiv 0 0))
        if all (nullBalance . xPubBal) ext1
            then do
                incrementCounter dataXPubBals (length ext1)
                return ext1
            else do
                ext2 <- derive_until_gap gap 0 (aderiv 0 igap)
                chg <- derive_until_gap gap 1 (aderiv 1 0)
                let bals = ext1 <> ext2 <> chg
                incrementCounter dataXPubBals (length bals)
                return bals
      where
        aderiv m =
            deriveAddresses
                (deriveFunction (xPubDeriveType xpub))
                (pubSubKey (xPubSpecKey xpub) m)
        xbalance m b n = XPubBal {xPubBalPath = [m, n], xPubBal = b}
        derive_until_gap _ _ [] = return []
        derive_until_gap gap m as = do
            let (as1, as2) = splitAt (fromIntegral gap) as
            bs <- getBalances (map snd as1)
            let xbs = zipWith (xbalance m) bs (map fst as1)
            if all nullBalance bs
                then return xbs
                else (xbs <>) <$> derive_until_gap gap m as2

    xPubUnspents _xspec xbals limits = do
        us <- concat <$> mapM h cs
        incrementCounter dataXPubUnspents (length us)
        return . applyLimits limits $ sortOn Down us
      where
        l = deOffset limits
        cs = filter ((> 0) . balanceUnspentCount . xPubBal) xbals
        i b = do
            us <- getAddressUnspents (balanceAddress (xPubBal b)) l
            return us
        f b t = XPubUnspent {xPubUnspentPath = xPubBalPath b, xPubUnspent = t}
        h b = map (f b) <$> i b

    xPubTxs _xspec xbals limits = do
        let as = map balanceAddress $
                 filter (not . nullBalance) $
                 map xPubBal xbals
        txs <- getAddressesTxs as limits
        incrementCounter dataXPubTxs (length txs)
        return txs

    xPubTxCount xspec xbals = do
        incrementCounter dataXPubTxCount 1
        fromIntegral . length <$> xPubTxs xspec xbals def