haskoin-store-1.9.0: src/Haskoin/Store/Fleet/Pool.hs
-- |
-- Module : Haskoin.Store.Fleet.Pool
-- Copyright : (c) Haskoin Developers
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- The mempool that stores sharing one Redis answer from, as pure decisions:
-- the second part of "A fleet that answers as one" in
-- @docs\/layered-database.md@, after the fleet tip of "Haskoin.Store.Fleet".
-- "Haskoin.Store.Fleet.Redis" carries it out.
--
-- = The problem
--
-- Every store has a mempool of its own, filled in the order in which its
-- peers sent it transactions, and no two are the same. Stores that answer
-- from the same block still answer differently if each lays its own mempool
-- on top.
--
-- = One pool, the mempool as of the fleet tip
--
-- The stores keep one pool in Redis and all answer from it. It goes with
-- the fleet tip: it holds transactions that are valid on top of the chain
-- up to that block, and it changes with the tip in the same step, so that
-- no request sees a block without what it did to the pool.
--
-- A store's own mempool does not go away. It is what the store offers to
-- the pool and relays to its peers, and what it answers from when Redis is
-- down.
--
-- = The rules
--
-- * __A store offers what it has__ ('offer'). Each input of the transaction
-- must spend an output of a transaction of the pool, or one that the store
-- finds unspent in its view at the fleet tip. A store that is ahead of the
-- tip holds back what only makes sense after its newer blocks.
--
-- * __The first of two that spend the same output stays__, unless the store
-- that offers the second names the first as replaced, having replaced it
-- by its own rules. This is a rule, not a truth, as it is for a node that
-- keeps the first of two conflicting transactions that it hears of.
--
-- * __A transaction leaves only for a reason__ ('withdraw', 'advance'): it
-- was replaced, it is too old, or the tip moved to a block that confirms
-- it or spends what it spends. A store that simply does not have a
-- transaction leaves it alone, because another store has it.
--
-- * __Whatever goes takes along what spends it.__ A transaction whose
-- parent left unconfirmed spends an output that does not exist.
--
-- * __The pool follows the tip__ ('advance'). The store that moves the tip
-- says what the blocks in between did: which transactions they confirmed
-- and which outputs they spent, and, across a reorganisation, which
-- transactions of the abandoned blocks come back and which are gone for
-- good, as their coinbases are.
--
-- Transactions and outputs can be of any type that can be compared; the
-- pool only needs to know which transaction made an output.
module Haskoin.Store.Fleet.Pool
( -- * The pool
Pool (..),
empty,
member,
descendants,
-- * Offering and withdrawing
Offer (..),
Refusal (..),
offer,
withdraw,
-- * Following the tip
Change (..),
advance,
-- * What the pool holds for each address
Entry (..),
AddressPool (..),
Totals (..),
addresses,
totals,
-- * Replacing by fee
replaceable,
)
where
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Ord (Down (..))
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Word (Word64)
-- | The shared mempool, for transactions of type @t@ and outputs of type
-- @o@. The second and third fields follow from the first, and are kept so
-- that nothing has to be searched for.
data Pool t o = Pool
{ -- | the outputs that each transaction spends
spends :: !(Map t [o]),
-- | the transaction that spends each output that is spent
spender :: !(Map o t),
-- | the transactions of the pool that spend outputs of each transaction,
-- whether that one is in the pool or in the chain: a confirmed
-- transaction can come back in a reorganisation, and what spends it must
-- then be found again
children :: !(Map t (Set t))
}
deriving (Eq, Show)
-- | A pool with nothing in it.
empty :: Pool t o
empty = Pool {spends = Map.empty, spender = Map.empty, children = Map.empty}
-- | Whether a transaction is in the pool.
member :: (Ord t) => t -> Pool t o -> Bool
member t pool = Map.member t pool.spends
-- | The transactions of the pool that spend outputs of the given ones, or
-- of those in turn, with the given ones that are in the pool.
descendants :: (Ord t) => [t] -> Pool t o -> Set t
descendants from pool = go Set.empty (filter (`member` pool) from)
where
go seen [] = seen
go seen (t : rest)
| Set.member t seen = go seen rest
| otherwise = go (Set.insert t seen) (maybe [] Set.toList (Map.lookup t pool.children) <> rest)
-- | Why a transaction was not taken.
data Refusal t o
= -- | it is in the pool already, which is no harm
Known
| -- | these transactions of the pool spend what it spends, and were there
-- first
Conflict ![t]
| -- | these inputs spend outputs that are neither made by the pool nor
-- unspent at the tip
Orphan ![o]
deriving (Eq, Show)
-- | What became of an offer.
data Offer t o
= Taken !(Pool t o)
| Refused !(Refusal t o)
deriving (Eq, Show)
-- | Offer a transaction to the pool, given the transaction that made each
-- output, a test for the outputs that the offering store finds unspent in
-- its view at the fleet tip, the outputs that the transaction spends, and
-- the transactions of the pool that the store replaced with it.
--
-- Those leave first, with what spends them. The transaction is then taken
-- if nothing left in the pool spends what it spends, and if each of its
-- inputs is made by the pool or unspent at the tip.
offer :: (Ord t, Ord o) => (o -> t) -> (o -> Bool) -> t -> [o] -> [t] -> Pool t o -> Offer t o
offer maker unspent t inputs replaced pool0
| member t pool0 = Refused Known
| not (null rivals) = Refused (Conflict rivals)
| not (null orphans) = Refused (Orphan orphans)
| otherwise = Taken (insert maker t inputs pool)
where
pool = withdraw replaced pool0
rivals = Set.toList (Set.fromList (mapMaybe (`Map.lookup` pool.spender) inputs))
orphans = [o | o <- inputs, not (member (maker o) pool), not (unspent o)]
-- | Put a transaction in, without asking.
insert :: (Ord t, Ord o) => (o -> t) -> t -> [o] -> Pool t o -> Pool t o
insert maker t inputs pool =
Pool
{ spends = Map.insert t inputs pool.spends,
spender = foldl' (\m o -> Map.insert o t m) pool.spender inputs,
children = foldl' (\m o -> Map.insertWith Set.union (maker o) (Set.singleton t) m) pool.children inputs
}
-- | Take transactions out of the pool, with everything that spends them.
withdraw :: (Ord t, Ord o) => [t] -> Pool t o -> Pool t o
withdraw ts pool = foldl' (flip remove) pool (Set.toList (descendants ts pool))
-- | Take one transaction out, leaving what spends it.
remove :: (Ord t, Ord o) => t -> Pool t o -> Pool t o
remove t pool =
Pool
{ spends = Map.delete t pool.spends,
spender = foldl' (flip Map.delete) pool.spender inputs,
children = Map.filter (not . Set.null) (Map.map (Set.delete t) pool.children)
}
where
inputs = Map.findWithDefault [] t pool.spends
-- | What the blocks between two tips did, as far as the pool is concerned.
data Change t o = Change
{ -- | transactions of abandoned blocks that can be unconfirmed again, with
-- what they spend, in an order in which each comes after those it spends
returned :: ![(t, [o])],
-- | transactions of abandoned blocks that cannot, such as coinbases:
-- their outputs no longer exist
gone :: ![t],
-- | transactions that the new blocks confirm
confirmed :: ![t],
-- | outputs that the transactions of the new blocks spend
spent :: ![o]
}
deriving (Eq, Show)
-- | Bring the pool from one tip to another.
--
-- The abandoned blocks come first: their transactions that can come back
-- do, putting out whatever in the pool spends what they spend, and what
-- spends the others leaves. Then the new blocks: what they
-- confirm leaves the pool, alone, since what spends it now spends an output
-- of the chain; and what spends an output that they spend leaves with
-- everything that spends it.
advance :: (Ord t, Ord o) => (o -> t) -> Change t o -> Pool t o -> Pool t o
advance maker change pool0 = clashed
where
back = foldl' comeBack pool0 change.returned
-- a transaction that was confirmed has the better claim to what it
-- spends: a transaction of the pool that spends the same cannot have
-- been valid, and leaves
comeBack p (t, inputs)
| member t p = p
| otherwise = insert maker t inputs (withdraw (mapMaybe (`Map.lookup` p.spender) inputs) p)
orphaned = concatMap (\t -> maybe [] Set.toList (Map.lookup t back.children)) change.gone
alive = withdraw orphaned back
settled = foldl' (flip remove) alive change.confirmed
clashed = withdraw (mapMaybe (`Map.lookup` settled.spender) change.spent) settled
-- | What a transaction does at an address, for addresses of type @a@: the
-- entries by address of "Haskoin.Store.Database.Mempool", which a store
-- works out from the transaction alone and gives to the pool with it.
data Entry a o
= -- | it spends from the address or pays to it
Touches !a
| -- | it spends this output of the address, of this amount
Spends !a !o !Word64
| -- | it pays this output to the address, with this amount
Pays !a !o !Word64
deriving (Eq, Show)
-- | What the pool holds for an address: the union of the entries of its
-- transactions at that address, in the order in which queries list them.
data AddressPool t o = AddressPool
{ -- | the transactions that touch it with the time each entered a
-- mempool, newest first, and in the order of their ids within a time
txs :: ![(Word64, t)],
-- | the outputs paid to it, in the same order, with their amounts
paid :: ![(Word64, o, Word64)],
-- | the outputs of it that are spent, with their amounts
spent :: !(Map o Word64)
}
deriving (Eq, Show)
-- | The sums that the pool keeps for an address, so that a balance does
-- not take reading its entries: the summary of
-- 'Haskoin.Store.Database.Types.MemSummary'.
data Totals = Totals
{ txs :: !Word64,
paid :: !Word64,
paidCount :: !Word64,
spent :: !Word64,
spentCount :: !Word64,
-- | amount of the outputs that the pool both pays and spends
both :: !Word64,
bothCount :: !Word64
}
deriving (Eq, Show)
-- | What a pool holds for each address, given its transactions, each with
-- the time it entered a mempool and its entries. This is the specification
-- of what "Haskoin.Store.Fleet.Redis" keeps up to date as transactions come
-- and go: it depends on which transactions are in the pool and on nothing
-- else, so the tests work it out again after every step and compare.
addresses :: (Ord a, Ord t, Ord o) => [(t, Word64, [Entry a o])] -> Map a (AddressPool t o)
addresses members = Map.map ordered (Map.fromListWith merge (concatMap single members))
where
single (t, time, entries) = map (one t time) entries
one t time = \case
Touches a -> (a, AddressPool [(time, t)] [] Map.empty)
Spends a o v -> (a, AddressPool [] [] (Map.singleton o v))
Pays a o v -> (a, AddressPool [] [(time, o, v)] Map.empty)
merge x y = AddressPool (x.txs <> y.txs) (x.paid <> y.paid) (Map.union x.spent y.spent)
ordered p =
AddressPool
(Map.elems (Map.fromList [((Down time, t), (time, t)) | (time, t) <- p.txs]))
(Map.elems (Map.fromList [((Down time, o), (time, o, v)) | (time, o, v) <- p.paid]))
p.spent
-- | The sums of what a pool holds for an address.
totals :: (Ord o) => AddressPool t o -> Totals
totals p =
Totals
{ txs = fromIntegral (length p.txs),
paid = sum [v | (_, _, v) <- p.paid],
paidCount = fromIntegral (length p.paid),
spent = sum (Map.elems p.spent),
spentCount = fromIntegral (Map.size p.spent),
both = sum inside,
bothCount = fromIntegral (length inside)
}
where
inside = [v | (_, o, v) <- p.paid, Map.member o p.spent]
-- | The transactions of a pool that can be replaced by fee, given those
-- that say so themselves: those, and whatever spends from one that can be.
-- A transaction inherits this from its unconfirmed parents only, so it
-- changes when a parent is confirmed and when one comes back from an
-- abandoned block. This is the specification of what Redis keeps right as
-- transactions come and go.
replaceable :: (Ord t) => (t -> Bool) -> (o -> t) -> Pool t o -> Set t
replaceable signals maker pool = go (Set.filter signals members)
where
members = Map.keysSet pool.spends
inherits found t = any ((`Set.member` found) . maker) (Map.findWithDefault [] t pool.spends)
go found
| grown == found = found
| otherwise = go grown
where
grown = Set.union found (Set.filter (inherits found) members)