packages feed

haskoin-store-0.64.1: 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