haskoin-store-1.9.0: test/Haskoin/Store/BlockStore/PendingSpec.hs
-- | Mempool transactions on their way into the database.
module Haskoin.Store.BlockStore.PendingSpec (spec) where
import Data.ByteString (ByteString)
import Data.HashMap.Strict qualified as HashMap
import Data.HashSet qualified as HashSet
import Data.List (elemIndex)
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..), addUTCTime)
import Haskoin
import Haskoin.Store.BlockStore.Pending
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
t0 :: UTCTime
t0 = UTCTime (fromGregorian 2026 11 9) 0
-- | Transactions where each one spends an output from outside, or an output
-- of an earlier one: parents come first.
family :: Gen [Tx]
family = do
n <- choose (1, 12)
go n []
where
go 0 acc = return (reverse acc)
go k acc = do
parents <- sublistOf (map txHash acc)
let outside = OutPoint (TxHash (sha256 ("outside" :: ByteString))) (fromIntegral k)
ins = [TxIn op "" maxBound | op <- outside : [OutPoint h 0 | h <- parents]]
tx = Tx 2 ins [TxOut 1000 "", TxOut 1000 ""] [] (fromIntegral k)
go (k - 1 :: Int) (tx : acc)
-- | The parents of a transaction among others.
parentsIn :: [Tx] -> Tx -> [TxHash]
parentsIn txs tx = [op.hash | i <- tx.inputs, let op = i.outpoint, op.hash `elem` map txHash txs]
-- | Receive transactions, each missing those of its parents that are not
-- received before it.
receiveAll :: [Tx] -> Pending -> Pending
receiveAll txs p0 = foldl (\p tx -> received t0 tx (HashSet.fromList (parentsIn txs tx)) p) p0 txs
spec :: Spec
spec = describe "Pending transactions" $ do
prop "takes only transactions with no missing parents, parents first" $
forAll family $ \txs -> forAll (sublistOf txs) $ \done -> forAll (choose (0, 20)) $ \n ->
let p = foldr (imported . txHash) (receiveAll txs empty) done
(taken, rest) = takeReady n p
order = map (txHash . (.tx)) taken
in conjoin
[ property (length taken <= n),
property (all (null . (.deps)) taken),
conjoin
[ counterexample "a child before its parent" $
maybe True (> i) (elemIndex h order)
| (i, t) <- zip [0 ..] taken,
h <- [txHash c | c <- txs, txHash t.tx `elem` parentsIn txs c]
],
queued rest === queued p - length taken
]
prop "makes a child ready once all its parents are in the database" $
forAll family $ \txs ->
let p = foldr (imported . txHash) (receiveAll txs empty) txs
in fst (takeReady (length txs) p) `shouldMatchList'` txs
prop "does not request again what is requested or received" $
forAll family $ \txs ->
let hs = map txHash txs
(asked, got) = splitAt (length hs `div` 2) txs
p = receiveAll got (requested t0 (map txHash asked) empty)
in all (`isPending` p) hs .&&. size p === length hs
prop "forgets requests after twenty seconds, and transactions after an hour" $
forAll family $ \txs ->
let hs = map txHash txs
asked = requested t0 hs empty
got = receiveAll txs empty
in conjoin
[ property (all (`isPending` prune (addUTCTime 20 t0) asked) hs),
property (not (any (`isPending` prune (addUTCTime 21 t0) asked) hs)),
queued (prune (addUTCTime 3600 t0) got) === length txs,
queued (prune (addUTCTime 3601 t0) got) === 0
]
prop "drops what reached the database and updates what still waits" $
forAll family $ \txs ->
let p = receiveAll txs empty
(gone, still) = splitAt (length txs `div` 2) (map txHash txs)
p' = refresh (HashMap.fromList ([(h, Nothing) | h <- gone] <> [(h, Just HashSet.empty) | h <- still])) p
in queued p' === length still .&&. null (waiting p')
where
shouldMatchList' taken txs =
HashSet.fromList (map (txHash . (.tx)) taken) === HashSet.fromList (map txHash txs)