{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} module Haskoin.Store.Database.Writer ( Writer , runWriter ) where import Control.Applicative ((<|>)) import Control.DeepSeq (NFData) import Control.Monad.Except (MonadError) import Control.Monad.Reader (ReaderT) import qualified Control.Monad.Reader as R import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import qualified Data.ByteString.Short as B.Short import Data.Hashable (Hashable) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as M import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as I import Data.Maybe (fromJust, isJust, maybeToList) import Database.RocksDB (BatchOp) import Database.RocksDB.Query (deleteOp, insertOp, writeBatch) import GHC.Generics (Generic) import Haskoin (Address, BlockHash, BlockHeight, OutPoint (..), TxHash, headerHash, txHash) import Haskoin.Store.Common (StoreRead (..), StoreWrite (..)) import Haskoin.Store.Data (Balance (..), BlockData (..), BlockRef (..), Spender, TxData (..), TxRef (..), Unspent (..)) import Haskoin.Store.Database.Reader (DatabaseReader (..), withDatabaseReader) import Haskoin.Store.Database.Types (AddrOutKey (..), AddrTxKey (..), BalKey (..), BalVal (..), BestKey (..), BlockKey (..), HeightKey (..), MemKey (..), OutVal (..), SpenderKey (..), TxKey (..), UnspentKey (..), UnspentVal (..), balanceToVal, unspentToVal, valToBalance, valToUnspent) import UnliftIO (MonadIO, TVar, atomically, modifyTVar, newTVarIO, readTVarIO) data Dirty a = Modified a | Deleted deriving (Eq, Show, Generic, Hashable, NFData) instance Functor Dirty where fmap _ Deleted = Deleted fmap f (Modified a) = Modified (f a) data Writer = Writer { getReader :: !DatabaseReader , getState :: !(TVar Memory) } instance MonadIO m => StoreRead (ReaderT Writer m) where getInitialGap = R.asks (databaseInitialGap . getReader) getNetwork = R.asks (databaseNetwork . getReader) getBestBlock = R.ask >>= getBestBlockI getBlocksAtHeight h = R.ask >>= getBlocksAtHeightI h getBlock b = R.ask >>= getBlockI b getTxData t = R.ask >>= getTxDataI t getSpender p = R.ask >>= getSpenderI p getSpenders t = R.ask >>= getSpendersI t getUnspent a = R.ask >>= getUnspentI a getBalance a = R.ask >>= getBalanceI a getMempool = R.ask >>= getMempoolI getAddressesTxs = undefined getAddressesUnspents = undefined getMaxGap = R.asks (databaseMaxGap . getReader) instance MonadIO m => StoreWrite (ReaderT Writer m) where setBest h = R.ask >>= setBestI h insertBlock b = R.ask >>= insertBlockI b setBlocksAtHeight hs g = R.ask >>= setBlocksAtHeightI hs g insertTx t = R.ask >>= insertTxI t insertSpender p s = R.ask >>= insertSpenderI p s deleteSpender p = R.ask >>= deleteSpenderI p insertAddrTx a t = R.ask >>= insertAddrTxI a t deleteAddrTx a t = R.ask >>= deleteAddrTxI a t insertAddrUnspent a u = R.ask >>= insertAddrUnspentI a u deleteAddrUnspent a u = R.ask >>= deleteAddrUnspentI a u setMempool xs = R.ask >>= setMempoolI xs insertUnspent u = R.ask >>= insertUnspentI u deleteUnspent p = R.ask >>= deleteUnspentI p setBalance b = R.ask >>= setBalanceI b data Memory = Memory { hBest :: !(Maybe BlockHash) , hBlock :: !(HashMap BlockHash BlockData) , hHeight :: !(HashMap BlockHeight [BlockHash]) , hTx :: !(HashMap TxHash TxData) , hSpender :: !(HashMap TxHash (IntMap (Dirty Spender))) , hUnspent :: !(HashMap TxHash (IntMap (Dirty UnspentVal))) , hBalance :: !(HashMap Address BalVal) , hAddrTx :: !(HashMap Address (HashMap TxRef (Dirty ()))) , hAddrOut :: !(HashMap Address (HashMap BlockRef (HashMap OutPoint (Dirty OutVal)))) , hMempool :: !(Maybe [TxRef]) } deriving (Eq, Show) instance MonadIO m => StoreWrite (ReaderT (TVar Memory) m) where setBest h = do v <- R.ask atomically $ modifyTVar v (setBestH h) insertBlock b = do v <- R.ask atomically $ modifyTVar v (insertBlockH b) setBlocksAtHeight h g = do v <- R.ask atomically $ modifyTVar v (setBlocksAtHeightH h g) insertTx t = do v <- R.ask atomically $ modifyTVar v (insertTxH t) insertSpender p s = do v <- R.ask atomically $ modifyTVar v (insertSpenderH p s) deleteSpender p = do v <- R.ask atomically $ modifyTVar v (deleteSpenderH p) insertAddrTx a t = do v <- R.ask atomically $ modifyTVar v (insertAddrTxH a t) deleteAddrTx a t = do v <- R.ask atomically $ modifyTVar v (deleteAddrTxH a t) insertAddrUnspent a u = do v <- R.ask atomically $ modifyTVar v (insertAddrUnspentH a u) deleteAddrUnspent a u = do v <- R.ask atomically $ modifyTVar v (deleteAddrUnspentH a u) setMempool xs = do v <- R.ask atomically $ modifyTVar v (setMempoolH xs) setBalance b = do v <- R.ask atomically $ modifyTVar v (setBalanceH b) insertUnspent h = do v <- R.ask atomically $ modifyTVar v (insertUnspentH h) deleteUnspent p = do v <- R.ask atomically $ modifyTVar v (deleteUnspentH p) runWriter :: (MonadIO m, MonadError e m) => DatabaseReader -> ReaderT Writer m a -> m a runWriter bdb@DatabaseReader{databaseHandle = db} f = do hm <- newTVarIO emptyMemory x <- R.runReaderT f Writer {getReader = bdb, getState = hm} ops <- hashMapOps <$> readTVarIO hm writeBatch db ops return x hashMapOps :: Memory -> [BatchOp] hashMapOps db = bestBlockOp (hBest db) <> blockHashOps (hBlock db) <> blockHeightOps (hHeight db) <> txOps (hTx db) <> spenderOps (hSpender db) <> balOps (hBalance db) <> addrTxOps (hAddrTx db) <> addrOutOps (hAddrOut db) <> maybeToList (mempoolOp <$> hMempool db) <> unspentOps (hUnspent db) bestBlockOp :: Maybe BlockHash -> [BatchOp] bestBlockOp Nothing = [] bestBlockOp (Just b) = [insertOp BestKey b] blockHashOps :: HashMap BlockHash BlockData -> [BatchOp] blockHashOps = map (uncurry f) . M.toList where f = insertOp . BlockKey blockHeightOps :: HashMap BlockHeight [BlockHash] -> [BatchOp] blockHeightOps = map (uncurry f) . M.toList where f = insertOp . HeightKey txOps :: HashMap TxHash TxData -> [BatchOp] txOps = map (uncurry f) . M.toList where f = insertOp . TxKey spenderOps :: HashMap TxHash (IntMap (Dirty Spender)) -> [BatchOp] spenderOps = concatMap (uncurry f) . M.toList where f h = map (uncurry (g h)) . I.toList g h i (Modified s) = insertOp (SpenderKey (OutPoint h (fromIntegral i))) s g h i Deleted = deleteOp (SpenderKey (OutPoint h (fromIntegral i))) balOps :: HashMap Address BalVal -> [BatchOp] balOps = map (uncurry f) . M.toList where f = insertOp . BalKey addrTxOps :: HashMap Address (HashMap TxRef (Dirty ())) -> [BatchOp] addrTxOps = concatMap (uncurry f) . M.toList where f a = map (uncurry (g a)) . M.toList g a t (Modified ()) = insertOp (AddrTxKey a t) () g a t Deleted = deleteOp (AddrTxKey a t) addrOutOps :: HashMap Address (HashMap BlockRef (HashMap OutPoint (Dirty OutVal))) -> [BatchOp] addrOutOps = concat . concatMap (uncurry f) . M.toList where f a = map (uncurry (g a)) . M.toList g a b = map (uncurry (h a b)) . M.toList h a b p (Modified l) = insertOp (AddrOutKey { addrOutKeyA = a , addrOutKeyB = b , addrOutKeyP = p }) l h a b p Deleted = deleteOp AddrOutKey { addrOutKeyA = a , addrOutKeyB = b , addrOutKeyP = p } mempoolOp :: [TxRef] -> BatchOp mempoolOp = insertOp MemKey . map (\TxRef { txRefBlock = MemRef t , txRefHash = h } -> (t, h)) unspentOps :: HashMap TxHash (IntMap (Dirty UnspentVal)) -> [BatchOp] unspentOps = concatMap (uncurry f) . M.toList where f h = map (uncurry (g h)) . I.toList g h i (Modified u) = insertOp (UnspentKey (OutPoint h (fromIntegral i))) u g h i Deleted = deleteOp (UnspentKey (OutPoint h (fromIntegral i))) setBestI :: MonadIO m => BlockHash -> Writer -> m () setBestI bh Writer {getState = hm} = withMemory hm $ setBest bh insertBlockI :: MonadIO m => BlockData -> Writer -> m () insertBlockI b Writer {getState = hm} = withMemory hm $ insertBlock b setBlocksAtHeightI :: MonadIO m => [BlockHash] -> BlockHeight -> Writer -> m () setBlocksAtHeightI hs g Writer {getState = hm} = withMemory hm $ setBlocksAtHeight hs g insertTxI :: MonadIO m => TxData -> Writer -> m () insertTxI t Writer {getState = hm} = withMemory hm $ insertTx t insertSpenderI :: MonadIO m => OutPoint -> Spender -> Writer -> m () insertSpenderI p s Writer {getState = hm} = withMemory hm $ insertSpender p s deleteSpenderI :: MonadIO m => OutPoint -> Writer -> m () deleteSpenderI p Writer {getState = hm} = withMemory hm $ deleteSpender p insertAddrTxI :: MonadIO m => Address -> TxRef -> Writer -> m () insertAddrTxI a t Writer {getState = hm} = withMemory hm $ insertAddrTx a t deleteAddrTxI :: MonadIO m => Address -> TxRef -> Writer -> m () deleteAddrTxI a t Writer {getState = hm} = withMemory hm $ deleteAddrTx a t insertAddrUnspentI :: MonadIO m => Address -> Unspent -> Writer -> m () insertAddrUnspentI a u Writer {getState = hm} = withMemory hm $ insertAddrUnspent a u deleteAddrUnspentI :: MonadIO m => Address -> Unspent -> Writer -> m () deleteAddrUnspentI a u Writer {getState = hm} = withMemory hm $ deleteAddrUnspent a u setMempoolI :: MonadIO m => [TxRef] -> Writer -> m () setMempoolI xs Writer {getState = hm} = withMemory hm $ setMempool xs getBestBlockI :: MonadIO m => Writer -> m (Maybe BlockHash) getBestBlockI Writer {getState = hm, getReader = db} = runMaybeT $ MaybeT f <|> MaybeT g where f = getBestBlockH <$> readTVarIO hm g = withDatabaseReader db getBestBlock getBlocksAtHeightI :: MonadIO m => BlockHeight -> Writer -> m [BlockHash] getBlocksAtHeightI bh Writer {getState = hm, getReader = db} = getBlocksAtHeightH bh <$> readTVarIO hm >>= \case Just bs -> return bs Nothing -> withDatabaseReader db $ getBlocksAtHeight bh getBlockI :: MonadIO m => BlockHash -> Writer -> m (Maybe BlockData) getBlockI bh Writer {getReader = db, getState = hm} = runMaybeT $ MaybeT f <|> MaybeT g where f = getBlockH bh <$> readTVarIO hm g = withDatabaseReader db $ getBlock bh getTxDataI :: MonadIO m => TxHash -> Writer -> m (Maybe TxData) getTxDataI th Writer {getReader = db, getState = hm} = runMaybeT $ MaybeT f <|> MaybeT g where f = getTxDataH th <$> readTVarIO hm g = withDatabaseReader db $ getTxData th getSpenderI :: MonadIO m => OutPoint -> Writer -> m (Maybe Spender) getSpenderI op Writer {getReader = db, getState = hm} = getSpenderH op <$> readTVarIO hm >>= \case Just (Modified s) -> return (Just s) Just Deleted -> return Nothing Nothing -> withDatabaseReader db (getSpender op) getSpendersI :: MonadIO m => TxHash -> Writer -> m (IntMap Spender) getSpendersI t Writer {getReader = db, getState = hm} = do hsm <- fmap to_maybe . getSpendersH t <$> readTVarIO hm dsm <- fmap Just <$> withDatabaseReader db (getSpenders t) return $ I.map fromJust $ I.filter isJust $ hsm <> dsm where to_maybe Deleted = Nothing to_maybe (Modified x) = Just x getBalanceI :: MonadIO m => Address -> Writer -> m Balance getBalanceI a Writer {getReader = db, getState = hm} = getBalanceH a <$> readTVarIO hm >>= \case Just b -> return $ valToBalance a b Nothing -> withDatabaseReader db $ getBalance a setBalanceI :: MonadIO m => Balance -> Writer -> m () setBalanceI b Writer {getState = hm} = withMemory hm $ setBalance b getUnspentI :: MonadIO m => OutPoint -> Writer -> m (Maybe Unspent) getUnspentI op Writer {getReader = db, getState = hm} = getUnspentH op <$> readTVarIO hm >>= \case Just Deleted -> return Nothing Just (Modified u) -> return (Just (valToUnspent op u)) Nothing -> withDatabaseReader db (getUnspent op) insertUnspentI :: MonadIO m => Unspent -> Writer -> m () insertUnspentI u Writer {getState = hm} = withMemory hm $ insertUnspent u deleteUnspentI :: MonadIO m => OutPoint -> Writer -> m () deleteUnspentI p Writer {getState = hm} = withMemory hm $ deleteUnspent p getMempoolI :: MonadIO m => Writer -> m [TxRef] getMempoolI Writer {getState = hm, getReader = db} = getMempoolH <$> readTVarIO hm >>= \case Just xs -> return xs Nothing -> withDatabaseReader db getMempool withMemory :: MonadIO m => TVar Memory -> ReaderT (TVar Memory) m a -> m a withMemory = flip R.runReaderT emptyMemory :: Memory emptyMemory = Memory { hBest = Nothing , hBlock = M.empty , hHeight = M.empty , hTx = M.empty , hSpender = M.empty , hUnspent = M.empty , hBalance = M.empty , hAddrTx = M.empty , hAddrOut = M.empty , hMempool = Nothing } getBestBlockH :: Memory -> Maybe BlockHash getBestBlockH = hBest getBlocksAtHeightH :: BlockHeight -> Memory -> Maybe [BlockHash] getBlocksAtHeightH h = M.lookup h . hHeight getBlockH :: BlockHash -> Memory -> Maybe BlockData getBlockH h = M.lookup h . hBlock getTxDataH :: TxHash -> Memory -> Maybe TxData getTxDataH t = M.lookup t . hTx getSpenderH :: OutPoint -> Memory -> Maybe (Dirty Spender) getSpenderH op db = do m <- M.lookup (outPointHash op) (hSpender db) I.lookup (fromIntegral (outPointIndex op)) m getSpendersH :: TxHash -> Memory -> IntMap (Dirty Spender) getSpendersH t = M.lookupDefault I.empty t . hSpender getBalanceH :: Address -> Memory -> Maybe BalVal getBalanceH a = M.lookup a . hBalance getMempoolH :: Memory -> Maybe [TxRef] getMempoolH = hMempool setBestH :: BlockHash -> Memory -> Memory setBestH h db = db {hBest = Just h} insertBlockH :: BlockData -> Memory -> Memory insertBlockH bd db = db { hBlock = M.insert (headerHash (blockDataHeader bd)) bd (hBlock db) } setBlocksAtHeightH :: [BlockHash] -> BlockHeight -> Memory -> Memory setBlocksAtHeightH hs g db = db {hHeight = M.insert g hs (hHeight db)} insertTxH :: TxData -> Memory -> Memory insertTxH tx db = db {hTx = M.insert (txHash (txData tx)) tx (hTx db)} insertSpenderH :: OutPoint -> Spender -> Memory -> Memory insertSpenderH op s db = db { hSpender = M.insertWith (<>) (outPointHash op) (I.singleton (fromIntegral (outPointIndex op)) (Modified s)) (hSpender db) } deleteSpenderH :: OutPoint -> Memory -> Memory deleteSpenderH op db = db { hSpender = M.insertWith (<>) (outPointHash op) (I.singleton (fromIntegral (outPointIndex op)) Deleted) (hSpender db) } setBalanceH :: Balance -> Memory -> Memory setBalanceH bal db = db {hBalance = M.insert (balanceAddress bal) b (hBalance db)} where b = balanceToVal bal insertAddrTxH :: Address -> TxRef -> Memory -> Memory insertAddrTxH a btx db = db { hAddrTx = M.insertWith (<>) a (M.singleton btx (Modified ())) (hAddrTx db) } deleteAddrTxH :: Address -> TxRef -> Memory -> Memory deleteAddrTxH a btx db = db { hAddrTx = M.insertWith (<>) a (M.singleton btx Deleted) (hAddrTx db) } insertAddrUnspentH :: Address -> Unspent -> Memory -> Memory insertAddrUnspentH a u db = let uns = OutVal { outValAmount = unspentAmount u , outValScript = B.Short.fromShort (unspentScript u) } s = M.singleton a (M.singleton (unspentBlock u) (M.singleton (unspentPoint u) (Modified uns))) in db { hAddrOut = M.unionWith (M.unionWith M.union) s (hAddrOut db) } deleteAddrUnspentH :: Address -> Unspent -> Memory -> Memory deleteAddrUnspentH a u db = let s = M.singleton a (M.singleton (unspentBlock u) (M.singleton (unspentPoint u) Deleted)) in db {hAddrOut = M.unionWith (M.unionWith M.union) s (hAddrOut db)} setMempoolH :: [TxRef] -> Memory -> Memory setMempoolH xs db = db {hMempool = Just xs} getUnspentH :: OutPoint -> Memory -> Maybe (Dirty UnspentVal) getUnspentH op db = do m <- M.lookup (outPointHash op) (hUnspent db) I.lookup (fromIntegral (outPointIndex op)) m insertUnspentH :: Unspent -> Memory -> Memory insertUnspentH u db = db { hUnspent = M.insertWith (<>) (outPointHash (unspentPoint u)) (I.singleton (fromIntegral (outPointIndex (unspentPoint u))) (Modified (snd (unspentToVal u)))) (hUnspent db) } deleteUnspentH :: OutPoint -> Memory -> Memory deleteUnspentH op db = db { hUnspent = M.insertWith (<>) (outPointHash op) (I.singleton (fromIntegral (outPointIndex op)) Deleted) (hUnspent db) }