{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
module Network.Haskoin.Store.Logic where
import Conduit
import Control.Monad
import Control.Monad.Except
import qualified Data.ByteString as B
import qualified Data.ByteString.Short as B.Short
import Data.Function
import qualified Data.HashMap.Strict as M
import qualified Data.IntMap.Strict as I
import Data.List
import Data.Maybe
import Data.Serialize
import Data.Word
import Database.RocksDB
import Haskoin
import Network.Haskoin.Store.Data
import Network.Haskoin.Store.Data.HashMap
import Network.Haskoin.Store.Data.ImportDB
import UnliftIO
data ImportException
= PrevBlockNotBest !BlockHash
| UnconfirmedCoinbase !TxHash
| BestBlockUnknown
| BestBlockNotFound !BlockHash
| BlockNotBest !BlockHash
| OrphanTx !TxHash
| TxNotFound !TxHash
| NoUnspent !OutPoint
| TxDeleted !TxHash
| TxDoubleSpend !TxHash
| TxConfirmed !TxHash
| OutputOutOfRange !OutPoint
| InsufficientBalance !Address
| InsufficientOutputs !Address
| InsufficientFunds !TxHash
| InitException !InitException
| DuplicatePrevOutput !TxHash
deriving (Show, Read, Eq, Ord, Exception)
initDB ::
(MonadIO m, MonadError ImportException m)
=> Network
-> DB
-> TVar UnspentMap
-> m ()
initDB net db um =
runImportDB db um $ \i ->
isInitialized i >>= \case
Left e -> throwError (InitException e)
Right True -> return ()
Right False -> do
importBlock net i (genesisBlock net) (genesisNode net)
setInit i
newMempoolTx ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> Network
-> i
-> Tx
-> PreciseUnixTime
-> m ()
newMempoolTx net i tx now =
getTransaction i (txHash tx) >>= \case
Just x | not (transactionDeleted x) -> return ()
_ -> go
where
go = do
orp <-
any isNothing <$>
mapM (getTransaction i . outPointHash . prevOutput) (txIn tx)
unless orp f
f = do
us <-
forM (txIn tx) $ \TxIn {prevOutput = op} -> do
t <- getImportTx i (outPointHash op)
getTxOutput (outPointIndex op) t
let ds = map spenderHash (mapMaybe outputSpender us)
if null ds
then importTx i (MemRef now) tx
else g ds
g ds = do
rbf <-
if getReplaceByFee net
then and <$> mapM isrbf ds
else return False
if rbf
then r ds
else n
r ds = do
forM_ ds (deleteTx i False)
importTx i (MemRef now) tx
n = insertDeletedMempoolTx i tx now
isrbf th = transactionRBF <$> getImportTx i th
newBlock ::
(MonadError ImportException m, MonadIO m)
=> Network
-> DB
-> TVar UnspentMap
-> Block
-> BlockNode
-> m ()
newBlock net db um b n = runImportDB db um $ \i -> importBlock net i b n
revertBlock ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> BlockHash
-> m ()
revertBlock i bh = do
bd <-
getBestBlock i >>= \case
Nothing -> throwError BestBlockUnknown
Just h ->
getBlock i h >>= \case
Nothing -> throwError (BestBlockNotFound h)
Just b
| h == bh -> return b
| otherwise -> throwError (BlockNotBest bh)
forM_ (blockDataTxs bd) $ deleteTx i False
setBest i (prevBlock (blockDataHeader bd))
insertBlock i bd {blockDataMainChain = False}
importBlock ::
(MonadError ImportException m, StoreRead i m, StoreWrite i m, UnspentStore i m)
=> Network
-> i
-> Block
-> BlockNode
-> m ()
importBlock net i b n = do
getBestBlock i >>= \case
Nothing
| isGenesis n -> return ()
| otherwise -> throwError BestBlockUnknown
Just h
| prevBlock (blockHeader b) == h -> return ()
| otherwise ->
throwError (PrevBlockNotBest (prevBlock (nodeHeader n)))
insertBlock
i
BlockData
{ blockDataHeight = nodeHeight n
, blockDataMainChain = True
, blockDataWork = nodeWork n
, blockDataHeader = nodeHeader n
, blockDataSize = fromIntegral (B.length (encode b))
, blockDataTxs = map txHash (blockTxns b)
}
insertAtHeight i (headerHash (nodeHeader n)) (nodeHeight n)
setBest i (headerHash (nodeHeader n))
txs <- concat <$> mapM (getRecursiveTx i . txHash) (tail (blockTxns b))
mapM_ (deleteTx i False . txHash . transactionData) (reverse txs)
zipWithM_ (\x t -> importTx i (br x) t) [0 ..] (blockTxns b)
forM_ txs $ \tr -> do
let tx = transactionData tr
th = txHash tx
when (th `notElem` hs) $
case transactionBlock tr of
MemRef t -> newMempoolTx net i tx t
BlockRef {} -> throwError (TxConfirmed (txHash tx))
where
hs = map txHash (blockTxns b)
br pos = BlockRef {blockRefHeight = nodeHeight n, blockRefPos = pos}
importTx ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> BlockRef
-> Tx
-> m ()
importTx i br tx = do
when (length (nub (map prevOutput (txIn tx))) < length (txIn tx)) $
throwError (DuplicatePrevOutput (txHash tx))
us <-
if iscb
then return []
else forM (txIn tx) $ \TxIn {prevOutput = op} ->
getUnspent i op >>= \case
Nothing
| confirmed br ->
getOutput i op >>= \case
Nothing ->
throwError (OrphanTx (txHash tx))
Just Output {outputSpender = Just s} -> do
deleteTx i False (spenderHash s)
getUnspent i op >>= \case
Nothing ->
throwError
(TxDoubleSpend (txHash tx))
Just u -> return u
Just Output {outputSpender = Nothing} ->
throwError (TxDoubleSpend (txHash tx))
| otherwise -> throwError (NoUnspent op)
Just u -> return u
when (iscb && not (confirmed br)) $
throwError (UnconfirmedCoinbase (txHash tx))
unless iscb $ do
when (sum (map unspentAmount us) < sum (map outValue (txOut tx))) $
throwError (InsufficientFunds th)
zipWithM_
(spendOutput i br (txHash tx))
[0 ..]
us
zipWithM_ (newOutput i br (txHash tx)) [0 ..] (txOut tx)
rbf <- getrbf
insertTx
i
Transaction
{ transactionBlock = br
, transactionVersion = txVersion tx
, transactionLockTime = txLockTime tx
, transactionFee = fee us
, transactionInputs =
if iscb
then zipWith mkcb (txIn tx) ws
else zipWith3 mkin us (txIn tx) ws
, transactionOutputs = map mkout (txOut tx)
, transactionDeleted = False
, transactionRBF = rbf
}
unless (confirmed br) $ insertMempoolTx i (txHash tx) (memRefTime br)
where
th = txHash tx
iscb = all (== nullOutPoint) (map prevOutput (txIn tx))
fee us =
if iscb
then 0
else sum (map unspentAmount us) - sum (map outValue (txOut tx))
ws = map Just (txWitness tx) <> repeat Nothing
getrbf
| iscb = return False
| any ((< 0xffffffff - 1) . txInSequence) (txIn tx) = return True
| confirmed br = return False
| otherwise =
let hs = nub $ map (outPointHash . prevOutput) (txIn tx)
in fmap or . forM hs $ \h ->
getTransaction i h >>= \case
Nothing -> throwError (TxNotFound h)
Just t
| confirmed (transactionBlock t) -> return False
| transactionRBF t -> return True
| otherwise -> return False
mkcb ip w =
Coinbase
{ inputPoint = prevOutput ip
, inputSequence = txInSequence ip
, inputSigScript = scriptInput ip
, inputWitness = w
}
mkin u ip w =
Input
{ inputPoint = prevOutput ip
, inputSequence = txInSequence ip
, inputSigScript = scriptInput ip
, inputPkScript = B.Short.fromShort (unspentScript u)
, inputAmount = unspentAmount u
, inputWitness = w
}
mkout o =
Output
{ outputAmount = outValue o
, outputScript = scriptOutput o
, outputSpender = Nothing
}
getRecursiveTx :: (Monad m, StoreRead i m) => i -> TxHash -> m [Transaction]
getRecursiveTx i th =
getTransaction i th >>= \case
Nothing -> return []
Just t ->
fmap (t :) $ do
let ss =
nub
(map spenderHash
(mapMaybe outputSpender (transactionOutputs t)))
concat <$> mapM (getRecursiveTx i) ss
deleteTx ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> Bool
-> TxHash
-> m ()
deleteTx i mo h =
getTransaction i h >>= \case
Nothing -> throwError (TxNotFound h)
Just t
| not (transactionDeleted t) ->
if mo && confirmed (transactionBlock t)
then throwError (TxConfirmed h)
else go t
| otherwise -> return ()
where
go t = do
forM_ (mapMaybe outputSpender (transactionOutputs t)) $ \s ->
deleteTx i False (spenderHash s)
forM_ (take (length (transactionOutputs t)) [0 ..]) $ \n ->
delOutput i (OutPoint h n)
let ps = filter (/= nullOutPoint) (map inputPoint (transactionInputs t))
forM_ ps $ \op -> unspendOutput i op (transactionBlock t)
unless (confirmed (transactionBlock t)) $
deleteMempoolTx i h (memRefTime (transactionBlock t))
insertTx i t {transactionDeleted = True}
insertDeletedMempoolTx ::
(MonadError ImportException m, StoreRead i m, StoreWrite i m)
=> i
-> Tx
-> PreciseUnixTime
-> m ()
insertDeletedMempoolTx i tx now = do
us <-
forM (txIn tx) $ \TxIn {prevOutput = op} ->
getImportTx i (outPointHash op) >>= getTxOutput (outPointIndex op)
rbf <- getrbf
insertTx
i
Transaction
{ transactionBlock = MemRef now
, transactionVersion = txVersion tx
, transactionLockTime = txLockTime tx
, transactionFee = fee us
, transactionInputs = zipWith3 mkin us (txIn tx) ws
, transactionOutputs = map mkout (txOut tx)
, transactionDeleted = True
, transactionRBF = rbf
}
where
ws = map Just (txWitness tx) <> repeat Nothing
getrbf
| any ((< 0xffffffff - 1) . txInSequence) (txIn tx) = return True
| otherwise =
let hs = nub $ map (outPointHash . prevOutput) (txIn tx)
in fmap or . forM hs $ \h ->
getTransaction i h >>= \case
Nothing -> throwError (TxNotFound h)
Just t
| confirmed (transactionBlock t) -> return False
| transactionRBF t -> return True
| otherwise -> return False
fee us = sum (map outputAmount us) - sum (map outValue (txOut tx))
mkin u ip w =
Input
{ inputPoint = prevOutput ip
, inputSequence = txInSequence ip
, inputSigScript = scriptInput ip
, inputPkScript = outputScript u
, inputAmount = outputAmount u
, inputWitness = w
}
mkout o =
Output
{ outputAmount = outValue o
, outputScript = scriptOutput o
, outputSpender = Nothing
}
newOutput ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> BlockRef
-> TxHash
-> Word32
-> TxOut
-> m ()
newOutput i tb th ix to = do
addUnspent i u
case scriptToAddressBS (scriptOutput to) of
Left _ -> return ()
Right a -> do
insertAddrUnspent i a u
insertAddrTx
i
AddressTx
{ addressTxAddress = a
, addressTxHash = th
, addressTxBlock = tb
}
increaseBalance i (confirmed tb) a (outValue to)
where
u =
Unspent
{ unspentBlock = tb
, unspentAmount = outValue to
, unspentScript = B.Short.toShort (scriptOutput to)
, unspentPoint = OutPoint th ix
}
delOutput ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> OutPoint
-> m ()
delOutput i op = do
t <- getImportTx i (outPointHash op)
u <- getTxOutput (outPointIndex op) t
delUnspent i op
case scriptToAddressBS (outputScript u) of
Left _ -> return ()
Right a -> do
removeAddrUnspent
i
a
Unspent
{ unspentScript = B.Short.toShort (outputScript u)
, unspentBlock = transactionBlock t
, unspentPoint = op
, unspentAmount = outputAmount u
}
removeAddrTx
i
AddressTx
{ addressTxAddress = a
, addressTxHash = outPointHash op
, addressTxBlock = transactionBlock t
}
reduceBalance
i
(confirmed (transactionBlock t))
a
(outputAmount u)
getImportTx ::
(MonadError ImportException m, StoreRead i m)
=> i
-> TxHash
-> m Transaction
getImportTx i th =
getTransaction i th >>= \case
Nothing -> throwError $ TxNotFound th
Just t
| transactionDeleted t -> throwError $ TxDeleted th
| otherwise -> return t
getTxOutput ::
(MonadError ImportException m) => Word32 -> Transaction -> m Output
getTxOutput i tx = do
unless (fromIntegral i < length (transactionOutputs tx)) $
throwError $
OutputOutOfRange
OutPoint
{outPointHash = txHash (transactionData tx), outPointIndex = i}
return $ transactionOutputs tx !! fromIntegral i
spendOutput ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> BlockRef
-> TxHash
-> Word32
-> Unspent
-> m ()
spendOutput i tb th ix u = do
insertOutput
i
(unspentPoint u)
Output
{ outputAmount = unspentAmount u
, outputScript = B.Short.fromShort (unspentScript u)
, outputSpender = Just Spender {spenderHash = th, spenderIndex = ix}
}
delUnspent i (unspentPoint u)
case scriptToAddressBS (B.Short.fromShort (unspentScript u)) of
Left _ -> return ()
Right a -> do
removeAddrUnspent i a u
reduceBalance i (confirmed tb) a (unspentAmount u)
insertAddrTx
i
AddressTx
{ addressTxAddress = a
, addressTxHash = th
, addressTxBlock = tb
}
unspendOutput ::
( MonadError ImportException m
, StoreRead i m
, StoreWrite i m
, UnspentStore i m
)
=> i
-> OutPoint
-> BlockRef
-> m ()
unspendOutput i op br = do
tx <- getImportTx i (outPointHash op)
out <- getTxOutput (outPointIndex op) tx
when (isJust (outputSpender out)) $ do
insertTx
i
tx {transactionOutputs = zipWith f [0 ..] (transactionOutputs tx)}
let u =
Unspent
{ unspentAmount = outputAmount out
, unspentBlock = transactionBlock tx
, unspentScript = B.Short.toShort (outputScript out)
, unspentPoint = op
}
addUnspent i u
case scriptToAddressBS (outputScript out) of
Left _ -> return ()
Right a -> do
insertAddrUnspent i a u
increaseBalance i (confirmed br) a (outputAmount out)
where
f n o
| n == outPointIndex op = o {outputSpender = Nothing}
| otherwise = o
reduceBalance ::
(MonadError ImportException m, StoreRead i m, StoreWrite i m)
=> i
-> Bool
-> Address
-> Word64
-> m ()
reduceBalance i c a v = do
b <- getBalance i a
setBalance i =<<
if c
then do
unless (v <= balanceAmount b) $
throwError (InsufficientBalance a)
unless (balanceCount b > 0) $ throwError (InsufficientOutputs a)
return
b
{ balanceAmount = balanceAmount b - v
, balanceCount = balanceCount b - 1
}
else do
unless
(fromIntegral v <=
fromIntegral (balanceAmount b) + balanceZero b) $
throwError (InsufficientBalance a)
unless (balanceCount b > 0) $ throwError (InsufficientOutputs a)
return
b
{ balanceZero = balanceZero b - fromIntegral v
, balanceCount = balanceCount b - 1
}
increaseBalance ::
(MonadError ImportException m, StoreRead i m, StoreWrite i m)
=> i
-> Bool
-> Address
-> Word64
-> m ()
increaseBalance i c a v = do
b <- getBalance i a
setBalance i $
if c
then b
{ balanceAmount = balanceAmount b + v
, balanceCount = balanceCount b + 1
}
else b
{ balanceZero = balanceZero b + fromIntegral v
, balanceCount = balanceCount b + 1
}
pruneUnspentMap :: UnspentMap -> UnspentMap
pruneUnspentMap um
| M.size um > 2000 * 1000 =
let f is = unspentBlock (head (I.elems is))
ls =
sortBy
(compare `on` (f . snd))
(filter (not . I.null . snd) (M.toList um))
in M.fromList (drop (1000 * 1000) ls)
| otherwise = um