haskoin-store-1.9.0: src/Haskoin/Store/Fleet/Redis.hs
{-# LANGUAGE TemplateHaskell #-}
-- |
-- Module : Haskoin.Store.Fleet.Redis
-- Copyright : (c) Haskoin Developers
-- License : MIT
-- Maintainer : jprupp@protonmail.ch
-- Stability : experimental
-- Portability : POSIX
--
-- The rules of "Haskoin.Store.Fleet" carried out against Redis: the state
-- of a fleet in three keys, and a Lua script for each step that has to be
-- one step.
--
-- = Why scripts
--
-- Whether a store may report depends on the tip and on the leases of the
-- others, and the block that the tip moves to depends on every report. Read
-- and written in separate commands, each could change in between: two
-- stores would both find themselves alone, or the tip would move to a block
-- that a store had just stopped reporting. Redis runs a script without
-- letting anything else in, so each rule sees one state and changes it at
-- once.
--
-- = Whose clock
--
-- Leases are measured on the clock of Redis, read inside the scripts. The
-- stores then need not agree on the time, only on which Redis they share. A
-- time can be given instead, which is how the tests make leases run out
-- without waiting.
--
-- = Blocks and stores
--
-- A block is its hash, or any bytes of one fixed size, and a store is a
-- name that no other store of the fleet has.
--
-- = The shared mempool
--
-- The rules of "Haskoin.Store.Fleet.Pool" are one more script, with the
-- pool in keys next to those of the tip. A transaction is its id, of one
-- fixed size, and an output is the id of the transaction that made it and
-- four more bytes. An offer says which tip the store checked the
-- transaction against, and is turned away if the tip has moved since: what
-- was unspent at one block may be spent at the next. Moving the tip and
-- changing the pool for the blocks in between is one step ('fleetAdvance'),
-- so that no request finds a block without what it did to the pool.
--
-- A transaction comes with what it does at each address (t'PoolEntry'),
-- and the script keeps what the pool holds for each address in step: its
-- transactions and the outputs paid to it, each in a sorted set ordered as
-- queries list them, the outputs of it that are spent, and the sums that a
-- balance needs. 'Haskoin.Store.Fleet.Pool.addresses' says what those
-- should be. Kept as the transactions come and go, they cost a few commands
-- for each entry; worked out when asked, a balance would cost a read of
-- every entry of the address, which is what the store's own mempool area
-- did at first and was measured to be thousands of times too slow for a
-- busy address.
module Haskoin.Store.Fleet.Redis
( FleetKeys (..),
fleetKeys,
Moved (..),
fleetReport,
fleetMove,
fleetTip,
fleetLive,
-- * The shared mempool
Offered (..),
PoolChange (..),
PoolTx (..),
PoolEntry (..),
poolOffer,
poolWithdraw,
fleetAdvance,
FleetEvent (..),
fleetEventsEnd,
fleetEventsAfter,
poolSpends,
poolBody,
poolRead,
)
where
import Data.ByteString (ByteString)
import Data.ByteString qualified as B
import Data.ByteString.Char8 qualified as C
import Data.FileEmbed (embedFile, makeRelativeToProject)
import Data.Word (Word64)
import Database.Redis (Connection, Redis, Reply)
import Database.Redis qualified as R
import Haskoin.Store.Fleet (Time)
-- | The keys of a fleet in Redis, and the size of its blocks in bytes.
data FleetKeys = FleetKeys
{ -- | what every key of the fleet starts with
prefix :: !ByteString,
-- | the fleet tip: a block
tip :: !ByteString,
-- | a hash from each store to the time its lease runs out, in
-- milliseconds
lease :: !ByteString,
-- | a hash from each store to its recent blocks, newest first, one
-- after the other
recent :: !ByteString,
-- | bytes in a block
size :: !Int
}
-- | The keys of the fleet under a prefix, such as that of the cache of a
-- network, for blocks of a size.
fleetKeys :: ByteString -> Int -> FleetKeys
fleetKeys prefix size =
FleetKeys
{ prefix,
tip = prefix <> "fleet:tip",
lease = prefix <> "fleet:lease",
recent = prefix <> "fleet:recent",
size
}
-- | What became of an attempt to move the tip; see 'Haskoin.Store.Fleet.Move'.
data Moved
= -- | the tip is now this block
MovedTo !ByteString
| -- | the tip is where the live stores agree, or they agree on nothing
Stays
| -- | another store moved the tip first
Raced
deriving (Eq, Show)
-- | Report the recent blocks of a store, newest first, under a lease of so
-- many milliseconds, given the blocks that the store has views of. Says
-- whether the report was taken: it is not when the store has no view of the
-- tip and another store is live ('Haskoin.Store.Fleet.mayReport').
fleetReport :: Connection -> FleetKeys -> Maybe Time -> Time -> ByteString -> [ByteString] -> [ByteString] -> IO Bool
fleetReport conn keys clock term store blocks views =
(== 1) <$> run conn (R.eval reportScript (scriptKeys keys) args :: Redis (Either Reply Integer))
where
args = [store, num term, maybe "" num clock, num keys.size, B.concat blocks, B.concat views]
-- | Move the tip to the newest block that every live store reported, given
-- the tip that the caller saw.
fleetMove :: Connection -> FleetKeys -> Maybe Time -> Maybe ByteString -> IO Moved
fleetMove conn keys clock seen =
run conn (R.eval moveScript (scriptKeys keys) args) >>= \case
["moved", block] -> return (MovedTo block)
["stays"] -> return Stays
["raced"] -> return Raced
other -> fail ("Unexpected answer of the fleet script: " <> show (other :: [ByteString]))
where
args = [maybe "" id seen, maybe "" num clock, num keys.size]
-- | The fleet tip, if there is one.
fleetTip :: Connection -> FleetKeys -> IO (Maybe ByteString)
fleetTip conn keys = run conn (R.get keys.tip)
-- | The stores that have a lease, with the time each runs out, in
-- milliseconds. Leases that ran out are in it until the next report.
fleetLive :: Connection -> FleetKeys -> IO [(ByteString, Time)]
fleetLive conn keys = map (fmap (maybe 0 (fromIntegral . fst) . C.readInteger)) <$> run conn (R.hgetall keys.lease)
-- | What became of an offer; see 'Haskoin.Store.Fleet.Pool.Offer'.
data Offered
= -- | it is in the pool now
Taken
| -- | it was there already
Known
| -- | these transactions of the pool spend what it spends
Conflict ![ByteString]
| -- | these outputs are neither made by the pool nor unspent at the tip
Orphan ![ByteString]
| -- | the tip is not the one that the store checked the transaction
-- against: check again and offer again
Late
deriving (Eq, Show)
-- | What a transaction does at an address; see
-- 'Haskoin.Store.Fleet.Pool.Entry'. Addresses, outputs and scripts are
-- whatever bytes the stores of a fleet agree on.
data PoolEntry
= -- | it spends from the address or pays to it
PoolTouches !ByteString
| -- | it spends this output of the address, of this amount
PoolSpends !ByteString !ByteString !Word64
| -- | it pays this output to the address, with this amount and script
PoolPays !ByteString !ByteString !Word64 !ByteString
deriving (Eq, Show)
-- | A transaction as the pool keeps it.
data PoolTx = PoolTx
{ txid :: !ByteString,
-- | when it entered the mempool of the store that offers it. The pool
-- keeps the time of the first offer, so that every store lists the
-- transaction in the same place.
time :: !Word64,
-- | the outputs that it spends, in the order of its inputs
inputs :: ![ByteString],
-- | what a store needs to answer about it
body :: !ByteString,
-- | whether it says itself that it can be replaced by fee. Whether it
-- can also depends on the transactions of the pool that it spends
-- from, which the pool works out and keeps right as those come and go.
signals :: !Bool,
-- | what it does at each address
entries :: ![PoolEntry]
}
deriving (Eq, Show)
-- | What the blocks between two tips did; see
-- 'Haskoin.Store.Fleet.Pool.Change'.
data PoolChange = PoolChange
{ -- | transactions that come back
returned :: ![PoolTx],
-- | transactions of abandoned blocks that cannot come back
gone :: ![ByteString],
-- | transactions that the new blocks confirm
confirmed :: ![ByteString],
-- | outputs that the new blocks spend
spent :: ![ByteString]
}
deriving (Eq, Show)
-- | Offer a transaction to the pool: given the size of a transaction id,
-- the tip that the store checked against, the transaction, whether the
-- store finds each output that it spends unspent at that tip, in the order
-- of its inputs, and the transactions that it replaces.
poolOffer ::
Connection ->
FleetKeys ->
Int ->
Maybe ByteString ->
PoolTx ->
[Bool] ->
[ByteString] ->
IO Offered
poolOffer conn keys tsize seen tx unspents replaced =
run conn (R.eval poolScript [] args) >>= \case
["taken"] -> return Taken
["known"] -> return Known
"conflict" : rivals -> return (Conflict rivals)
"orphan" : outputs -> return (Orphan outputs)
["raced"] -> return Late
other -> fail ("Unexpected answer of the pool script: " <> show (other :: [ByteString]))
where
flags = C.pack [if unspent then '1' else '0' | unspent <- unspents]
args = ["offer", keys.prefix, num tsize, maybe "" id seen, flags, B.concat replaced] <> txArgs tx
-- | Take transactions out of the pool, with all that spends them. Gives
-- those that left.
poolWithdraw :: Connection -> FleetKeys -> Int -> [ByteString] -> IO [ByteString]
poolWithdraw conn keys tsize ts =
run conn (R.eval poolScript [] ["withdraw", keys.prefix, num tsize, B.concat ts])
-- | Move the tip to a block and bring the pool there, in one step: given
-- the tip that the caller saw, the block, which must be the one that the
-- live stores agree on, and what the blocks in between did.
--
-- A store that cannot say what they did, because it does not have the old
-- tip or because the way from it is too long, gives 'Nothing', and the pool
-- is emptied: but only if no other store is live, as when a fleet comes
-- back after all its stores were away. The stores then offer their mempools
-- to the empty pool. With another store live the answer is 'Raced', and
-- the tip waits for a store that knows the way.
--
-- The last argument is the blocks that the tip moves through, oldest
-- first and the new tip last, for the events of the fleet ('FleetEvent'):
-- with none given, the new tip is the one block told.
fleetAdvance ::
Connection ->
FleetKeys ->
Int ->
Maybe Time ->
Maybe ByteString ->
ByteString ->
Maybe PoolChange ->
[ByteString] ->
IO Moved
fleetAdvance conn keys tsize clock seen target known path =
run conn (R.eval poolScript [] args) >>= \case
["moved"] -> return (MovedTo target)
["stays"] -> return Stays
["raced"] -> return Raced
other -> fail ("Unexpected answer of the pool script: " <> show (other :: [ByteString]))
where
args =
["advance", keys.prefix, num tsize, maybe "" id seen, target, maybe "" num clock, num keys.size]
<> [maybe "1" (const "0") known]
<> [B.concat change.gone, B.concat change.confirmed, B.concat change.spent]
<> [num (length change.returned)]
<> concatMap txArgs change.returned
<> [B.concat path]
change = maybe (PoolChange [] [] [] []) id known
-- | Read from the pool as it is at a tip: given the size of a transaction
-- id, the tip that the caller answers from, what to read and its arguments,
-- as @read.lua@ takes them. 'Nothing' if the tip is another one by now.
poolRead :: Connection -> FleetKeys -> Int -> Maybe ByteString -> ByteString -> [ByteString] -> IO (Maybe [ByteString])
poolRead conn keys tsize tip what args =
run conn (R.eval readScript [] (what : keys.prefix : num tsize : maybe "" id tip : args)) >>= \case
"ok" : rows -> return (Just rows)
["raced"] -> return Nothing
other -> fail ("Unexpected answer of the read script: " <> show (take 1 other))
-- | Something that happened to the fleet, as every store of it tells its
-- clients.
--
-- A store alone tells what its own node saw. The stores of a fleet answer
-- from one tip and one pool, and their events have to be those of that tip
-- and that pool: otherwise a client would hear of a transaction from one
-- server and find that the next does not have it. So the script that
-- changes the pool or moves the tip appends what it did to a stream in
-- Redis, in the same step, and every store reads the stream.
data FleetEvent
= -- | the fleet tip moved to this block, or through it
FleetBlock !ByteString
| -- | a transaction entered the pool
FleetEntered !ByteString
| -- | a transaction left the pool unconfirmed
FleetLeft !ByteString
deriving (Eq, Ord, Show)
-- | The place in the stream of events after the last event that there is:
-- where a store that starts listening now begins.
fleetEventsEnd :: Connection -> FleetKeys -> IO ByteString
fleetEventsEnd conn keys =
run conn (R.sendRequest ["XREVRANGE", keys.prefix <> "fleet:events", "+", "-", "COUNT", "1"]) >>= \case
R.MultiBulk (Just [R.MultiBulk (Just (R.Bulk (Just at) : _))]) -> return at
_ -> return "0-0"
-- | The events after a place in the stream, each with its place, waiting
-- up to so many milliseconds for one if there is none yet.
fleetEventsAfter :: Connection -> FleetKeys -> ByteString -> Integer -> IO [(ByteString, FleetEvent)]
fleetEventsAfter conn keys from wait =
run conn (R.sendRequest ["XREAD", "BLOCK", num wait, "STREAMS", keys.prefix <> "fleet:events", from]) >>= \case
R.MultiBulk (Just [R.MultiBulk (Just [_, R.MultiBulk (Just records)])]) -> return (concatMap record records)
_ -> return []
where
record = \case
R.MultiBulk (Just [R.Bulk (Just at), R.MultiBulk (Just fields)]) ->
case (lookup "k" (pairs fields), lookup "h" (pairs fields)) of
(Just "b", Just h) -> [(at, FleetBlock h)]
(Just "t", Just h) -> [(at, FleetEntered h)]
(Just "d", Just h) -> [(at, FleetLeft h)]
_ -> []
_ -> []
pairs (R.Bulk (Just k) : R.Bulk (Just v) : more) = (k, v) : pairs more
pairs _ = []
-- | A transaction as the arguments of the pool script.
txArgs :: PoolTx -> [ByteString]
txArgs tx =
[tx.txid, num tx.time, B.concat tx.inputs, tx.body, if tx.signals then "1" else "0", num (length tx.entries)]
<> concatMap entry tx.entries
where
entry = \case
PoolTouches a -> ["t", a, "", "", ""]
PoolSpends a o v -> ["s", a, o, num v, ""]
PoolPays a o v s -> ["o", a, o, num v, s]
-- | The transactions of the pool, each with the outputs that it spends one
-- after the other.
poolSpends :: Connection -> FleetKeys -> IO [(ByteString, ByteString)]
poolSpends conn keys = run conn (R.hgetall (keys.prefix <> "pool:spends"))
-- | The body of a transaction of the pool.
poolBody :: Connection -> FleetKeys -> ByteString -> IO (Maybe ByteString)
poolBody conn keys t = run conn (R.hget (keys.prefix <> "pool:body") t)
scriptKeys :: FleetKeys -> [ByteString]
scriptKeys keys = [keys.tip, keys.lease, keys.recent]
num :: (Show a) => a -> ByteString
num = C.pack . show
-- | Run Redis commands, throwing if Redis answers with an error.
run :: Connection -> Redis (Either Reply a) -> IO a
run conn f =
R.runRedis conn f >>= \case
Right x -> return x
Left e -> fail ("Redis error: " <> show e)
-- | 'Haskoin.Store.Fleet.mayReport' and 'Haskoin.Store.Fleet.report' in one
-- step.
reportScript :: ByteString
reportScript = $(makeRelativeToProject "src/Haskoin/Store/Fleet/lua/report.lua" >>= embedFile)
-- | 'Haskoin.Store.Fleet.move'.
moveScript :: ByteString
moveScript = $(makeRelativeToProject "src/Haskoin/Store/Fleet/lua/move.lua" >>= embedFile)
-- | What a store reads from the pool, each read with a check that the tip
-- is the one that the store answers from.
readScript :: ByteString
readScript = $(makeRelativeToProject "src/Haskoin/Store/Fleet/lua/read.lua" >>= embedFile)
-- | 'Haskoin.Store.Fleet.Pool.offer', 'Haskoin.Store.Fleet.Pool.withdraw',
-- and 'Haskoin.Store.Fleet.Pool.advance' with the move of the tip.
poolScript :: ByteString
poolScript = $(makeRelativeToProject "src/Haskoin/Store/Fleet/lua/pool.lua" >>= embedFile)